Merge "Add support for paths."
diff --git a/Android.mk b/Android.mk
index e0bd348..22216b0 100644
--- a/Android.mk
+++ b/Android.mk
@@ -100,9 +100,11 @@
 	core/java/android/bluetooth/IBluetoothCallback.aidl \
 	core/java/android/bluetooth/IBluetoothHeadset.aidl \
 	core/java/android/bluetooth/IBluetoothPbap.aidl \
+	core/java/android/content/IClipboard.aidl \
 	core/java/android/content/IContentService.aidl \
 	core/java/android/content/IIntentReceiver.aidl \
 	core/java/android/content/IIntentSender.aidl \
+	core/java/android/content/IOnPrimaryClipChangedListener.aidl \
 	core/java/android/content/ISyncAdapter.aidl \
 	core/java/android/content/ISyncContext.aidl \
 	core/java/android/content/ISyncStatusObserver.aidl \
@@ -132,7 +134,6 @@
     core/java/android/service/wallpaper/IWallpaperConnection.aidl \
     core/java/android/service/wallpaper/IWallpaperEngine.aidl \
     core/java/android/service/wallpaper/IWallpaperService.aidl \
-	core/java/android/text/IClipboard.aidl \
 	core/java/android/view/accessibility/IAccessibilityManager.aidl \
 	core/java/android/view/accessibility/IAccessibilityManagerClient.aidl \
 	core/java/android/view/IApplicationToken.aidl \
diff --git a/api/9.xml b/api/9.xml
index 12ef981..abb67f9 100644
--- a/api/9.xml
+++ b/api/9.xml
@@ -303584,8 +303584,6 @@
 </implements>
 <implements name="java.io.Serializable">
 </implements>
-<implements name="java.util.SortedSet">
-</implements>
 <constructor name="TreeSet"
  type="java.util.TreeSet"
  static="false"
@@ -306943,10 +306941,6 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<implements name="java.util.concurrent.Future">
-</implements>
-<implements name="java.lang.Runnable">
-</implements>
 <constructor name="FutureTask"
  type="java.util.concurrent.FutureTask"
  static="false"
@@ -339917,32 +339911,6 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<method name="getLogWriter"
- return="java.io.PrintWriter"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
-<method name="getLoginTimeout"
- return="int"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
 <method name="getPooledConnection"
  return="javax.sql.PooledConnection"
  abstract="true"
@@ -339973,36 +339941,6 @@
 <exception name="SQLException" type="java.sql.SQLException">
 </exception>
 </method>
-<method name="setLogWriter"
- return="void"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<parameter name="theWriter" type="java.io.PrintWriter">
-</parameter>
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
-<method name="setLoginTimeout"
- return="void"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<parameter name="theTimeout" type="int">
-</parameter>
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
 </interface>
 <interface name="DataSource"
  abstract="true"
@@ -340041,62 +339979,6 @@
 <exception name="SQLException" type="java.sql.SQLException">
 </exception>
 </method>
-<method name="getLogWriter"
- return="java.io.PrintWriter"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
-<method name="getLoginTimeout"
- return="int"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
-<method name="setLogWriter"
- return="void"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<parameter name="theWriter" type="java.io.PrintWriter">
-</parameter>
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
-<method name="setLoginTimeout"
- return="void"
- abstract="true"
- native="false"
- synchronized="false"
- static="false"
- final="false"
- deprecated="not deprecated"
- visibility="public"
->
-<parameter name="theTimeout" type="int">
-</parameter>
-<exception name="SQLException" type="java.sql.SQLException">
-</exception>
-</method>
 </interface>
 <interface name="PooledConnection"
  abstract="true"
diff --git a/api/current.xml b/api/current.xml
index b24ba43..767f9af 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -38810,6 +38810,336 @@
 </parameter>
 </method>
 </class>
+<class name="ClipboardManager"
+ extends="android.text.ClipboardManager"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="addPrimaryClipChangedListener"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="what" type="android.content.ClipboardManager.OnPrimaryClipChangedListener">
+</parameter>
+</method>
+<method name="getPrimaryClip"
+ return="android.content.ClippedData"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getText"
+ return="java.lang.CharSequence"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="deprecated"
+ visibility="public"
+>
+</method>
+<method name="hasPrimaryClip"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="hasText"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="removePrimaryClipChangedListener"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="what" type="android.content.ClipboardManager.OnPrimaryClipChangedListener">
+</parameter>
+</method>
+<method name="setPrimaryClip"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="clip" type="android.content.ClippedData">
+</parameter>
+</method>
+<method name="setText"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="deprecated"
+ visibility="public"
+>
+<parameter name="text" type="java.lang.CharSequence">
+</parameter>
+</method>
+</class>
+<interface name="ClipboardManager.OnPrimaryClipChangedListener"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<method name="onPrimaryClipChanged"
+ return="void"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+</interface>
+<class name="ClippedData"
+ extends="java.lang.Object"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.os.Parcelable">
+</implements>
+<constructor name="ClippedData"
+ type="android.content.ClippedData"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="label" type="java.lang.CharSequence">
+</parameter>
+<parameter name="icon" type="android.graphics.Bitmap">
+</parameter>
+<parameter name="item" type="android.content.ClippedData.Item">
+</parameter>
+</constructor>
+<method name="addItem"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="item" type="android.content.ClippedData.Item">
+</parameter>
+</method>
+<method name="describeContents"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getIcon"
+ return="android.graphics.Bitmap"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getItem"
+ return="android.content.ClippedData.Item"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="index" type="int">
+</parameter>
+</method>
+<method name="getItemCount"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getLabel"
+ return="java.lang.CharSequence"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="writeToParcel"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="dest" type="android.os.Parcel">
+</parameter>
+<parameter name="flags" type="int">
+</parameter>
+</method>
+<field name="CREATOR"
+ type="android.os.Parcelable.Creator"
+ transient="false"
+ volatile="false"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+</class>
+<class name="ClippedData.Item"
+ extends="java.lang.Object"
+ abstract="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<constructor name="ClippedData.Item"
+ type="android.content.ClippedData.Item"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="text" type="java.lang.CharSequence">
+</parameter>
+</constructor>
+<constructor name="ClippedData.Item"
+ type="android.content.ClippedData.Item"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="intent" type="android.content.Intent">
+</parameter>
+</constructor>
+<constructor name="ClippedData.Item"
+ type="android.content.ClippedData.Item"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</constructor>
+<constructor name="ClippedData.Item"
+ type="android.content.ClippedData.Item"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="text" type="java.lang.CharSequence">
+</parameter>
+<parameter name="intent" type="android.content.Intent">
+</parameter>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</constructor>
+<method name="getIntent"
+ return="android.content.Intent"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getText"
+ return="java.lang.CharSequence"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getUri"
+ return="android.net.Uri"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+</class>
 <interface name="ComponentCallbacks"
  abstract="true"
  static="false"
@@ -44627,6 +44957,93 @@
 >
 </method>
 </interface>
+<interface name="IOnPrimaryClipChangedListener"
+ abstract="true"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.os.IInterface">
+</implements>
+<method name="dispatchPrimaryClipChanged"
+ return="void"
+ abstract="true"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<exception name="RemoteException" type="android.os.RemoteException">
+</exception>
+</method>
+</interface>
+<class name="IOnPrimaryClipChangedListener.Stub"
+ extends="android.os.Binder"
+ abstract="true"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.content.IOnPrimaryClipChangedListener">
+</implements>
+<constructor name="IOnPrimaryClipChangedListener.Stub"
+ type="android.content.IOnPrimaryClipChangedListener.Stub"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</constructor>
+<method name="asBinder"
+ return="android.os.IBinder"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="asInterface"
+ return="android.content.IOnPrimaryClipChangedListener"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="obj" type="android.os.IBinder">
+</parameter>
+</method>
+<method name="onTransact"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="code" type="int">
+</parameter>
+<parameter name="data" type="android.os.Parcel">
+</parameter>
+<parameter name="reply" type="android.os.Parcel">
+</parameter>
+<parameter name="flags" type="int">
+</parameter>
+<exception name="RemoteException" type="android.os.RemoteException">
+</exception>
+</method>
+</class>
 <class name="Intent"
  extends="java.lang.Object"
  abstract="false"
@@ -166226,15 +166643,23 @@
 </class>
 <class name="ClipboardManager"
  extends="java.lang.Object"
- abstract="false"
+ abstract="true"
+ static="false"
+ final="false"
+ deprecated="deprecated"
+ visibility="public"
+>
+<constructor name="ClipboardManager"
+ type="android.text.ClipboardManager"
  static="false"
  final="false"
  deprecated="not deprecated"
  visibility="public"
 >
+</constructor>
 <method name="getText"
  return="java.lang.CharSequence"
- abstract="false"
+ abstract="true"
  native="false"
  synchronized="false"
  static="false"
@@ -166245,7 +166670,7 @@
 </method>
 <method name="hasText"
  return="boolean"
- abstract="false"
+ abstract="true"
  native="false"
  synchronized="false"
  static="false"
@@ -166256,7 +166681,7 @@
 </method>
 <method name="setText"
  return="void"
- abstract="false"
+ abstract="true"
  native="false"
  synchronized="false"
  static="false"
@@ -211726,6 +212151,22 @@
 <parameter name="defStyle" type="int">
 </parameter>
 </constructor>
+<constructor name="WebView"
+ type="android.webkit.WebView"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="context" type="android.content.Context">
+</parameter>
+<parameter name="attrs" type="android.util.AttributeSet">
+</parameter>
+<parameter name="defStyle" type="int">
+</parameter>
+<parameter name="privateBrowsing" type="boolean">
+</parameter>
+</constructor>
 <method name="addJavascriptInterface"
  return="void"
  abstract="false"
@@ -212214,6 +212655,17 @@
  visibility="public"
 >
 </method>
+<method name="isPrivateBrowsingEnabled"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
 <method name="loadData"
  return="void"
  abstract="false"
@@ -271738,7 +272190,7 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="loop" type="boolean">
+<parameter name="disable" type="boolean">
 </parameter>
 <exception name="SocketException" type="java.net.SocketException">
 </exception>
@@ -273297,7 +273749,7 @@
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="value" type="boolean">
+<parameter name="keepAlive" type="boolean">
 </parameter>
 <exception name="SocketException" type="java.net.SocketException">
 </exception>
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 10ebd60..2786372 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -92,7 +92,7 @@
 import android.os.FileUtils.FileStatus;
 import android.os.storage.StorageManager;
 import android.telephony.TelephonyManager;
-import android.text.ClipboardManager;
+import android.content.ClipboardManager;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 6fe12fc..1fae516 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.content.Context;
+import android.os.Binder;
 import android.os.RemoteException;
 import android.os.Handler;
 import android.os.IBinder;
diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java
new file mode 100644
index 0000000..5371fa5
--- /dev/null
+++ b/core/java/android/content/ClipboardManager.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 android.content;
+
+import android.content.Context;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Interface to the clipboard service, for placing and retrieving text in
+ * the global clipboard.
+ *
+ * <p>
+ * You do not instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService}.
+ *
+ * @see android.content.Context#getSystemService
+ */
+public class ClipboardManager extends android.text.ClipboardManager {
+    private final static Object sStaticLock = new Object();
+    private static IClipboard sService;
+
+    private final Context mContext;
+
+    private final ArrayList<OnPrimaryClipChangedListener> mPrimaryClipChangedListeners
+             = new ArrayList<OnPrimaryClipChangedListener>();
+
+    private final IOnPrimaryClipChangedListener.Stub mPrimaryClipChangedServiceListener
+            = new IOnPrimaryClipChangedListener.Stub() {
+        public void dispatchPrimaryClipChanged() {
+            mHandler.sendEmptyMessage(MSG_REPORT_PRIMARY_CLIP_CHANGED);
+        }
+    };
+
+    static final int MSG_REPORT_PRIMARY_CLIP_CHANGED = 1;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REPORT_PRIMARY_CLIP_CHANGED:
+                    reportPrimaryClipChanged();
+            }
+        }
+    };
+
+    public interface OnPrimaryClipChangedListener {
+        void onPrimaryClipChanged();
+    }
+
+    static private IClipboard getService() {
+        synchronized (sStaticLock) {
+            if (sService != null) {
+                return sService;
+            }
+            IBinder b = ServiceManager.getService("clipboard");
+            sService = IClipboard.Stub.asInterface(b);
+            return sService;
+        }
+    }
+
+    /** {@hide} */
+    public ClipboardManager(Context context, Handler handler) {
+        mContext = context;
+    }
+
+    /**
+     * Sets the current primary clip on the clipboard.  This is the clip that
+     * is involved in normal cut and paste operations.
+     *
+     * @param clip The clipped data item to set.
+     */
+    public void setPrimaryClip(ClippedData clip) {
+        try {
+            getService().setPrimaryClip(clip);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Returns the current primary clip on the clipboard.
+     */
+    public ClippedData getPrimaryClip() {
+        try {
+            return getService().getPrimaryClip();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns true if there is currently a primary clip on the clipboard.
+     */
+    public boolean hasPrimaryClip() {
+        try {
+            return getService().hasPrimaryClip();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public void addPrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
+        synchronized (mPrimaryClipChangedListeners) {
+            if (mPrimaryClipChangedListeners.size() == 0) {
+                try {
+                    getService().addPrimaryClipChangedListener(
+                            mPrimaryClipChangedServiceListener);
+                } catch (RemoteException e) {
+                }
+            }
+            mPrimaryClipChangedListeners.add(what);
+        }
+    }
+
+    public void removePrimaryClipChangedListener(OnPrimaryClipChangedListener what) {
+        synchronized (mPrimaryClipChangedListeners) {
+            mPrimaryClipChangedListeners.remove(what);
+            if (mPrimaryClipChangedListeners.size() == 0) {
+                try {
+                    getService().removePrimaryClipChangedListener(
+                            mPrimaryClipChangedServiceListener);
+                } catch (RemoteException e) {
+                }
+            }
+        }
+    }
+
+    /**
+     * @deprecated Use {@link #getPrimaryClip()} instead.  This retrieves
+     * the primary clip and tries to coerce it to a string.
+     */
+    public CharSequence getText() {
+        ClippedData clip = getPrimaryClip();
+        if (clip != null && clip.getItemCount() > 0) {
+            return clip.getItem(0).getText();
+        }
+        return null;
+    }
+
+    /**
+     * @deprecated Use {@link #setPrimaryClip(ClippedData)} instead.  This
+     * creates a ClippedItem holding the given text and sets it as the
+     * primary clip.  It has no label or icon.
+     */
+    public void setText(CharSequence text) {
+        setPrimaryClip(new ClippedData(null, null, new ClippedData.Item(text)));
+    }
+
+    /**
+     * Returns true if the clipboard has a primary clip containing text; false otherwise.
+     */
+    public boolean hasText() {
+        try {
+            return getService().hasClipboardText();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    void reportPrimaryClipChanged() {
+        Object[] listeners;
+
+        synchronized (mPrimaryClipChangedListeners) {
+            final int N = mPrimaryClipChangedListeners.size();
+            if (N <= 0) {
+                return;
+            }
+            listeners = mPrimaryClipChangedListeners.toArray();
+        }
+
+        for (int i=0; i<listeners.length; i++) {
+            ((OnPrimaryClipChangedListener)listeners[i]).onPrimaryClipChanged();
+        }
+    }
+}
diff --git a/core/java/android/content/ClippedData.aidl b/core/java/android/content/ClippedData.aidl
new file mode 100644
index 0000000..5246526
--- /dev/null
+++ b/core/java/android/content/ClippedData.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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 android.content;
+
+parcelable ClippedData;
diff --git a/core/java/android/content/ClippedData.java b/core/java/android/content/ClippedData.java
new file mode 100644
index 0000000..ebb194f
--- /dev/null
+++ b/core/java/android/content/ClippedData.java
@@ -0,0 +1,189 @@
+/**
+ * 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 android.content;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Representation of a clipped data on the clipboard.
+ *
+ * <p>ClippedData is a complex type containing one or Item instances,
+ * each of which can hold one or more representations of an item of data.
+ * For display to the user, it also has a label and iconic representation.</p>
+ *
+ * <p>The types than an individial item can currently contain are:</p>
+ *
+ * <ul>
+ * <li> Text: a basic string of text.  This is actually a CharSequence,
+ * so it can be formatted text supported by corresponding Android built-in
+ * style spans.  (Custom application spans are not supported and will be
+ * stripped when transporting through the clipboard.)
+ * <li> Intent: an arbitrary Intent object.  A typical use is the shortcut
+ * to create when pasting a clipped item on to the home screen.
+ * <li> Uri: a URI reference.  Currently this should only be a content: URI.
+ * This representation allows an application to share complex or large clips,
+ * by providing a URI to a content provider holding the data.
+ * </ul>
+ */
+public class ClippedData implements Parcelable {
+    CharSequence mLabel;
+    Bitmap mIcon;
+
+    final ArrayList<Item> mItems = new ArrayList<Item>();
+
+    public static class Item {
+        CharSequence mText;
+        Intent mIntent;
+        Uri mUri;
+
+        public Item(CharSequence text) {
+            mText = text;
+        }
+
+        public Item(Intent intent) {
+            mIntent = intent;
+        }
+
+        public Item(Uri uri) {
+            mUri = uri;
+        }
+
+        public Item(CharSequence text, Intent intent, Uri uri) {
+            mText = text;
+            mIntent = intent;
+            mUri = uri;
+        }
+
+        public CharSequence getText() {
+            return mText;
+        }
+
+        public Intent getIntent() {
+            return mIntent;
+        }
+
+        public Uri getUri() {
+            return mUri;
+        }
+    }
+
+    /**
+     * Create a new clip.
+     *
+     * @param label Label to show to the user describing this clip.
+     * @param icon Bitmap providing the user with an iconing representation of
+     * the clip.
+     * @param item The contents of the first item in the clip.
+     */
+    public ClippedData(CharSequence label, Bitmap icon, Item item) {
+        if (item == null) {
+            throw new NullPointerException("item is null");
+        }
+        mLabel = label;
+        mIcon = icon;
+        mItems.add(item);
+    }
+
+    public void addItem(Item item) {
+        if (item == null) {
+            throw new NullPointerException("item is null");
+        }
+        mItems.add(item);
+    }
+
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    public Bitmap getIcon() {
+        return mIcon;
+    }
+
+    public int getItemCount() {
+        return mItems.size();
+    }
+
+    public Item getItem(int index) {
+        return mItems.get(index);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        TextUtils.writeToParcel(mLabel, dest, flags);
+        if (mIcon != null) {
+            dest.writeInt(1);
+            mIcon.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
+        final int N = mItems.size();
+        dest.writeInt(N);
+        for (int i=0; i<N; i++) {
+            Item item = mItems.get(i);
+            TextUtils.writeToParcel(item.mText, dest, flags);
+            if (item.mIntent != null) {
+                dest.writeInt(1);
+                item.mIntent.writeToParcel(dest, flags);
+            } else {
+                dest.writeInt(0);
+            }
+            if (item.mUri != null) {
+                dest.writeInt(1);
+                item.mUri.writeToParcel(dest, flags);
+            } else {
+                dest.writeInt(0);
+            }
+        }
+    }
+
+    ClippedData(Parcel in) {
+        mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        if (in.readInt() != 0) {
+            mIcon = Bitmap.CREATOR.createFromParcel(in);
+        }
+        final int N = in.readInt();
+        for (int i=0; i<N; i++) {
+            CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+            Intent intent = in.readInt() != 0 ? Intent.CREATOR.createFromParcel(in) : null;
+            Uri uri = in.readInt() != 0 ? Uri.CREATOR.createFromParcel(in) : null;
+            mItems.add(new Item(text, intent, uri));
+        }
+    }
+
+    public static final Parcelable.Creator<ClippedData> CREATOR =
+        new Parcelable.Creator<ClippedData>() {
+
+            public ClippedData createFromParcel(Parcel source) {
+                return new ClippedData(source);
+            }
+
+            public ClippedData[] newArray(int size) {
+                return new ClippedData[size];
+            }
+        };
+}
diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/content/IClipboard.aidl
similarity index 68%
rename from core/java/android/text/IClipboard.aidl
rename to core/java/android/content/IClipboard.aidl
index 4deb5c8..b4534a9 100644
--- a/core/java/android/text/IClipboard.aidl
+++ b/core/java/android/content/IClipboard.aidl
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package android.text;
+package android.content;
+
+import android.content.ClippedData;
+import android.content.IOnPrimaryClipChangedListener;
 
 /**
  * Programming interface to the clipboard, which allows copying and pasting
@@ -22,21 +25,14 @@
  * {@hide}
  */
 interface IClipboard {
-    /**
-     * Returns the text on the clipboard.  It will eventually be possible
-     * to store types other than text too, in which case this will return
-     * null if the type cannot be coerced to text.
-     */
-    CharSequence getClipboardText();
-
-    /**
-     * Sets the contents of the clipboard to the specified text.
-     */
-    void setClipboardText(CharSequence text);
+    void setPrimaryClip(in ClippedData clip);
+    ClippedData getPrimaryClip();
+    boolean hasPrimaryClip();
+    void addPrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener);
+    void removePrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener);
 
     /**
      * Returns true if the clipboard contains text; false otherwise.
      */
     boolean hasClipboardText();
 }
-
diff --git a/core/java/android/content/IOnPrimaryClipChangedListener.aidl b/core/java/android/content/IOnPrimaryClipChangedListener.aidl
new file mode 100644
index 0000000..fb42a45
--- /dev/null
+++ b/core/java/android/content/IOnPrimaryClipChangedListener.aidl
@@ -0,0 +1,21 @@
+/**
+ * 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 android.content;
+
+oneway interface IOnPrimaryClipChangedListener {
+    void dispatchPrimaryClipChanged();
+}
diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java
index 70baaef..bd23db4 100644
--- a/core/java/android/content/res/ColorStateList.java
+++ b/core/java/android/content/res/ColorStateList.java
@@ -45,7 +45,6 @@
  *   &lt;item android:state_focused="true" android:color="@color/testcolor1"/&gt;
  *   &lt;item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /&gt;
  *   &lt;item android:state_enabled="false" android:color="@color/testcolor3" /&gt;
- *   &lt;item android:state_active="true" android:color="@color/testcolor4" /&gt;
  *   &lt;item android:color="@color/testcolor5"/&gt;
  * &lt;/selector&gt;
  * </pre>
@@ -56,6 +55,9 @@
  * An item with no state spec is considered to match any set of states and is generally useful as
  * a final item to be used as a default.  Note that if you have such an item before any other items
  * in the list then any subsequent items will end up being ignored.
+ * <p>For more information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State
+ * List Resource</a>.</p>
  */
 public class ColorStateList implements Parcelable {
 
diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java
index 02ec137..e2e4d2c 100644
--- a/core/java/android/net/Proxy.java
+++ b/core/java/android/net/Proxy.java
@@ -150,30 +150,14 @@
                     // Return no proxy
                     retval = java.net.Proxy.NO_PROXY;
                 } else {
-                    java.net.Proxy retProxy =
+                    retval =
                         new java.net.Proxy(java.net.Proxy.Type.HTTP, sGlobalProxySpec.proxyAddress);
-                    sProxyInfoLock.readLock().unlock();
-                    if (isLocalHost(url)) {
-                        sProxyInfoLock.readLock().lock();
-                        retval = java.net.Proxy.NO_PROXY;
-                    } else {
-                        sProxyInfoLock.readLock().lock();
-                        retval = retProxy;
-                    }
                 }
             } else {
                 // If network is WiFi, return no proxy.
                 // Otherwise, return the Mobile Operator proxy.
                 if (!isNetworkWifi(ctx)) {
-                    java.net.Proxy retProxy = getDefaultProxy(url);
-                    sProxyInfoLock.readLock().unlock();
-                    if (isLocalHost(url)) {
-                        sProxyInfoLock.readLock().lock();
-                        retval = java.net.Proxy.NO_PROXY;
-                    } else {
-                        sProxyInfoLock.readLock().lock();
-                        retval = retProxy;
-                    }
+                    retval = getDefaultProxy(url);
                 } else {
                     retval = java.net.Proxy.NO_PROXY;
                 }
@@ -181,6 +165,9 @@
         } finally {
             sProxyInfoLock.readLock().unlock();
         }
+        if ((retval != java.net.Proxy.NO_PROXY) && (isLocalHost(url))) {
+            retval = java.net.Proxy.NO_PROXY;
+        }
         return retval;
     }
 
@@ -348,7 +335,7 @@
         private Context mContext;
 
         SettingsObserver(Context ctx) {
-            super(new Handler());
+            super(new Handler(ctx.getMainLooper()));
             mContext = ctx;
         }
 
@@ -368,10 +355,13 @@
         // No lock upgrading (from read to write) allowed
         sProxyInfoLock.readLock().unlock();
         sProxyInfoLock.writeLock().lock();
-        sGlobalProxyChangedObserver = new SettingsObserver(ctx);
-        // Downgrading locks (from write to read) is allowed
-        sProxyInfoLock.readLock().lock();
-        sProxyInfoLock.writeLock().unlock();
+        try {
+            sGlobalProxyChangedObserver = new SettingsObserver(ctx);
+        } finally {
+            // Downgrading locks (from write to read) is allowed
+            sProxyInfoLock.readLock().lock();
+            sProxyInfoLock.writeLock().unlock();
+        }
         ctx.getContentResolver().registerContentObserver(uriGlobalProxy, false,
                 sGlobalProxyChangedObserver);
         ctx.getContentResolver().registerContentObserver(uriGlobalExclList, false,
diff --git a/core/java/android/text/ClipboardManager.java b/core/java/android/text/ClipboardManager.java
index 52039af..0b239cf 100644
--- a/core/java/android/text/ClipboardManager.java
+++ b/core/java/android/text/ClipboardManager.java
@@ -16,73 +16,26 @@
 
 package android.text;
 
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.ServiceManager;
-import android.util.Log;
-
 /**
- * Interface to the clipboard service, for placing and retrieving text in
- * the global clipboard.
- *
- * <p>
- * You do not instantiate this class directly; instead, retrieve it through
- * {@link android.content.Context#getSystemService}.
- *
- * @see android.content.Context#getSystemService
+ * @deprecated Old text-only interace to the clipboard.  See
+ * {@link android.content.ClipboardManager} for the modern API.
  */
-public class ClipboardManager {
-    private static IClipboard sService;
-
-    private Context mContext;
-
-    static private IClipboard getService() {
-        if (sService != null) {
-            return sService;
-        }
-        IBinder b = ServiceManager.getService("clipboard");
-        sService = IClipboard.Stub.asInterface(b);
-        return sService;
-    }
-
-    /** {@hide} */
-    public ClipboardManager(Context context, Handler handler) {
-        mContext = context;
-    }
-
+@Deprecated
+public abstract class ClipboardManager {
     /**
      * Returns the text on the clipboard.  It will eventually be possible
      * to store types other than text too, in which case this will return
      * null if the type cannot be coerced to text.
      */
-    public CharSequence getText() {
-        try {
-            return getService().getClipboardText();
-        } catch (RemoteException e) {
-            return null;
-        }
-    }
+    public abstract CharSequence getText();
 
     /**
      * Sets the contents of the clipboard to the specified text.
      */
-    public void setText(CharSequence text) {
-        try {
-            getService().setClipboardText(text);
-        } catch (RemoteException e) {
-        }
-    }
+    public abstract void setText(CharSequence text);
 
     /**
      * Returns true if the clipboard contains text; false otherwise.
      */
-    public boolean hasText() {
-        try {
-            return getService().hasClipboardText();
-        } catch (RemoteException e) {
-            return false;
-        }
-    }
+    public abstract boolean hasText();
 }
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index f533944..0466c69 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -234,7 +234,7 @@
                             LineBackgroundSpan.class);
                     // All LineBackgroundSpans on a line contribute to its
                     // background.
-                   spans = sp.getSpans(start, end, LineBackgroundSpan.class);
+                   spans = getParagraphSpans(sp, start, end, LineBackgroundSpan.class);
                 }
 
                 for (int n = 0; n < spans.length; n++) {
@@ -309,7 +309,7 @@
                 if (start >= spanEnd && (i == first || isFirstParaLine)) {
                     spanEnd = sp.nextSpanTransition(start, textLength,
                                                     ParagraphStyle.class);
-                    spans = sp.getSpans(start, spanEnd, ParagraphStyle.class);
+                    spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
 
                     align = mAlignment;
                     for (int n = spans.length-1; n >= 0; n--) {
@@ -425,7 +425,7 @@
                 int start = getLineStart(line);
                 int spanEnd = spanned.nextSpanTransition(start, spanned.length(),
                         TabStopSpan.class);
-                TabStopSpan[] tabSpans = spanned.getSpans(start, spanEnd, TabStopSpan.class);
+                TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, TabStopSpan.class);
                 if (tabSpans.length > 0) {
                     tabStops = new TabStops(TAB_INCREMENT, tabSpans);
                 }
@@ -713,7 +713,7 @@
         if (hasTabOrEmoji && mText instanceof Spanned) {
             // Just checking this line should be good enough, tabs should be
             // consistent across all lines in a paragraph.
-            TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+            TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
             if (tabs.length > 0) {
                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
             }
@@ -820,7 +820,7 @@
         if (hasTabsOrEmoji && mText instanceof Spanned) {
             // Just checking this line should be good enough, tabs should be
             // consistent across all lines in a paragraph.
-            TabStopSpan[] tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+            TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class);
             if (tabs.length > 0) {
                 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
             }
@@ -1308,7 +1308,7 @@
 
         if (mSpannedText) {
             Spanned sp = (Spanned) mText;
-            AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
+            AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line),
                                                 getLineEnd(line),
                                                 AlignmentSpan.class);
 
@@ -1361,7 +1361,7 @@
         int lineEnd = getLineEnd(line);
         int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd,
                 LeadingMarginSpan.class);
-        LeadingMarginSpan[] spans = spanned.getSpans(lineStart, spanEnd,
+        LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd,
                                                 LeadingMarginSpan.class);
         if (spans.length == 0) {
             return 0; // no leading margin span;
@@ -1416,7 +1416,7 @@
                         Spanned spanned = (Spanned) text;
                         int spanEnd = spanned.nextSpanTransition(start, end,
                                 TabStopSpan.class);
-                        TabStopSpan[] spans = spanned.getSpans(start, spanEnd,
+                        TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd,
                                 TabStopSpan.class);
                         if (spans.length > 0) {
                             tabStops = new TabStops(TAB_INCREMENT, spans);
@@ -1513,7 +1513,7 @@
 
         if (text instanceof Spanned) {
             if (tabs == null) {
-                tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
+                tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class);
                 alltabs = true;
             }
 
@@ -1540,6 +1540,38 @@
         return mSpannedText;
     }
 
+    /**
+     * Returns the same as <code>text.getSpans()</code>, except where
+     * <code>start</code> and <code>end</code> are the same and are not
+     * at the very beginning of the text, in which case an empty array
+     * is returned instead.
+     * <p>
+     * This is needed because of the special case that <code>getSpans()</code>
+     * on an empty range returns the spans adjacent to that range, which is
+     * primarily for the sake of <code>TextWatchers</code> so they will get
+     * notifications when text goes from empty to non-empty.  But it also
+     * has the unfortunate side effect that if the text ends with an empty
+     * paragraph, that paragraph accidentally picks up the styles of the
+     * preceding paragraph (even though those styles will not be picked up
+     * by new text that is inserted into the empty paragraph).
+     * <p>
+     * The reason it just checks whether <code>start</code> and <code>end</code>
+     * is the same is that the only time a line can contain 0 characters
+     * is if it is the final paragraph of the Layout; otherwise any line will
+     * contain at least one printing or newline character.  The reason for the
+     * additional check if <code>start</code> is greater than 0 is that
+     * if the empty paragraph is the entire content of the buffer, paragraph
+     * styles that are already applied to the buffer will apply to text that
+     * is inserted into it.
+     */
+    /* package */ static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) {
+        if (start == end && start > 0) {
+            return (T[]) ArrayUtils.emptyArray(type);
+        }
+
+        return text.getSpans(start, end, type);
+    }
+
     private void ellipsize(int start, int end, int line,
                            char[] dest, int destoff) {
         int ellipsisCount = getEllipsisCount(line);
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 44157de..cc969cb 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -155,7 +155,7 @@
             LineHeightSpan[] chooseht = null;
 
             if (spanned != null) {
-                LeadingMarginSpan[] sp = spanned.getSpans(paraStart, paraEnd,
+                LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
                         LeadingMarginSpan.class);
                 for (int i = 0; i < sp.length; i++) {
                     LeadingMarginSpan lms = sp[i];
@@ -174,7 +174,7 @@
                     }
                 }
 
-                chooseht = spanned.getSpans(paraStart, paraEnd, LineHeightSpan.class);
+                chooseht = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
 
                 if (chooseht.length != 0) {
                     if (choosehtv == null ||
@@ -267,7 +267,7 @@
                             hasTabOrEmoji = true;
                             if (spanned != null) {
                                 // First tab this para, check for tabstops
-                                TabStopSpan[] spans = spanned.getSpans(paraStart,
+                                TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
                                         paraEnd, TabStopSpan.class);
                                 if (spans.length > 0) {
                                     tabStops = new TabStops(TAB_INCREMENT, spans);
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index 681ef70..6e89a8b 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -179,6 +179,7 @@
     private boolean         mSupportMultipleWindows = false;
     private boolean         mShrinksStandaloneImagesToFit = false;
     private long            mMaximumDecodedImageSize = 0; // 0 means default
+    private boolean         mPrivateBrowsingEnabled = false;
     // HTML5 API flags
     private boolean         mAppCacheEnabled = false;
     private boolean         mDatabaseEnabled = false;
@@ -1468,6 +1469,24 @@
         }
     }
 
+    /**
+     * Returns whether private browsing is enabled.
+     */
+    /* package */ boolean isPrivateBrowsingEnabled() {
+        return mPrivateBrowsingEnabled;
+    }
+
+    /**
+     * Sets whether private browsing is enabled.
+     * @param flag Whether private browsing should be enabled.
+     */
+    /* package */ synchronized void setPrivateBrowsingEnabled(boolean flag) {
+        if (mPrivateBrowsingEnabled != flag) {
+            mPrivateBrowsingEnabled = flag;
+            postSync();
+        }
+    }
+
     int getDoubleTapToastCount() {
         return mDoubleTapToastCount;
     }
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 03382c3b..43a3e04 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Widget;
 import android.app.AlertDialog;
+import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -45,7 +46,6 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.speech.tts.TextToSpeech;
-import android.text.IClipboard;
 import android.text.Selection;
 import android.text.Spannable;
 import android.util.AttributeSet;
@@ -86,6 +86,8 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -825,7 +827,18 @@
      * @param defStyle The default style resource ID.
      */
     public WebView(Context context, AttributeSet attrs, int defStyle) {
-        this(context, attrs, defStyle, null);
+        this(context, attrs, defStyle, false);
+    }
+
+    /**
+     * Construct a new WebView with layout parameters and a default style.
+     * @param context A Context object used to access application assets.
+     * @param attrs An AttributeSet passed to our parent.
+     * @param defStyle The default style resource ID.
+     */
+    public WebView(Context context, AttributeSet attrs, int defStyle,
+            boolean privateBrowsing) {
+        this(context, attrs, defStyle, null, privateBrowsing);
     }
 
     /**
@@ -841,7 +854,7 @@
      * @hide pending API council approval.
      */
     protected WebView(Context context, AttributeSet attrs, int defStyle,
-            Map<String, Object> javascriptInterfaces) {
+            Map<String, Object> javascriptInterfaces, boolean privateBrowsing) {
         super(context, attrs, defStyle);
 
         if (AccessibilityManager.getInstance(context).isEnabled()) {
@@ -863,6 +876,10 @@
          */
         init();
         updateMultiTouchSupport(context);
+
+        if (privateBrowsing) {
+            startPrivateBrowsing();
+        }
     }
 
     void updateMultiTouchSupport(Context context) {
@@ -1668,6 +1685,34 @@
         }
     }
 
+    /**
+     * Returns true if private browsing is enabled in this WebView.
+     */
+    public boolean isPrivateBrowsingEnabled () {
+        return getSettings().isPrivateBrowsingEnabled();
+    }
+
+    private void startPrivateBrowsing () {
+        boolean wasPrivateBrowsingEnabled = isPrivateBrowsingEnabled();
+
+        getSettings().setPrivateBrowsingEnabled(true);
+
+        if (!wasPrivateBrowsingEnabled) {
+            StringBuilder data = new StringBuilder(1024);
+            try {
+                InputStreamReader file = new InputStreamReader(mContext.getResources().openRawResource(com.android.internal.R.raw.incognito_mode_start_page));
+                int size;
+                char[] buffer = new char[1024];
+                while ((size = file.read(buffer)) != -1) {
+                    data.append(buffer, 0, size);
+                }
+            } catch (IOException e) {
+                // This should never happen since this is a static resource.
+            }
+            loadDataWithBaseURL(null, data.toString(), "text/html", "utf-8", null);
+        }
+    }
+
     private boolean extendScroll(int y) {
         int finalY = mScroller.getFinalY();
         int newY = pinLocY(finalY + y);
@@ -4053,13 +4098,9 @@
                     , com.android.internal.R.string.text_copied
                     , Toast.LENGTH_SHORT).show();
             copiedSomething = true;
-            try {
-                IClipboard clip = IClipboard.Stub.asInterface(
-                        ServiceManager.getService("clipboard"));
-                        clip.setClipboardText(selection);
-            } catch (android.os.RemoteException e) {
-                Log.e(LOGTAG, "Clipboard failed", e);
-            }
+            ClipboardManager cm = (ClipboardManager)getContext()
+                    .getSystemService(Context.CLIPBOARD_SERVICE);
+            cm.setText(selection);
         }
         invalidate(); // remove selection region and pointer
         return copiedSomething;
diff --git a/core/jni/android_database_SQLiteDatabase.cpp b/core/jni/android_database_SQLiteDatabase.cpp
index 45d2e1e..4b3f1c0 100644
--- a/core/jni/android_database_SQLiteDatabase.cpp
+++ b/core/jni/android_database_SQLiteDatabase.cpp
@@ -63,6 +63,7 @@
 
 static jfieldID offset_db_handle;
 static jmethodID method_custom_function_callback;
+static jclass string_class = NULL;
 
 static char *createStr(const char *path, short extra) {
     int len = strlen(path) + extra;
@@ -394,27 +395,36 @@
         LOGE("custom_function_callback cannot call into Java on this thread");
         return;
     }
+    // get global ref to CustomFunction object from our user data
+    jobject function = (jobject)sqlite3_user_data(context);
 
     // pack up the arguments into a string array
-    jobjectArray strArray = env->NewObjectArray(argc, env->FindClass("java/lang/String"), NULL);
-    if (!strArray) {
-        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
-        return;
-    }
+    if (!string_class)
+        string_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/String"));
+    jobjectArray strArray = env->NewObjectArray(argc, string_class, NULL);
+    if (!strArray)
+        goto done;
     for (int i = 0; i < argc; i++) {
         char* arg = (char *)sqlite3_value_text(argv[i]);
-        jobject obj = env->NewStringUTF(arg);
-        if (!obj) {
-            jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
+        if (!arg) {
+            LOGE("NULL argument in custom_function_callback.  This should not happen.");
             return;
         }
+        jobject obj = env->NewStringUTF(arg);
+        if (!obj)
+            goto done;
         env->SetObjectArrayElement(strArray, i, obj);
         env->DeleteLocalRef(obj);
     }
 
-    // get global ref to CustomFunction object from our user data
-    jobject function = (jobject)sqlite3_user_data(context);
     env->CallVoidMethod(function, method_custom_function_callback, strArray);
+
+done:
+    if (env->ExceptionCheck()) {
+        LOGE("An exception was thrown by custom sqlite3 function.");
+        LOGE_EX(env);
+        env->ExceptionClear();
+    }
 }
 
 static jint native_addCustomFunction(JNIEnv* env, jobject object,
@@ -423,7 +433,7 @@
     sqlite3 * handle = (sqlite3 *)env->GetIntField(object, offset_db_handle);
     char const *nameStr = env->GetStringUTFChars(name, NULL);
     jobject ref = env->NewGlobalRef(function);
-    LOGD("native_addCustomFunction %s ref: %d", nameStr, ref);
+    LOGD("native_addCustomFunction %s ref: %p", nameStr, ref);
     int err = sqlite3_create_function(handle, nameStr, numArgs, SQLITE_UTF8,
             (void *)ref, custom_function_callback, NULL, NULL);
     env->ReleaseStringUTFChars(name, nameStr);
diff --git a/core/res/res/raw/incognito_mode_start_page.html b/core/res/res/raw/incognito_mode_start_page.html
new file mode 100644
index 0000000..b070c6d
--- /dev/null
+++ b/core/res/res/raw/incognito_mode_start_page.html
@@ -0,0 +1,24 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
+    <title>New incognito window</title>
+  </head>
+  <body>
+    <p><strong>You've gone incognito</strong>. Pages you view in this window
+      won't appear in your browser history or search history, and they won't
+      leave other traces, like cookies, on your computer after you close the
+      incognito window. Any files you download or bookmarks you create will be
+      preserved, however.</p>
+
+    <p><strong>Going incognito doesn't affect the behavior of other people,
+	servers, or software. Be wary of:</strong></p>
+
+    <ul>
+      <li>Websites that collect or share information about you</li>
+      <li>Internet service providers or employers that track the pages you visit</li>
+      <li>Malicious software that tracks your keystrokes in exchange for free smileys</li>
+      <li>Surveillance by secret agents</li>
+      <li>People standing behind you</li>
+    </ul>
+  </body>
+</html>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index a77717f..036da95 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -36,6 +36,8 @@
             android:description="@string/permdesc_testDenied" />
 
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
     <uses-permission android:name="android.permission.CLEAR_APP_CACHE" />
     <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" />
diff --git a/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java b/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java
new file mode 100644
index 0000000..0fe83e1
--- /dev/null
+++ b/core/tests/coretests/src/android/bluetooth/BluetoothStressTest.java
@@ -0,0 +1,440 @@
+/*
+ * 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 android.bluetooth;
+
+import android.app.Instrumentation;
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+public class BluetoothStressTest extends InstrumentationTestCase {
+    private static final String TAG = "BluetoothEnablerStressTest";
+
+    /**
+     * Timeout for {@link BluetoothAdapter#disable()} in ms.
+     */
+    private static final int DISABLE_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#enable()} in ms.
+     */
+    private static final int ENABLE_TIMEOUT = 20000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#setScanMode(int)} in ms.
+     */
+    private static final int SET_SCAN_MODE_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#startDiscovery()} in ms.
+     */
+    private static final int START_DISCOVERY_TIMEOUT = 5000;
+
+    /**
+     * Timeout for {@link BluetoothAdapter#cancelDiscovery()} in ms.
+     */
+    private static final int CANCEL_DISCOVERY_TIMEOUT = 5000;
+
+    private static final int DISCOVERY_STARTED_FLAG = 1;
+    private static final int DISCOVERY_FINISHED_FLAG = 1 << 1;
+    private static final int SCAN_MODE_NONE_FLAG = 1 << 2;
+    private static final int SCAN_MODE_CONNECTABLE_FLAG = 1 << 3;
+    private static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG = 1 << 4;
+    private static final int STATE_OFF_FLAG = 1 << 5;
+    private static final int STATE_TURNING_ON_FLAG = 1 << 6;
+    private static final int STATE_ON_FLAG = 1 << 7;
+    private static final int STATE_TURNING_OFF_FLAG = 1 << 8;
+
+    /**
+     * Time between polls in ms.
+     */
+    private static final int POLL_TIME = 100;
+
+    private static final int ENABLE_ITERATIONS = 100;
+    private static final int DISCOVERABLE_ITERATIONS = 1000;
+    private static final int SCAN_ITERATIONS = 1000;
+
+    private Context mContext;
+
+    private Instrumentation mInstrumentation;
+
+    private class BluetoothReceiver extends BroadcastReceiver {
+        private int mFiredFlags = 0;
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (this) {
+                if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(intent.getAction())) {
+                    mFiredFlags |= DISCOVERY_STARTED_FLAG;
+                } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
+                    mFiredFlags |= DISCOVERY_FINISHED_FLAG;
+                } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
+                    int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+                            BluetoothAdapter.ERROR);
+                    assertNotSame(mode, BluetoothAdapter.ERROR);
+                    switch (mode) {
+                        case BluetoothAdapter.SCAN_MODE_NONE:
+                            mFiredFlags |= SCAN_MODE_NONE_FLAG;
+                            break;
+                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+                            mFiredFlags |= SCAN_MODE_CONNECTABLE_FLAG;
+                            break;
+                        case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+                            mFiredFlags |= SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
+                            break;
+                    }
+                } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+                    int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+                            BluetoothAdapter.ERROR);
+                    assertNotSame(state, BluetoothAdapter.ERROR);
+                    switch (state) {
+                        case BluetoothAdapter.STATE_OFF:
+                            mFiredFlags |= STATE_OFF_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_TURNING_ON:
+                            mFiredFlags |= STATE_TURNING_ON_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_ON:
+                            mFiredFlags |= STATE_ON_FLAG;
+                            break;
+                        case BluetoothAdapter.STATE_TURNING_OFF:
+                            mFiredFlags |= STATE_TURNING_OFF_FLAG;
+                            break;
+                    }
+                }
+            }
+        }
+
+        public int getFiredFlags() {
+            synchronized (this) {
+                return mFiredFlags;
+            }
+        }
+
+        public void resetFiredFlags() {
+            synchronized (this) {
+                mFiredFlags = 0;
+            }
+        }
+    }
+
+    private BluetoothReceiver mReceiver = new BluetoothReceiver();
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mInstrumentation = getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
+        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
+        filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        mContext.registerReceiver(mReceiver, filter);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        mContext.unregisterReceiver(mReceiver);
+    }
+
+    @LargeTest
+    public void testEnableDisable() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        for (int i = 0; i < ENABLE_ITERATIONS; i++) {
+            Log.i(TAG, "Enable iteration " + (i + 1) + " of " + ENABLE_ITERATIONS);
+            enable(adapter);
+            disable(adapter);
+        }
+    }
+
+    @LargeTest
+    public void testDiscoverable() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        enable(adapter);
+
+        for (int i = 0; i < DISCOVERABLE_ITERATIONS; i++) {
+            Log.i(TAG, "Discoverable iteration " + (i + 1) + " of " + DISCOVERABLE_ITERATIONS);
+            discoverable(adapter);
+            undiscoverable(adapter);
+        }
+
+        disable(adapter);
+    }
+
+    @LargeTest
+    public void testScan() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        enable(adapter);
+
+        for (int i = 0; i < SCAN_ITERATIONS; i++) {
+            Log.i(TAG, "Scan iteration " + (i + 1) + " of " + SCAN_ITERATIONS);
+            startScan(adapter);
+            stopScan(adapter);
+        }
+
+        disable(adapter);
+    }
+
+    private void disable(BluetoothAdapter adapter) {
+        int mask = STATE_TURNING_OFF_FLAG | STATE_OFF_FLAG | SCAN_MODE_NONE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        int state = adapter.getState();
+        switch (state) {
+            case BluetoothAdapter.STATE_OFF:
+                assertFalse(adapter.isEnabled());
+                return;
+            case BluetoothAdapter.STATE_ON:
+                assertTrue(adapter.isEnabled());
+                assertTrue(adapter.disable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_ON:
+                assertFalse(adapter.isEnabled());
+                assertTrue(adapter.disable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_OFF:
+                assertFalse(adapter.isEnabled());
+                mask = 0; // Don't check for received intents since we might have missed them.
+                break;
+            default:
+                fail("disable() invalid state: " + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < DISABLE_TIMEOUT) {
+            state = adapter.getState();
+            if (state == BluetoothAdapter.STATE_OFF) {
+                assertFalse(adapter.isEnabled());
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    return;
+                }
+            } else {
+                assertFalse(adapter.isEnabled());
+                assertEquals(BluetoothAdapter.STATE_TURNING_OFF, state);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("disable() timeout: " +
+                "state=" + state + " (expected " + BluetoothAdapter.STATE_OFF + ") " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void enable(BluetoothAdapter adapter) {
+        int mask = STATE_TURNING_ON_FLAG | STATE_ON_FLAG | SCAN_MODE_CONNECTABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        int state = adapter.getState();
+        switch (state) {
+            case BluetoothAdapter.STATE_ON:
+                assertTrue(adapter.isEnabled());
+                return;
+            case BluetoothAdapter.STATE_OFF:
+            case BluetoothAdapter.STATE_TURNING_OFF:
+                assertFalse(adapter.isEnabled());
+                assertTrue(adapter.enable());
+                break;
+            case BluetoothAdapter.STATE_TURNING_ON:
+                assertFalse(adapter.isEnabled());
+                mask = 0; // Don't check for received intents since we might have missed them.
+                break;
+            default:
+                fail("enable() invalid state: state=" + state);
+        }
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < ENABLE_TIMEOUT) {
+            state = adapter.getState();
+            if (state == BluetoothAdapter.STATE_ON) {
+                assertTrue(adapter.isEnabled());
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    return;
+                }
+            } else {
+                assertFalse(adapter.isEnabled());
+                assertEquals(BluetoothAdapter.STATE_TURNING_ON, state);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("enable() timeout: " +
+                "state=" + state + " (expected " + BluetoothAdapter.STATE_OFF + ") " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void discoverable(BluetoothAdapter adapter) {
+        int mask = SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("discoverable() bluetooth not enabled");
+        }
+
+        int scanMode = adapter.getScanMode();
+        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+            return;
+        }
+
+        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
+            scanMode = adapter.getScanMode();
+            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    return;
+                }
+            } else {
+                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("discoverable() timeout: " +
+                "scanMode=" + scanMode + " (expected " +
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE + ") " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void undiscoverable(BluetoothAdapter adapter) {
+        int mask = SCAN_MODE_CONNECTABLE_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("undiscoverable(): bluetooth not enabled");
+        }
+
+        int scanMode = adapter.getScanMode();
+        if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+            return;
+        }
+
+        assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+        assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < SET_SCAN_MODE_TIMEOUT) {
+            scanMode = adapter.getScanMode();
+            if (scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+                if ((mReceiver.getFiredFlags() & mask) == mask) {
+                    mReceiver.resetFiredFlags();
+                    return;
+                }
+            } else {
+                assertEquals(scanMode, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("undiscoverable() timeout: " +
+                "scanMode=" + scanMode + " (expected " +
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE + ") " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void startScan(BluetoothAdapter adapter) {
+        int mask = DISCOVERY_STARTED_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("startScan(): bluetooth not enabled");
+        }
+
+        if (adapter.isDiscovering()) {
+            return;
+        }
+
+        assertTrue(adapter.startDiscovery());
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < START_DISCOVERY_TIMEOUT) {
+            if (adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
+                mReceiver.resetFiredFlags();
+                return;
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("startScan() timeout: " +
+                "isDiscovering=" + adapter.isDiscovering() + " " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void stopScan(BluetoothAdapter adapter) {
+        int mask = DISCOVERY_FINISHED_FLAG;
+        mReceiver.resetFiredFlags();
+
+        if (!adapter.isEnabled()) {
+            fail("stopScan(): bluetooth not enabled");
+        }
+
+        if (!adapter.isDiscovering()) {
+            return;
+        }
+
+        // TODO: put assertTrue() around cancelDiscovery() once it starts
+        // returning true.
+        adapter.cancelDiscovery();
+
+        long s = System.currentTimeMillis();
+        while (System.currentTimeMillis() - s < CANCEL_DISCOVERY_TIMEOUT) {
+            if (!adapter.isDiscovering() && ((mReceiver.getFiredFlags() & mask) == mask)) {
+                mReceiver.resetFiredFlags();
+                return;
+            }
+            sleep(POLL_TIME);
+        }
+
+        int firedFlags = mReceiver.getFiredFlags();
+        mReceiver.resetFiredFlags();
+        fail("stopScan() timeout: " +
+                "isDiscovering=" + adapter.isDiscovering() + " " +
+                "flags=" + firedFlags + " (expected " + mask + ")");
+    }
+
+    private void sleep(long time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
index 7c28516..cbf8c87 100755
--- a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
+++ b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
@@ -69,7 +69,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mOrigState = getMediaState();
+        mOrigState = checkMediaState(Environment.MEDIA_MOUNTED);
         if (!mountMedia()) {
             Log.i(TAG, "sdcard not mounted? Some of these tests might fail");
         }
@@ -78,12 +78,12 @@
     @Override
     protected void tearDown() throws Exception {
         // Restore media state.
-        boolean newState = getMediaState();
+        boolean newState = checkMediaState(Environment.MEDIA_MOUNTED);
         if (newState != mOrigState) {
             if (mOrigState) {
-                getMs().mountVolume(Environment.getExternalStorageDirectory().getPath());
+                mountMedia();
             } else {
-                getMs().unmountVolume(Environment.getExternalStorageDirectory().getPath(), true);
+                unmountMedia();
             }
         }
         super.tearDown();
@@ -576,6 +576,7 @@
 
     @LargeTest
     public void testInstallSdcard() {
+        mountMedia();
         sampleInstallFromRawResource(PackageManager.INSTALL_EXTERNAL, true);
     }
 
@@ -913,41 +914,62 @@
         return null;
     }
 
-    boolean getMediaState() {
+    boolean checkMediaState(String desired) {
         try {
-        String mPath = Environment.getExternalStorageDirectory().getPath();
-        String state = getMs().getVolumeState(mPath);
-        return Environment.MEDIA_MOUNTED.equals(state);
+            String mPath = Environment.getExternalStorageDirectory().getPath();
+            String actual = getMs().getVolumeState(mPath);
+            if (desired.equals(actual)) {
+                return true;
+            } else {
+                return false;
+            }
         } catch (RemoteException e) {
+            Log.e(TAG, "Exception while checking media state", e);
             return false;
         }
     }
 
     boolean mountMedia() {
-        if (getMediaState()) {
+        if (checkMediaState(Environment.MEDIA_MOUNTED)) {
             return true;
         }
+
+        final String path = Environment.getExternalStorageDirectory().toString();
+        StorageListener observer = new StorageListener(Environment.MEDIA_MOUNTED);
+        StorageManager sm = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
+        sm.registerListener(observer);
         try {
-        String mPath = Environment.getExternalStorageDirectory().toString();
-        int ret = getMs().mountVolume(mPath);
-        return ret == StorageResultCode.OperationSucceeded;
-        } catch (RemoteException e) {
+            // Wait on observer
+            synchronized (observer) {
+                int ret = getMs().mountVolume(path);
+                if (ret != StorageResultCode.OperationSucceeded) {
+                    throw new Exception("Could not mount the media");
+                }
+                long waitTime = 0;
+                while ((!observer.isDone()) && (waitTime < MAX_WAIT_TIME)) {
+                    observer.wait(WAIT_TIME_INCR);
+                    waitTime += WAIT_TIME_INCR;
+                }
+                if (!observer.isDone()) {
+                    throw new Exception("Timed out waiting for unmount media notification");
+                }
+                return true;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Exception : " + e);
             return false;
+        } finally {
+            sm.unregisterListener(observer);
         }
     }
 
     private boolean unmountMedia() {
-        String path = Environment.getExternalStorageDirectory().getPath();
-        try {
-            String state = getMs().getVolumeState(path);
-            if (Environment.MEDIA_UNMOUNTED.equals(state)) {
-                return true;
-            }
-        } catch (RemoteException e) {
-            failStr(e);
+        if (checkMediaState(Environment.MEDIA_UNMOUNTED)) {
+            return true;
         }
 
-        StorageListener observer = new StorageListener();
+        final String path = Environment.getExternalStorageDirectory().getPath();
+        StorageListener observer = new StorageListener(Environment.MEDIA_UNMOUNTED);
         StorageManager sm = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
         sm.registerListener(observer);
         try {
@@ -976,7 +998,7 @@
         // Install pkg on sdcard
         InstallParams ip = sampleInstallFromRawResource(PackageManager.INSTALL_EXTERNAL, false);
         if (localLOGV) Log.i(TAG, "Installed pkg on sdcard");
-        boolean origState = getMediaState();
+        boolean origState = checkMediaState(Environment.MEDIA_MOUNTED);
         boolean registeredReceiver = false;
         SdMountReceiver receiver = new SdMountReceiver(new String[]{ip.pkg.packageName});
         try {
@@ -1468,7 +1490,7 @@
      */
     @LargeTest
     public void testInstallSdcardUnmount() {
-        boolean origState = getMediaState();
+        boolean origState = checkMediaState(Environment.MEDIA_MOUNTED);
         try {
             // Unmount sdcard
             assertTrue(unmountMedia());
@@ -1493,22 +1515,22 @@
      */
     @LargeTest
     public void testInstallManifestSdcardUnmount() {
-       boolean origState = getMediaState();
-       try {
-           // Unmount sdcard
-           assertTrue(unmountMedia());
-           InstallParams ip = new InstallParams("install.apk", R.raw.install_loc_sdcard);
-           installFromRawResource(ip, 0, true, false, -1,
-                   PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY);
-       } finally {
-           // Restore original media state
-           if (origState) {
-               mountMedia();
-           } else {
-               unmountMedia();
-           }
-       }
-   }
+        boolean origState = checkMediaState(Environment.MEDIA_MOUNTED);
+        try {
+            // Unmount sdcard
+            assertTrue(unmountMedia());
+            InstallParams ip = new InstallParams("install.apk", R.raw.install_loc_sdcard);
+            installFromRawResource(ip, 0, true, false, -1,
+                    PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY);
+        } finally {
+            // Restore original media state
+            if (origState) {
+                mountMedia();
+            } else {
+                unmountMedia();
+            }
+        }
+    }
 
    /*---------- Recommended install location tests ----*/
    /* Precedence: FlagManifestExistingUser
@@ -2269,7 +2291,7 @@
     @LargeTest
     public void testInstallOnSdPermissionsUnmount() {
         InstallParams ip = null;
-        boolean origMediaState = getMediaState();
+        boolean origMediaState = checkMediaState(Environment.MEDIA_MOUNTED);
         try {
             // **: Upon installing a package, are its declared permissions published?
             int iFlags = PackageManager.INSTALL_INTERNAL;
@@ -2300,8 +2322,10 @@
      */
     @LargeTest
     public void testInstallSdcardStaleContainer() {
-        boolean origMediaState = getMediaState();
+        boolean origMediaState = checkMediaState(Environment.MEDIA_MOUNTED);
         try {
+            // Mount media first
+            mountMedia();
             String outFileName = "install.apk";
             int rawResId = R.raw.install;
             PackageManager pm = mContext.getPackageManager();
@@ -2342,7 +2366,7 @@
      */
     @LargeTest
     public void testInstallSdcardStaleContainerReinstall() {
-        boolean origMediaState = getMediaState();
+        boolean origMediaState = checkMediaState(Environment.MEDIA_MOUNTED);
         try {
             // Mount media first
             mountMedia();
@@ -2375,7 +2399,6 @@
             } else {
                 unmountMedia();
             }
-
         }
     }
     /*
diff --git a/core/tests/coretests/src/android/os/storage/StorageListener.java b/core/tests/coretests/src/android/os/storage/StorageListener.java
index d6dae22..6a26b88 100644
--- a/core/tests/coretests/src/android/os/storage/StorageListener.java
+++ b/core/tests/coretests/src/android/os/storage/StorageListener.java
@@ -21,21 +21,24 @@
 public class StorageListener extends StorageEventListener {
     private static final boolean localLOGV = true;
 
-    public static final String TAG="StorageListener";
+    public static final String TAG = "StorageListener";
 
-    String oldState;
-    String newState;
-    String path;
+    private String mTargetState;
     private boolean doneFlag = false;
+
+    public StorageListener(String targetState) {
+        mTargetState = targetState;
+    }
+
     @Override
     public void onStorageStateChanged(String path, String oldState, String newState) {
         if (localLOGV) Log.i(TAG, "Storage state changed from " + oldState + " to " + newState);
+
         synchronized (this) {
-            this.oldState = oldState;
-            this.newState = newState;
-            this.path = path;
-            doneFlag = true;
-            notifyAll();
+            if (mTargetState.equals(newState)) {
+                doneFlag = true;
+                notifyAll();
+            }
         }
     }
 
diff --git a/docs/html/guide/topics/resources/color-list-resource.jd b/docs/html/guide/topics/resources/color-list-resource.jd
index 449b66f..b20915c 100644
--- a/docs/html/guide/topics/resources/color-list-resource.jd
+++ b/docs/html/guide/topics/resources/color-list-resource.jd
@@ -55,7 +55,6 @@
         android:state_pressed=["true" | "false"]
         android:state_focused=["true" | "false"]
         android:state_selected=["true" | "false"]
-        android:state_active=["true" | "false"]
         android:state_checkable=["true" | "false"]
         android:state_checked=["true" | "false"]
         android:state_enabled=["true" | "false"]
diff --git a/docs/html/guide/topics/resources/drawable-resource.jd b/docs/html/guide/topics/resources/drawable-resource.jd
index 1c3cc4d..035ddb9 100644
--- a/docs/html/guide/topics/resources/drawable-resource.jd
+++ b/docs/html/guide/topics/resources/drawable-resource.jd
@@ -638,7 +638,6 @@
         android:state_pressed=["true" | "false"]
         android:state_focused=["true" | "false"]
         android:state_selected=["true" | "false"]
-        android:state_active=["true" | "false"]
         android:state_checkable=["true" | "false"]
         android:state_checked=["true" | "false"]
         android:state_enabled=["true" | "false"]
diff --git a/docs/html/guide/topics/resources/more-resources.jd b/docs/html/guide/topics/resources/more-resources.jd
index 1f03446..a647571 100644
--- a/docs/html/guide/topics/resources/more-resources.jd
+++ b/docs/html/guide/topics/resources/more-resources.jd
@@ -310,7 +310,7 @@
 &lt;TextView
     android:layout_height="@dimen/textview_height"
     android:layout_width="@dimen/textview_width"
-    android:textSize="@dimen/sixteen_sp"/>
+    android:textSize="@dimen/font_size"/>
 </pre>
   </dl>
 </dd> <!-- end example -->
diff --git a/include/media/stagefright/OMXCodec.h b/include/media/stagefright/OMXCodec.h
index 6c6949b..010ded1 100644
--- a/include/media/stagefright/OMXCodec.h
+++ b/include/media/stagefright/OMXCodec.h
@@ -181,6 +181,11 @@
     status_t setupH263EncoderParameters(const sp<MetaData>& meta);
     status_t setupMPEG4EncoderParameters(const sp<MetaData>& meta);
     status_t setupAVCEncoderParameters(const sp<MetaData>& meta);
+    status_t findTargetColorFormat(
+            const sp<MetaData>& meta, OMX_COLOR_FORMATTYPE *colorFormat);
+
+    status_t isColorFormatSupported(
+            OMX_COLOR_FORMATTYPE colorFormat, int portIndex);
 
     // If profile/level is set in the meta data, its value in the meta
     // data will be used; otherwise, the default value will be used.
diff --git a/libs/rs/rsContext.cpp b/libs/rs/rsContext.cpp
index dfdeb98..2a94651 100644
--- a/libs/rs/rsContext.cpp
+++ b/libs/rs/rsContext.cpp
@@ -798,7 +798,7 @@
         return false;
     }
     if (!waitForSpace) {
-        if (mIO.mToClient.getFreeSpace() < len) {
+        if (mIO.mToClient.getFreeSpace() <= (len + 8)) {
             // Not enough room, and not waiting.
             return false;
         }
diff --git a/libs/utils/ZipFileRO.cpp b/libs/utils/ZipFileRO.cpp
index 28dc512..a4c3500 100644
--- a/libs/utils/ZipFileRO.cpp
+++ b/libs/utils/ZipFileRO.cpp
@@ -253,21 +253,21 @@
 
     /*
      * Grab the CD offset and size, and the number of entries in the
-     * archive.  Verify that they look reasonable.
+     * archive. After that, we can release our EOCD hunt buffer.
      */
     unsigned int numEntries = get2LE(eocdPtr + kEOCDNumEntries);
     unsigned int dirSize = get4LE(eocdPtr + kEOCDSize);
     unsigned int dirOffset = get4LE(eocdPtr + kEOCDFileOffset);
+    free(scanBuf);
 
+    // Verify that they look reasonable.
     if ((long long) dirOffset + (long long) dirSize > (long long) eocdOffset) {
         LOGW("bad offsets (dir %ld, size %u, eocd %ld)\n",
             (long) dirOffset, dirSize, (long) eocdOffset);
-        free(scanBuf);
         return false;
     }
     if (numEntries == 0) {
         LOGW("empty archive?\n");
-        free(scanBuf);
         return false;
     }
 
@@ -277,14 +277,12 @@
     mDirectoryMap = new FileMap();
     if (mDirectoryMap == NULL) {
         LOGW("Unable to create directory map: %s", strerror(errno));
-        free(scanBuf);
         return false;
     }
 
     if (!mDirectoryMap->create(mFileName, mFd, dirOffset, dirSize, true)) {
         LOGW("Unable to map '%s' (%zd to %zd): %s\n", mFileName,
                 dirOffset, dirOffset + dirSize, strerror(errno));
-        free(scanBuf);
         return false;
     }
 
@@ -683,7 +681,7 @@
             goto bail;
         } else if ((size_t) actual != uncompLen) {
             LOGE("Partial write during uncompress (%zd of %zd)\n",
-                actual, uncompLen);
+                (size_t)actual, (size_t)uncompLen);
             goto bail;
         } else {
             LOGI("+++ successful write\n");
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index 403a68e..31e4631 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -19,7 +19,7 @@
 /**
  * The AudioFormat class is used to access a number of audio format and
  * channel configuration constants. They are for instance used
- * in __link AudioTrack} and __link AudioRecord}.
+ * in {@link AudioTrack} and {@link AudioRecord}.
  * 
  */
 public class AudioFormat {
diff --git a/media/libstagefright/AwesomePlayer.cpp b/media/libstagefright/AwesomePlayer.cpp
index 436f098..e426fca 100644
--- a/media/libstagefright/AwesomePlayer.cpp
+++ b/media/libstagefright/AwesomePlayer.cpp
@@ -854,7 +854,7 @@
 
 status_t AwesomePlayer::initVideoDecoder() {
     uint32_t flags = 0;
-#if 1
+#if 0
     if (mRTPSession != NULL) {
         // XXX hack.
 
@@ -1225,7 +1225,10 @@
     } else if (!strcmp("rtsp://gtalk", mUri.string())) {
         if (mLooper == NULL) {
             mLooper = new ALooper;
-            mLooper->start();
+            mLooper->start(
+                    false /* runOnCallingThread */,
+                    false /* canCallJava */,
+                    PRIORITY_HIGHEST);
         }
 
 #if 0
@@ -1240,39 +1243,6 @@
         mLooper->registerHandler(mRTPSession);
 
 #if 0
-        // My H264 SDP
-        static const char *raw =
-            "v=0\r\n"
-            "o=- 64 233572944 IN IP4 127.0.0.0\r\n"
-            "s=QuickTime\r\n"
-            "t=0 0\r\n"
-            "a=range:npt=0-315\r\n"
-            "a=isma-compliance:2,2.0,2\r\n"
-            "m=video 5434 RTP/AVP 97\r\n"
-            "c=IN IP4 127.0.0.1\r\n"
-            "b=AS:30\r\n"
-            "a=rtpmap:97 H264/90000\r\n"
-            "a=fmtp:97 packetization-mode=1;profile-level-id=42000C;"
-              "sprop-parameter-sets=Z0IADJZUCg+I,aM44gA==\r\n"
-            "a=mpeg4-esid:201\r\n"
-            "a=cliprect:0,0,240,320\r\n"
-            "a=framesize:97 320-240\r\n";
-#elif 0
-        // My H263 SDP
-        static const char *raw =
-            "v=0\r\n"
-            "o=- 64 233572944 IN IP4 127.0.0.0\r\n"
-            "s=QuickTime\r\n"
-            "t=0 0\r\n"
-            "a=range:npt=0-315\r\n"
-            "a=isma-compliance:2,2.0,2\r\n"
-            "m=video 5434 RTP/AVP 97\r\n"
-            "c=IN IP4 127.0.0.1\r\n"
-            "b=AS:30\r\n"
-            "a=rtpmap:97 H263-1998/90000\r\n"
-            "a=cliprect:0,0,240,320\r\n"
-            "a=framesize:97 320-240\r\n";
-#elif 0
         // My AMR SDP
         static const char *raw =
             "v=0\r\n"
@@ -1299,12 +1269,9 @@
             "c=IN IP4 127.0.0.1\r\n"
             "b=AS:30\r\n"
             "a=rtpmap:97 H264/90000\r\n"
-            "a=fmtp:97 packetization-mode=1;profile-level-id=42E00D;"
-              "sprop-parameter-sets=J0LgDZWgUG/lQA==,KM4DnoA=\r\n"
-            "a=mpeg4-esid:201\r\n"
             "a=cliprect:0,0,200,320\r\n"
             "a=framesize:97 320-200\r\n";
-#elif 0
+#else
         // GTalk H263 SDP
         static const char *raw =
             "v=0\r\n"
@@ -1319,22 +1286,6 @@
             "a=rtpmap:98 H263-1998/90000\r\n"
             "a=cliprect:0,0,200,320\r\n"
             "a=framesize:98 320-200\r\n";
-#else
-    // sholes H264 SDP
-    static const char *raw =
-        "v=0\r\n"
-        "o=- 64 233572944 IN IP4 127.0.0.0\r\n"
-        "s=QuickTime\r\n"
-        "t=0 0\r\n"
-        "a=range:npt=now-\r\n"
-        "m=video 5434 RTP/AVP 96\r\n"
-        "c=IN IP4 127.0.0.1\r\n"
-        "b=AS:320000\r\n"
-        "a=rtpmap:96 H264/90000\r\n"
-        "a=fmtp:96 packetization-mode=1;profile-level-id=42001E;"
-          "sprop-parameter-sets=Z0KACukCg+QgAAB9AAAOpgCA,aM48gA==\r\n"
-        "a=cliprect:0,0,240,320\r\n"
-        "a=framesize:96 320-240\r\n";
 #endif
 
         sp<ASessionDescription> desc = new ASessionDescription;
diff --git a/media/libstagefright/MPEG4Writer.cpp b/media/libstagefright/MPEG4Writer.cpp
index 4928951..0d8c3c6 100644
--- a/media/libstagefright/MPEG4Writer.cpp
+++ b/media/libstagefright/MPEG4Writer.cpp
@@ -68,7 +68,7 @@
     bool mIsAvc;
     bool mIsAudio;
     bool mIsMPEG4;
-    int64_t mMaxTimeStampUs;
+    int64_t mTrackDurationUs;
     int64_t mEstimatedTrackSizeBytes;
     int64_t mMaxWriteTimeUs;
     int32_t mTimeScale;
@@ -711,7 +711,7 @@
       mDone(false),
       mPaused(false),
       mResumed(false),
-      mMaxTimeStampUs(0),
+      mTrackDurationUs(0),
       mEstimatedTrackSizeBytes(0),
       mSamplesHaveSameSize(true),
       mCodecSpecificData(NULL),
@@ -958,7 +958,7 @@
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
 
     mDone = false;
-    mMaxTimeStampUs = 0;
+    mTrackDurationUs = 0;
     mReachedEOS = false;
     mEstimatedTrackSizeBytes = 0;
 
@@ -1300,15 +1300,15 @@
         }
 
         if (mResumed) {
-            previousPausedDurationUs += (timestampUs - mMaxTimeStampUs - lastDurationUs);
+            previousPausedDurationUs += (timestampUs - mTrackDurationUs - lastDurationUs);
             mResumed = false;
         }
 
         timestampUs -= previousPausedDurationUs;
         LOGV("time stamp: %lld and previous paused duration %lld",
                 timestampUs, previousPausedDurationUs);
-        if (timestampUs > mMaxTimeStampUs) {
-            mMaxTimeStampUs = timestampUs;
+        if (timestampUs > mTrackDurationUs) {
+            mTrackDurationUs = timestampUs;
         }
 
         mSampleSizes.push_back(sampleSize);
@@ -1407,6 +1407,7 @@
     }
     SttsTableEntry sttsEntry(sampleCount, lastDurationUs);
     mSttsTableEntries.push_back(sttsEntry);
+    mTrackDurationUs += lastDurationUs;
     mReachedEOS = true;
     LOGI("Received total/0-length (%d/%d) buffers and encoded %d frames. Max write time: %lld us - %s",
             count, nZeroLengthFrames, mNumSamples, mMaxWriteTimeUs, mIsAudio? "audio": "video");
@@ -1472,7 +1473,7 @@
 void MPEG4Writer::Track::findMinAvgMaxSampleDurationMs(
         int32_t *min, int32_t *avg, int32_t *max) {
     CHECK(!mSampleSizes.empty());
-    int32_t avgSampleDurationMs = mMaxTimeStampUs / 1000 / mNumSamples;
+    int32_t avgSampleDurationMs = mTrackDurationUs / 1000 / mNumSamples;
     int32_t minSampleDurationMs = 0x7FFFFFFF;
     int32_t maxSampleDurationMs = 0;
     for (List<SttsTableEntry>::iterator it = mSttsTableEntries.begin();
@@ -1514,7 +1515,7 @@
 }
 
 void MPEG4Writer::Track::logStatisticalData(bool isAudio) {
-    if (mMaxTimeStampUs <= 0 || mSampleSizes.empty()) {
+    if (mTrackDurationUs <= 0 || mSampleSizes.empty()) {
         LOGI("nothing is recorded");
         return;
     }
@@ -1523,7 +1524,7 @@
 
     if (collectStats) {
         LOGI("%s track - duration %lld us, total %d frames",
-                isAudio? "audio": "video", mMaxTimeStampUs,
+                isAudio? "audio": "video", mTrackDurationUs,
                 mNumSamples);
         int32_t min, avg, max;
         findMinAvgMaxSampleDurationMs(&min, &avg, &max);
@@ -1541,7 +1542,7 @@
             it != mSampleSizes.end(); ++it) {
             totalBytes += (*it);
         }
-        float bitRate = (totalBytes * 8000000.0) / mMaxTimeStampUs;
+        float bitRate = (totalBytes * 8000000.0) / mTrackDurationUs;
         LOGI("avg bit rate (bps): %.2f", bitRate);
 
         int64_t duration = mOwner->interleaveDuration();
@@ -1568,7 +1569,7 @@
 }
 
 int64_t MPEG4Writer::Track::getDurationUs() const {
-    return mMaxTimeStampUs;
+    return mTrackDurationUs;
 }
 
 int64_t MPEG4Writer::Track::getEstimatedTrackSizeBytes() const {
diff --git a/media/libstagefright/OMXCodec.cpp b/media/libstagefright/OMXCodec.cpp
index d19fbe5..4f3bffd 100644
--- a/media/libstagefright/OMXCodec.cpp
+++ b/media/libstagefright/OMXCodec.cpp
@@ -762,6 +762,65 @@
     }
 }
 
+status_t OMXCodec::findTargetColorFormat(
+        const sp<MetaData>& meta, OMX_COLOR_FORMATTYPE *colorFormat) {
+    LOGV("findTargetColorFormat");
+    CHECK(mIsEncoder);
+
+    *colorFormat = OMX_COLOR_FormatYUV420SemiPlanar;
+    int32_t targetColorFormat;
+    if (meta->findInt32(kKeyColorFormat, &targetColorFormat)) {
+        *colorFormat = (OMX_COLOR_FORMATTYPE) targetColorFormat;
+    } else {
+        if (!strcasecmp("OMX.TI.Video.encoder", mComponentName)) {
+            *colorFormat = OMX_COLOR_FormatYCbYCr;
+        }
+    }
+
+    // Check whether the target color format is supported.
+    return isColorFormatSupported(*colorFormat, kPortIndexInput);
+}
+
+status_t OMXCodec::isColorFormatSupported(
+        OMX_COLOR_FORMATTYPE colorFormat, int portIndex) {
+    LOGV("isColorFormatSupported: %d", static_cast<int>(colorFormat));
+
+    // Enumerate all the color formats supported by
+    // the omx component to see whether the given
+    // color format is supported.
+    OMX_VIDEO_PARAM_PORTFORMATTYPE portFormat;
+    InitOMXParams(&portFormat);
+    portFormat.nPortIndex = portIndex;
+    OMX_U32 index = 0;
+    portFormat.nIndex = index;
+    while (true) {
+        if (OMX_ErrorNone != mOMX->getParameter(
+                mNode, OMX_IndexParamVideoPortFormat,
+                &portFormat, sizeof(portFormat))) {
+
+            return UNKNOWN_ERROR;
+        }
+        // Make sure that omx component does not overwrite
+        // the incremented index (bug 2897413).
+        CHECK_EQ(index, portFormat.nIndex);
+        if ((portFormat.eColorFormat == colorFormat)) {
+            LOGV("Found supported color format: %d", portFormat.eColorFormat);
+            return OK;  // colorFormat is supported!
+        }
+        ++index;
+        portFormat.nIndex = index;
+
+        // OMX Spec defines less than 50 color formats
+        // 1000 is more than enough for us to tell whether the omx
+        // component in question is buggy or not.
+        if (index >= 1000) {
+            LOGE("More than %ld color formats are supported???", index);
+            break;
+        }
+    }
+    return UNKNOWN_ERROR;
+}
+
 void OMXCodec::setVideoInputFormat(
         const char *mime, const sp<MetaData>& meta) {
 
@@ -787,10 +846,8 @@
         CHECK(!"Should not be here. Not a supported video mime type.");
     }
 
-    OMX_COLOR_FORMATTYPE colorFormat = OMX_COLOR_FormatYUV420SemiPlanar;
-    if (!strcasecmp("OMX.TI.Video.encoder", mComponentName)) {
-        colorFormat = OMX_COLOR_FormatYCbYCr;
-    }
+    OMX_COLOR_FORMATTYPE colorFormat;
+    CHECK_EQ(OK, findTargetColorFormat(meta, &colorFormat));
 
     status_t err;
     OMX_PARAM_PORTDEFINITIONTYPE def;
diff --git a/media/libstagefright/OggExtractor.cpp b/media/libstagefright/OggExtractor.cpp
index 16f094b..641a876 100644
--- a/media/libstagefright/OggExtractor.cpp
+++ b/media/libstagefright/OggExtractor.cpp
@@ -361,11 +361,17 @@
                 memcpy(tmp->data(), buffer->data(), buffer->range_length());
                 tmp->set_range(0, buffer->range_length());
                 buffer->release();
-            } else if (mVi.rate) {
+            } else {
                 // XXX Not only is this not technically the correct time for
                 // this packet, we also stamp every packet in this page
                 // with the same time. This needs fixing later.
-                timeUs = mCurrentPage.mGranulePosition * 1000000ll / mVi.rate;
+
+                if (mVi.rate) {
+                    // Rate may not have been initialized yet if we're currently
+                    // reading the configuration packets...
+                    // Fortunately, the timestamp doesn't matter for those.
+                    timeUs = mCurrentPage.mGranulePosition * 1000000ll / mVi.rate;
+                }
                 tmp->set_range(0, 0);
             }
             buffer = tmp;
diff --git a/media/libstagefright/rtsp/APacketSource.cpp b/media/libstagefright/rtsp/APacketSource.cpp
index 88336ba..a577704 100644
--- a/media/libstagefright/rtsp/APacketSource.cpp
+++ b/media/libstagefright/rtsp/APacketSource.cpp
@@ -92,7 +92,9 @@
 
 static sp<ABuffer> MakeAVCCodecSpecificData(const char *params) {
     AString val;
-    CHECK(GetAttribute(params, "profile-level-id", &val));
+    if (!GetAttribute(params, "profile-level-id", &val)) {
+        return NULL;
+    }
 
     sp<ABuffer> profileLevelID = decodeHex(val);
     CHECK(profileLevelID != NULL);
@@ -105,7 +107,10 @@
     size_t numPicParameterSets = 0;
     size_t totalPicParameterSetSize = 0;
 
-    CHECK(GetAttribute(params, "sprop-parameter-sets", &val));
+    if (!GetAttribute(params, "sprop-parameter-sets", &val)) {
+        return NULL;
+    }
+
     size_t start = 0;
     for (;;) {
         ssize_t commaPos = val.find(",", start);
@@ -256,9 +261,11 @@
         sp<ABuffer> codecSpecificData =
             MakeAVCCodecSpecificData(params.c_str());
 
-        mFormat->setData(
-                kKeyAVCC, 0,
-                codecSpecificData->data(), codecSpecificData->size());
+        if (codecSpecificData != NULL) {
+            mFormat->setData(
+                    kKeyAVCC, 0,
+                    codecSpecificData->data(), codecSpecificData->size());
+        }
     } else if (!strncmp(desc.c_str(), "H263-2000/", 10)
             || !strncmp(desc.c_str(), "H263-1998/", 10)) {
         mFormat->setCString(kKeyMIMEType, MEDIA_MIMETYPE_VIDEO_H263);
diff --git a/media/libstagefright/rtsp/ARTPConnection.cpp b/media/libstagefright/rtsp/ARTPConnection.cpp
index 9abdab4..5bd306b 100644
--- a/media/libstagefright/rtsp/ARTPConnection.cpp
+++ b/media/libstagefright/rtsp/ARTPConnection.cpp
@@ -405,6 +405,14 @@
     buffer->setInt32Data(u16at(&data[2]));
     buffer->setRange(payloadOffset, size - payloadOffset);
 
+#if IGNORE_RTCP_TIME
+    if (!source->timeEstablished()) {
+        source->timeUpdate(rtpTime, 0);
+        source->timeUpdate(rtpTime + 20, 0x100000000ll);
+        CHECK(source->timeEstablished());
+    }
+#endif
+
     source->processRTPPacket(buffer);
 
     return OK;
@@ -541,12 +549,6 @@
         source = new ARTPSource(
                 srcId, info->mSessionDesc, info->mIndex, info->mNotifyMsg);
 
-#if IGNORE_RTCP_TIME
-        // For H.263 gtalk to work...
-        source->timeUpdate(0, 0);
-        source->timeUpdate(30, 0x100000000ll);
-#endif
-
         info->mSources.add(srcId, source);
     } else {
         source = info->mSources.valueAt(index);
diff --git a/media/libstagefright/rtsp/ARTPSource.cpp b/media/libstagefright/rtsp/ARTPSource.cpp
index 2aa0c1f..e08183e 100644
--- a/media/libstagefright/rtsp/ARTPSource.cpp
+++ b/media/libstagefright/rtsp/ARTPSource.cpp
@@ -58,6 +58,7 @@
     } else if (!strncmp(desc.c_str(), "H263-1998/", 10)
             || !strncmp(desc.c_str(), "H263-2000/", 10)) {
         mAssembler = new AH263Assembler(notify);
+        mIssueFIRRequests = true;
     } else if (!strncmp(desc.c_str(), "AMR/", 4)) {
         mAssembler = new AAMRAssembler(notify, false /* isWide */, params);
     } else  if (!strncmp(desc.c_str(), "AMR-WB/", 7)) {
diff --git a/media/libstagefright/rtsp/ARTPSource.h b/media/libstagefright/rtsp/ARTPSource.h
index 8e483a8..252d6d6 100644
--- a/media/libstagefright/rtsp/ARTPSource.h
+++ b/media/libstagefright/rtsp/ARTPSource.h
@@ -46,6 +46,10 @@
     void addReceiverReport(const sp<ABuffer> &buffer);
     void addFIR(const sp<ABuffer> &buffer);
 
+    bool timeEstablished() const {
+        return mNumTimes == 2;
+    }
+
 private:
     uint32_t mID;
     uint32_t mHighestSeqNumber;
diff --git a/preloaded-classes b/preloaded-classes
index 6a10bb3..33dba37 100644
--- a/preloaded-classes
+++ b/preloaded-classes
@@ -697,8 +697,6 @@
 com.ibm.icu4jni.charset.CharsetICU
 com.ibm.icu4jni.charset.NativeConverter
 com.ibm.icu4jni.common.ErrorCode
-com.ibm.icu4jni.lang.UCharacter
-com.ibm.icu4jni.regex.NativeRegEx
 com.ibm.icu4jni.text.Collator
 com.ibm.icu4jni.text.NativeBreakIterator
 com.ibm.icu4jni.text.NativeCollation
@@ -1168,7 +1166,6 @@
 org.apache.harmony.security.x509.TBSCertificate
 org.apache.harmony.security.x509.Time
 org.apache.harmony.security.x509.Validity
-org.apache.harmony.text.BidiWrapper
 org.apache.harmony.xml.ExpatAttributes
 org.apache.harmony.xml.ExpatParser
 org.apache.harmony.xml.ExpatPullParser
diff --git a/services/java/com/android/server/ClipboardService.java b/services/java/com/android/server/ClipboardService.java
index aa8cded..4e4fc0c 100644
--- a/services/java/com/android/server/ClipboardService.java
+++ b/services/java/com/android/server/ClipboardService.java
@@ -16,42 +16,77 @@
 
 package com.android.server;
 
-import android.text.IClipboard;
+import android.content.ClippedData;
+import android.content.IClipboard;
+import android.content.IOnPrimaryClipChangedListener;
 import android.content.Context;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
 
 /**
  * Implementation of the clipboard for copy and paste.
  */
 public class ClipboardService extends IClipboard.Stub {
-    private CharSequence mClipboard = "";
+    private ClippedData mPrimaryClip;
+    private final RemoteCallbackList<IOnPrimaryClipChangedListener> mPrimaryClipListeners
+            = new RemoteCallbackList<IOnPrimaryClipChangedListener>();
 
     /**
      * Instantiates the clipboard.
      */
     public ClipboardService(Context context) { }
 
-    // javadoc from interface
-    public void setClipboardText(CharSequence text) {
+    public void setPrimaryClip(ClippedData clip) {
         synchronized (this) {
-            if (text == null) {
-                text = "";
+            if (clip != null && clip.getItemCount() <= 0) {
+                throw new IllegalArgumentException("No items");
             }
+            mPrimaryClip = clip;
+            final int n = mPrimaryClipListeners.beginBroadcast();
+            for (int i = 0; i < n; i++) {
+                try {
+                    mPrimaryClipListeners.getBroadcastItem(i).dispatchPrimaryClipChanged();
+                } catch (RemoteException e) {
+
+                    // The RemoteCallbackList will take care of removing
+                    // the dead object for us.
+                }
+            }
+            mPrimaryClipListeners.finishBroadcast();
+        }
+    }
     
-            mClipboard = text;
-        }
-    }
-
-    // javadoc from interface
-    public CharSequence getClipboardText() {
+    public ClippedData getPrimaryClip() {
         synchronized (this) {
-            return mClipboard;
+            return mPrimaryClip;
         }
     }
 
-    // javadoc from interface
+    public boolean hasPrimaryClip() {
+        synchronized (this) {
+            return mPrimaryClip != null;
+        }
+    }
+
+    public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
+        synchronized (this) {
+            mPrimaryClipListeners.register(listener);
+        }
+    }
+
+    public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
+        synchronized (this) {
+            mPrimaryClipListeners.unregister(listener);
+        }
+    }
+
     public boolean hasClipboardText() {
         synchronized (this) {
-            return mClipboard.length() > 0;
+            if (mPrimaryClip != null) {
+                CharSequence text = mPrimaryClip.getItem(0).getText();
+                return text != null && text.length() > 0;
+            }
+            return false;
         }
     }
 }
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index 4f11231..67796c6 100755
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -68,7 +68,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 
-class NotificationManagerService extends INotificationManager.Stub
+/** {@hide} */
+public class NotificationManagerService extends INotificationManager.Stub
 {
     private static final String TAG = "NotificationService";
     private static final boolean DBG = false;
@@ -316,7 +317,8 @@
 
         public void onNotificationError(String pkg, String tag, int id,
                 int uid, int initialPid, String message) {
-            Slog.d(TAG, "onNotification error pkg=" + pkg + " tag=" + tag + " id=" + id);
+            Slog.d(TAG, "onNotification error pkg=" + pkg + " tag=" + tag + " id=" + id
+                    + "; will crashApplication(uid=" + uid + ", pid=" + initialPid + ")");
             cancelNotification(pkg, tag, id, 0, 0);
             long ident = Binder.clearCallingIdentity();
             try {
@@ -671,11 +673,20 @@
         enqueueNotificationWithTag(pkg, null /* tag */, id, notification, idOut);
     }
 
-    public void enqueueNotificationWithTag(String pkg, String tag, int id,
-            Notification notification, int[] idOut)
+    public void enqueueNotificationWithTag(String pkg, String tag, int id, Notification notification,
+            int[] idOut)
     {
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
+        enqueueNotificationInternal(pkg, Binder.getCallingUid(), Binder.getCallingPid(),
+                tag, id, notification, idOut);
+    }
+
+    // Not exposed via Binder; for system use only (otherwise malicious apps could spoof the
+    // uid/pid of another application)
+    public void enqueueNotificationInternal(String pkg, int callingUid, int callingPid,
+            String tag, int id, Notification notification, int[] idOut)
+    {
+        Slog.d(TAG, "enqueueNotificationWithTag: calling uid=" + callingUid 
+                + ", pid=" + callingPid);
         
         checkIncomingCall(pkg);
 
diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java
index 415073e..a32cd4c 100644
--- a/services/java/com/android/server/am/ActivityManagerService.java
+++ b/services/java/com/android/server/am/ActivityManagerService.java
@@ -2359,6 +2359,10 @@
             }
             
             if (proc.thread != null) {
+                if (proc.pid == Process.myPid()) {
+                    Log.w(TAG, "crashApplication: trying to crash self!");
+                    return;
+                }
                 long ident = Binder.clearCallingIdentity();
                 try {
                     proc.thread.scheduleCrash(message);
diff --git a/services/java/com/android/server/am/ServiceRecord.java b/services/java/com/android/server/am/ServiceRecord.java
index 0542497..75365ad 100644
--- a/services/java/com/android/server/am/ServiceRecord.java
+++ b/services/java/com/android/server/am/ServiceRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import com.android.internal.os.BatteryStatsImpl;
+import com.android.server.NotificationManagerService;
 
 import android.app.INotificationManager;
 import android.app.Notification;
@@ -252,6 +253,8 @@
     }
     
     public void postNotification() {
+        final int appUid = appInfo.uid;
+        final int appPid = app.pid;
         if (foregroundId != 0 && foregroundNoti != null) {
             // Do asynchronous communication with notification manager to
             // avoid deadlocks.
@@ -260,14 +263,15 @@
             final Notification localForegroundNoti = foregroundNoti;
             ams.mHandler.post(new Runnable() {
                 public void run() {
-                    INotificationManager inm = NotificationManager.getService();
-                    if (inm == null) {
+                    NotificationManagerService nm =
+                            (NotificationManagerService) NotificationManager.getService();
+                    if (nm == null) {
                         return;
                     }
                     try {
                         int[] outId = new int[1];
-                        inm.enqueueNotification(localPackageName, localForegroundId,
-                                localForegroundNoti, outId);
+                        nm.enqueueNotificationInternal(localPackageName, appUid, appPid,
+                                null, localForegroundId, localForegroundNoti, outId);
                     } catch (RuntimeException e) {
                         Slog.w(ActivityManagerService.TAG,
                                 "Error showing notification for service", e);
@@ -275,7 +279,6 @@
                         // get to be foreground.
                         ams.setServiceForeground(name, ServiceRecord.this,
                                 localForegroundId, null, true);
-                    } catch (RemoteException e) {
                     }
                 }
             });
diff --git a/tests/DumpRenderTree/src/com/android/dumprendertree/TestShellActivity.java b/tests/DumpRenderTree/src/com/android/dumprendertree/TestShellActivity.java
index f4773b5..8c7254c 100644
--- a/tests/DumpRenderTree/src/com/android/dumprendertree/TestShellActivity.java
+++ b/tests/DumpRenderTree/src/com/android/dumprendertree/TestShellActivity.java
@@ -754,7 +754,7 @@
 
     private static class NewWindowWebView extends WebView {
         public NewWindowWebView(Context context, Map<String, Object> jsIfaces) {
-            super(context, null, 0, jsIfaces);
+            super(context, null, 0, jsIfaces, false);
         }
     }
 
diff --git a/voip/java/android/net/rtp/AudioCodec.java b/voip/java/android/net/rtp/AudioCodec.java
new file mode 100644
index 0000000..89e6aa9
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioCodec.java
@@ -0,0 +1,56 @@
+/*
+ * 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 android.net.rtp;
+
+/** @hide */
+public class AudioCodec {
+    public static final AudioCodec ULAW = new AudioCodec("PCMU", 8000, 160, 0);
+    public static final AudioCodec ALAW = new AudioCodec("PCMA", 8000, 160, 8);
+
+    /**
+     * Returns system supported codecs.
+     */
+    public static AudioCodec[] getSystemSupportedCodecs() {
+        return new AudioCodec[] {AudioCodec.ULAW, AudioCodec.ALAW};
+    }
+
+    /**
+     * Returns the codec instance if it is supported by the system.
+     *
+     * @param name name of the codec
+     * @return the matched codec or null if the codec name is not supported by
+     *      the system
+     */
+    public static AudioCodec getSystemSupportedCodec(String name) {
+        for (AudioCodec codec : getSystemSupportedCodecs()) {
+            if (codec.name.equals(name)) return codec;
+        }
+        return null;
+    }
+
+    public final String name;
+    public final int sampleRate;
+    public final int sampleCount;
+    public final int defaultType;
+
+    private AudioCodec(String name, int sampleRate, int sampleCount, int defaultType) {
+        this.name = name;
+        this.sampleRate = sampleRate;
+        this.sampleCount = sampleCount;
+        this.defaultType = defaultType;
+    }
+}
diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java
new file mode 100644
index 0000000..dc86082
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioGroup.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 android.net.rtp;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ */
+/** @hide */
+public class AudioGroup {
+    public static final int MODE_ON_HOLD = 0;
+    public static final int MODE_MUTED = 1;
+    public static final int MODE_NORMAL = 2;
+    public static final int MODE_EC_ENABLED = 3;
+
+    private final Map<AudioStream, Integer> mStreams;
+    private int mMode = MODE_ON_HOLD;
+
+    private int mNative;
+    static {
+        System.loadLibrary("rtp_jni");
+    }
+
+    public AudioGroup() {
+        mStreams = new HashMap<AudioStream, Integer>();
+    }
+
+    public int getMode() {
+        return mMode;
+    }
+
+    public synchronized native void setMode(int mode);
+
+    synchronized void add(AudioStream stream, AudioCodec codec, int codecType, int dtmfType) {
+        if (!mStreams.containsKey(stream)) {
+            try {
+                int id = add(stream.getMode(), stream.dup(),
+                        stream.getRemoteAddress().getHostAddress(), stream.getRemotePort(),
+                        codec.name, codec.sampleRate, codec.sampleCount, codecType, dtmfType);
+                mStreams.put(stream, id);
+            } catch (NullPointerException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+    }
+
+    private native int add(int mode, int socket, String remoteAddress, int remotePort,
+            String codecName, int sampleRate, int sampleCount, int codecType, int dtmfType);
+
+    synchronized void remove(AudioStream stream) {
+        Integer id = mStreams.remove(stream);
+        if (id != null) {
+            remove(id);
+        }
+    }
+
+    private native void remove(int id);
+
+    /**
+     * Sends a DTMF digit to every {@link AudioStream} in this group. Currently
+     * only event {@code 0} to {@code 15} are supported.
+     *
+     * @throws IllegalArgumentException if the event is invalid.
+     */
+    public native synchronized void sendDtmf(int event);
+
+    public synchronized void reset() {
+        remove(-1);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        reset();
+        super.finalize();
+    }
+}
diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java
new file mode 100644
index 0000000..a955fd2
--- /dev/null
+++ b/voip/java/android/net/rtp/AudioStream.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.net.rtp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+
+/**
+ * AudioStream represents a RTP stream carrying audio payloads.
+ */
+/** @hide */
+public class AudioStream extends RtpStream {
+    private AudioCodec mCodec;
+    private int mCodecType = -1;
+    private int mDtmfType = -1;
+    private AudioGroup mGroup;
+
+    /**
+     * Creates an AudioStream on the given local address. Note that the local
+     * port is assigned automatically to conform with RFC 3550.
+     *
+     * @param address The network address of the local host to bind to.
+     * @throws SocketException if the address cannot be bound or a problem
+     *     occurs during binding.
+     */
+    public AudioStream(InetAddress address) throws SocketException {
+        super(address);
+    }
+
+    /**
+     * Returns {@code true} if the stream already joined an {@link AudioGroup}.
+     */
+    @Override
+    public final boolean isBusy() {
+        return mGroup != null;
+    }
+
+    /**
+     * Returns the joined {@link AudioGroup}.
+     */
+    public AudioGroup getAudioGroup() {
+        return mGroup;
+    }
+
+    /**
+     * Joins an {@link AudioGroup}. Each stream can join only one group at a
+     * time. The group can be changed by passing a different one or removed
+     * by calling this method with {@code null}.
+     *
+     * @param group The AudioGroup to join or {@code null} to leave.
+     * @throws IllegalStateException if the stream is not properly configured.
+     * @see AudioGroup
+     */
+    public void join(AudioGroup group) {
+        if (mGroup == group) {
+            return;
+        }
+        if (mGroup != null) {
+            mGroup.remove(this);
+            mGroup = null;
+        }
+        if (group != null) {
+            group.add(this, mCodec, mCodecType, mDtmfType);
+            mGroup = group;
+        }
+    }
+
+    /**
+     * Sets the {@link AudioCodec} and its RTP payload type. According to RFC
+     * 3551, the type must be in the range of 0 and 127, where 96 and above are
+     * dynamic types. For codecs with static mappings (non-negative
+     * {@link AudioCodec#defaultType}), assigning a different non-dynamic type
+     * is disallowed.
+     *
+     * @param codec The AudioCodec to be used.
+     * @param type The RTP payload type.
+     * @throws IllegalArgumentException if the type is invalid or used by DTMF.
+     * @throws IllegalStateException if the stream is busy.
+     */
+    public void setCodec(AudioCodec codec, int type) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (type < 0 || type > 127 || (type != codec.defaultType && type < 96)) {
+            throw new IllegalArgumentException("Invalid type");
+        }
+        if (type == mDtmfType) {
+            throw new IllegalArgumentException("The type is used by DTMF");
+        }
+        mCodec = codec;
+        mCodecType = type;
+    }
+
+    /**
+     * Sets the RTP payload type for dual-tone multi-frequency (DTMF) digits.
+     * The primary usage is to send digits to the remote gateway to perform
+     * certain tasks, such as second-stage dialing. According to RFC 2833, the
+     * RTP payload type for DTMF is assigned dynamically, so it must be in the
+     * range of 96 and 127. One can use {@code -1} to disable DTMF and free up
+     * the previous assigned value. This method cannot be called when the stream
+     * already joined an {@link AudioGroup}.
+     *
+     * @param type The RTP payload type to be used or {@code -1} to disable it.
+     * @throws IllegalArgumentException if the type is invalid or used by codec.
+     * @throws IllegalStateException if the stream is busy.
+     * @see AudioGroup#sendDtmf(int)
+     */
+    public void setDtmfType(int type) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (type != -1) {
+            if (type < 96 || type > 127) {
+                throw new IllegalArgumentException("Invalid type");
+            }
+            if (type == mCodecType) {
+                throw new IllegalArgumentException("The type is used by codec");
+            }
+        }
+        mDtmfType = type;
+    }
+}
diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java
new file mode 100644
index 0000000..ef5ca17
--- /dev/null
+++ b/voip/java/android/net/rtp/RtpStream.java
@@ -0,0 +1,173 @@
+/*
+ * 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 android.net.rtp;
+
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketException;
+
+/**
+ * RtpStream represents a base class of media streams running over
+ * Real-time Transport Protocol (RTP).
+ */
+/** @hide */
+public class RtpStream {
+    public static final int MODE_NORMAL = 0;
+    public static final int MODE_SEND_ONLY = 1;
+    public static final int MODE_RECEIVE_ONLY = 2;
+
+    private final InetAddress mLocalAddress;
+    private final int mLocalPort;
+
+    private InetAddress mRemoteAddress;
+    private int mRemotePort = -1;
+    private int mMode = MODE_NORMAL;
+
+    private int mNative;
+    static {
+        System.loadLibrary("rtp_jni");
+    }
+
+    /**
+     * Creates a RtpStream on the given local address. Note that the local
+     * port is assigned automatically to conform with RFC 3550.
+     *
+     * @param address The network address of the local host to bind to.
+     * @throws SocketException if the address cannot be bound or a problem
+     *     occurs during binding.
+     */
+    RtpStream(InetAddress address) throws SocketException {
+        mLocalPort = create(address.getHostAddress());
+        mLocalAddress = address;
+    }
+
+    private native int create(String address) throws SocketException;
+
+    /**
+     * Returns the network address of the local host.
+     */
+    public InetAddress getLocalAddress() {
+        return mLocalAddress;
+    }
+
+    /**
+     * Returns the network port of the local host.
+     */
+    public int getLocalPort() {
+        return mLocalPort;
+    }
+
+    /**
+     * Returns the network address of the remote host or {@code null} if the
+     * stream is not associated.
+     */
+    public InetAddress getRemoteAddress() {
+        return mRemoteAddress;
+    }
+
+    /**
+     * Returns the network port of the remote host or {@code -1} if the stream
+     * is not associated.
+     */
+    public int getRemotePort() {
+        return mRemotePort;
+    }
+
+    /**
+     * Returns {@code true} if the stream is busy. This method is intended to be
+     * overridden by subclasses.
+     */
+    public boolean isBusy() {
+        return false;
+    }
+
+    /**
+     * Returns the current mode. The initial mode is {@link #MODE_NORMAL}.
+     */
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Changes the current mode. It must be one of {@link #MODE_NORMAL},
+     * {@link #MODE_SEND_ONLY}, and {@link #MODE_RECEIVE_ONLY}.
+     *
+     * @param mode The mode to change to.
+     * @throws IllegalArgumentException if the mode is invalid.
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void setMode(int mode) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (mode != MODE_NORMAL && mode != MODE_SEND_ONLY && mode != MODE_RECEIVE_ONLY) {
+            throw new IllegalArgumentException("Invalid mode");
+        }
+        mMode = mode;
+    }
+
+    /**
+     * Associates with a remote host.
+     *
+     * @param address The network address of the remote host.
+     * @param port The network port of the remote host.
+     * @throws IllegalArgumentException if the address is not supported or the
+     *     port is invalid.
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void associate(InetAddress address, int port) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (!(address instanceof Inet4Address && mLocalAddress instanceof Inet4Address) &&
+                !(address instanceof Inet6Address && mLocalAddress instanceof Inet6Address)) {
+            throw new IllegalArgumentException("Unsupported address");
+        }
+        if (port < 0 || port > 65535) {
+            throw new IllegalArgumentException("Invalid port");
+        }
+        mRemoteAddress = address;
+        mRemotePort = port;
+    }
+
+    synchronized native int dup();
+
+    /**
+     * Releases allocated resources. The stream becomes inoperable after calling
+     * this method.
+     *
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void release() {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        close();
+    }
+
+    private synchronized native void close();
+
+    @Override
+    protected void finalize() throws Throwable {
+        close();
+        super.finalize();
+    }
+}
diff --git a/voip/java/android/net/sip/BinderHelper.java b/voip/java/android/net/sip/BinderHelper.java
new file mode 100644
index 0000000..bd3da32
--- /dev/null
+++ b/voip/java/android/net/sip/BinderHelper.java
@@ -0,0 +1,79 @@
+/*
+ * 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 android.net.sip;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Looper;
+import android.util.Log;
+
+// TODO: throw away this class after moving SIP classes to framework
+// This class helps to get IBinder instance of a service in a blocking call.
+// The method cannot be called in app's main thread as the ServiceConnection
+// callback will.
+class BinderHelper<T extends IInterface> {
+    private Context mContext;
+    private IBinder mBinder;
+    private Class<T> mClass;
+
+    BinderHelper(Context context, Class<T> klass) {
+        mContext = context;
+        mClass = klass;
+    }
+
+    void startService() {
+        mContext.startService(new Intent(mClass.getName()));
+    }
+
+    void stopService() {
+        mContext.stopService(new Intent(mClass.getName()));
+    }
+
+    IBinder getBinder() {
+        // cannot call this method in app's main thread
+        if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
+            throw new RuntimeException(
+                    "This method cannot be called in app's main thread");
+        }
+
+        final ConditionVariable cv = new ConditionVariable();
+        cv.close();
+        ServiceConnection c = new ServiceConnection() {
+            public synchronized void onServiceConnected(
+                    ComponentName className, IBinder binder) {
+                Log.v("BinderHelper", "service connected!");
+                mBinder = binder;
+                cv.open();
+                mContext.unbindService(this);
+            }
+
+            public void onServiceDisconnected(ComponentName className) {
+                cv.open();
+                mContext.unbindService(this);
+            }
+        };
+        if (mContext.bindService(new Intent(mClass.getName()), c, 0)) {
+            cv.block(4500);
+        }
+        return mBinder;
+    }
+}
diff --git a/voip/java/android/net/sip/ISipService.aidl b/voip/java/android/net/sip/ISipService.aidl
new file mode 100644
index 0000000..6c68213
--- /dev/null
+++ b/voip/java/android/net/sip/ISipService.aidl
@@ -0,0 +1,42 @@
+/*
+ * 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 android.net.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipProfile;
+
+/**
+ * {@hide}
+ */
+interface ISipService {
+    void open(in SipProfile localProfile);
+    void open3(in SipProfile localProfile,
+            String incomingCallBroadcastAction,
+            in ISipSessionListener listener);
+    void close(in String localProfileUri);
+    boolean isOpened(String localProfileUri);
+    boolean isRegistered(String localProfileUri);
+    void setRegistrationListener(String localProfileUri,
+            ISipSessionListener listener);
+
+    ISipSession createSession(in SipProfile localProfile,
+            in ISipSessionListener listener);
+    ISipSession getPendingSession(String callId);
+
+    SipProfile[] getListOfProfiles();
+}
diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl
new file mode 100644
index 0000000..fbcb056
--- /dev/null
+++ b/voip/java/android/net/sip/ISipSession.aidl
@@ -0,0 +1,147 @@
+/*
+ * 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 android.net.sip;
+
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SessionDescription;
+import android.net.sip.SipProfile;
+
+/**
+ * A SIP session that is associated with a SIP dialog or a transaction
+ * (e.g., registration) not within a dialog.
+ * @hide
+ */
+interface ISipSession {
+    /**
+     * Gets the IP address of the local host on which this SIP session runs.
+     *
+     * @return the IP address of the local host
+     */
+    String getLocalIp();
+
+    /**
+     * Gets the SIP profile that this session is associated with.
+     *
+     * @return the SIP profile that this session is associated with
+     */
+    SipProfile getLocalProfile();
+
+    /**
+     * Gets the SIP profile that this session is connected to. Only available
+     * when the session is associated with a SIP dialog.
+     *
+     * @return the SIP profile that this session is connected to
+     */
+    SipProfile getPeerProfile();
+
+    /**
+     * Gets the session state. The value returned must be one of the states in
+     * {@link SipSessionState}. One may convert it to {@link SipSessionState} by
+     * <code>
+     *      Enum.valueOf(SipSessionState.class, session.getState());
+     * </code>
+     *
+     * @return the session state
+     */
+    String getState();
+
+    /**
+     * Checks if the session is in a call.
+     *
+     * @return true if the session is in a call
+     */
+    boolean isInCall();
+
+    /**
+     * Gets the call ID of the session.
+     *
+     * @return the call ID
+     */
+    String getCallId();
+
+
+    /**
+     * Sets the listener to listen to the session events. A {@link ISipSession}
+     * can only hold one listener at a time. Subsequent calls to this method
+     * override the previous listener.
+     *
+     * @param listener to listen to the session events of this object
+     */
+    void setListener(in ISipSessionListener listener);
+
+
+    /**
+     * Performs registration to the server specified by the associated local
+     * profile. The session listener is called back upon success or failure of
+     * registration. The method is only valid to call when the session state is
+     * in {@link SipSessionState#READY_TO_CALL}.
+     *
+     * @param duration duration in second before the registration expires
+     * @see ISipSessionListener
+     */
+    void register(int duration);
+
+    /**
+     * Performs unregistration to the server specified by the associated local
+     * profile. Unregistration is technically the same as registration with zero
+     * expiration duration. The session listener is called back upon success or
+     * failure of unregistration. The method is only valid to call when the
+     * session state is in {@link SipSessionState#READY_TO_CALL}.
+     *
+     * @see ISipSessionListener
+     */
+    void unregister();
+
+    /**
+     * Initiates a call to the specified profile. The session listener is called
+     * back upon defined session events. The method is only valid to call when
+     * the session state is in {@link SipSessionState#READY_TO_CALL}.
+     *
+     * @param callee the SIP profile to make the call to
+     * @param sessionDescription the session description of this call
+     * @see ISipSessionListener
+     */
+    void makeCall(in SipProfile callee,
+            in SessionDescription sessionDescription);
+
+    /**
+     * Answers an incoming call with the specified session description. The
+     * method is only valid to call when the session state is in
+     * {@link SipSessionState#INCOMING_CALL}.
+     *
+     * @param sessionDescription the session description to answer this call
+     */
+    void answerCall(in SessionDescription sessionDescription);
+
+    /**
+     * Ends an established call, terminates an outgoing call or rejects an
+     * incoming call. The method is only valid to call when the session state is
+     * in {@link SipSessionState#IN_CALL},
+     * {@link SipSessionState#INCOMING_CALL},
+     * {@link SipSessionState#OUTGOING_CALL} or
+     * {@link SipSessionState#OUTGOING_CALL_RING_BACK}.
+     */
+    void endCall();
+
+    /**
+     * Changes the session description during a call. The method is only valid
+     * to call when the session state is in {@link SipSessionState#IN_CALL}.
+     *
+     * @param sessionDescription the new session description
+     */
+    void changeCall(in SessionDescription sessionDescription);
+}
diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl
new file mode 100644
index 0000000..8570958
--- /dev/null
+++ b/voip/java/android/net/sip/ISipSessionListener.aidl
@@ -0,0 +1,126 @@
+/*
+ * 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 android.net.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.SipProfile;
+
+/**
+ * Listener class to listen to SIP session events.
+ * @hide
+ */
+interface ISipSessionListener {
+    /**
+     * Called when an INVITE request is sent to initiate a new call.
+     *
+     * @param session the session object that carries out the transaction
+     */
+    void onCalling(in ISipSession session);
+
+    /**
+     * Called when an INVITE request is received.
+     *
+     * @param session the session object that carries out the transaction
+     * @param caller the SIP profile of the caller
+     * @param sessionDescription the caller's session description
+     */
+    void onRinging(in ISipSession session, in SipProfile caller,
+            in byte[] sessionDescription);
+
+    /**
+     * Called when a RINGING response is received for the INVITE request sent
+     *
+     * @param session the session object that carries out the transaction
+     */
+    void onRingingBack(in ISipSession session);
+
+    /**
+     * Called when the session is established.
+     *
+     * @param session the session object that is associated with the dialog
+     * @param sessionDescription the peer's session description
+     */
+    void onCallEstablished(in ISipSession session,
+            in byte[] sessionDescription);
+
+    /**
+     * Called when the session is terminated.
+     *
+     * @param session the session object that is associated with the dialog
+     */
+    void onCallEnded(in ISipSession session);
+
+    /**
+     * Called when the peer is busy during session initialization.
+     *
+     * @param session the session object that carries out the transaction
+     */
+    void onCallBusy(in ISipSession session);
+
+    /**
+     * Called when an error occurs during session initialization and
+     * termination.
+     *
+     * @param session the session object that carries out the transaction
+     * @param errorClass name of the exception class
+     * @param errorMessage error message
+     */
+    void onError(in ISipSession session, String errorClass,
+            String errorMessage);
+
+    /**
+     * Called when an error occurs during session modification negotiation.
+     *
+     * @param session the session object that carries out the transaction
+     * @param errorClass name of the exception class
+     * @param errorMessage error message
+     */
+    void onCallChangeFailed(in ISipSession session, String errorClass,
+            String errorMessage);
+
+    /**
+     * Called when a registration request is sent.
+     *
+     * @param session the session object that carries out the transaction
+     */
+    void onRegistering(in ISipSession session);
+
+    /**
+     * Called when registration is successfully done.
+     *
+     * @param session the session object that carries out the transaction
+     * @param duration duration in second before the registration expires
+     */
+    void onRegistrationDone(in ISipSession session, int duration);
+
+    /**
+     * Called when the registration fails.
+     *
+     * @param session the session object that carries out the transaction
+     * @param errorClass name of the exception class
+     * @param errorMessage error message
+     */
+    void onRegistrationFailed(in ISipSession session, String errorClass,
+            String errorMessage);
+
+    /**
+     * Called when the registration gets timed out.
+     *
+     * @param session the session object that carries out the transaction
+     */
+    void onRegistrationTimeout(in ISipSession session);
+}
diff --git a/voip/java/android/net/sip/SdpSessionDescription.java b/voip/java/android/net/sip/SdpSessionDescription.java
new file mode 100644
index 0000000..0c29935
--- /dev/null
+++ b/voip/java/android/net/sip/SdpSessionDescription.java
@@ -0,0 +1,428 @@
+/*
+ * 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 android.net.sip;
+
+import gov.nist.javax.sdp.SessionDescriptionImpl;
+import gov.nist.javax.sdp.fields.AttributeField;
+import gov.nist.javax.sdp.fields.ConnectionField;
+import gov.nist.javax.sdp.fields.MediaField;
+import gov.nist.javax.sdp.fields.OriginField;
+import gov.nist.javax.sdp.fields.ProtoVersionField;
+import gov.nist.javax.sdp.fields.SessionNameField;
+import gov.nist.javax.sdp.fields.TimeField;
+import gov.nist.javax.sdp.parser.SDPAnnounceParser;
+
+import android.util.Log;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+import javax.sdp.Connection;
+import javax.sdp.MediaDescription;
+import javax.sdp.SdpException;
+
+/**
+ * A session description that follows SDP (Session Description Protocol).
+ * Refer to <a href="http://tools.ietf.org/html/rfc4566">RFC 4566</a>.
+ * @hide
+ */
+public class SdpSessionDescription extends SessionDescription {
+    private static final String TAG = "SDP";
+    private static final String AUDIO = "audio";
+    private static final String RTPMAP = "rtpmap";
+    private static final String PTIME = "ptime";
+    private static final String SENDONLY = "sendonly";
+    private static final String RECVONLY = "recvonly";
+    private static final String INACTIVE = "inactive";
+
+    private SessionDescriptionImpl mSessionDescription;
+
+    /**
+     * The audio codec information parsed from "rtpmap".
+     */
+    public static class AudioCodec {
+        public final int payloadType;
+        public final String name;
+        public final int sampleRate;
+        public final int sampleCount;
+
+        public AudioCodec(int payloadType, String name, int sampleRate,
+                int sampleCount) {
+            this.payloadType = payloadType;
+            this.name = name;
+            this.sampleRate = sampleRate;
+            this.sampleCount = sampleCount;
+        }
+    }
+
+    /**
+     * The builder class used to create an {@link SdpSessionDescription} object.
+     */
+    public static class Builder {
+        private SdpSessionDescription mSdp = new SdpSessionDescription();
+        private SessionDescriptionImpl mSessionDescription;
+
+        public Builder(String sessionName) throws SdpException {
+            mSessionDescription = new SessionDescriptionImpl();
+            mSdp.mSessionDescription = mSessionDescription;
+            try {
+                ProtoVersionField proto = new ProtoVersionField();
+                proto.setVersion(0);
+                mSessionDescription.addField(proto);
+
+                TimeField time = new TimeField();
+                time.setZero();
+                mSessionDescription.addField(time);
+
+                SessionNameField session = new SessionNameField();
+                session.setValue(sessionName);
+                mSessionDescription.addField(session);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+        }
+
+        public Builder setConnectionInfo(String networkType, String addressType,
+                String addr) throws SdpException {
+            try {
+                ConnectionField connection = new ConnectionField();
+                connection.setNetworkType(networkType);
+                connection.setAddressType(addressType);
+                connection.setAddress(addr);
+                mSessionDescription.addField(connection);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+            return this;
+        }
+
+        public Builder setOrigin(SipProfile user, long sessionId,
+                long sessionVersion, String networkType, String addressType,
+                String address) throws SdpException {
+            try {
+                OriginField origin = new OriginField();
+                origin.setUsername(user.getUserName());
+                origin.setSessionId(sessionId);
+                origin.setSessionVersion(sessionVersion);
+                origin.setAddressType(addressType);
+                origin.setNetworkType(networkType);
+                origin.setAddress(address);
+                mSessionDescription.addField(origin);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+            return this;
+        }
+
+        public Builder addMedia(String media, int port, int numPorts,
+                String transport, Integer... types) throws SdpException {
+            MediaField field = new MediaField();
+            Vector<Integer> typeVector = new Vector<Integer>();
+            Collections.addAll(typeVector, types);
+            try {
+                field.setMediaType(media);
+                field.setMediaPort(port);
+                field.setPortCount(numPorts);
+                field.setProtocol(transport);
+                field.setMediaFormats(typeVector);
+                mSessionDescription.addField(field);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+           return this;
+        }
+
+        public Builder addMediaAttribute(String type, String name, String value)
+                throws SdpException {
+            try {
+                MediaDescription md = mSdp.getMediaDescription(type);
+                if (md == null) {
+                    throw new SdpException("Should add media first!");
+                }
+                AttributeField attribute = new AttributeField();
+                attribute.setName(name);
+                attribute.setValueAllowNull(value);
+                mSessionDescription.addField(attribute);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+            return this;
+        }
+
+        public Builder addSessionAttribute(String name, String value)
+                throws SdpException {
+            try {
+                AttributeField attribute = new AttributeField();
+                attribute.setName(name);
+                attribute.setValueAllowNull(value);
+                mSessionDescription.addField(attribute);
+            } catch (Exception e) {
+                throwSdpException(e);
+            }
+            return this;
+        }
+
+        private void throwSdpException(Exception e) throws SdpException {
+            if (e instanceof SdpException) {
+                throw (SdpException) e;
+            } else {
+                throw new SdpException(e.toString(), e);
+            }
+        }
+
+        public SdpSessionDescription build() {
+            return mSdp;
+        }
+    }
+
+    private SdpSessionDescription() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param sdpString an SDP session description to parse
+     */
+    public SdpSessionDescription(String sdpString) throws SdpException {
+        try {
+            mSessionDescription = new SDPAnnounceParser(sdpString).parse();
+        } catch (ParseException e) {
+            throw new SdpException(e.toString(), e);
+        }
+        verify();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param content a raw SDP session description to parse
+     */
+    public SdpSessionDescription(byte[] content) throws SdpException {
+        this(new String(content));
+    }
+
+    private void verify() throws SdpException {
+        // make sure the syntax is correct over the fields we're interested in
+        Vector<MediaDescription> descriptions = (Vector<MediaDescription>)
+                mSessionDescription.getMediaDescriptions(false);
+        for (MediaDescription md : descriptions) {
+            md.getMedia().getMediaPort();
+            Connection connection = md.getConnection();
+            if (connection != null) connection.getAddress();
+            md.getMedia().getFormats();
+        }
+        Connection connection = mSessionDescription.getConnection();
+        if (connection != null) connection.getAddress();
+    }
+
+    /**
+     * Gets the connection address of the media.
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return the media connection address of the peer
+     */
+    public String getPeerMediaAddress(String type) {
+        try {
+            MediaDescription md = getMediaDescription(type);
+            Connection connection = md.getConnection();
+            if (connection == null) {
+                connection = mSessionDescription.getConnection();
+            }
+            return ((connection == null) ? null : connection.getAddress());
+        } catch (SdpException e) {
+            // should not occur
+            return null;
+        }
+    }
+
+    /**
+     * Gets the connection port number of the media.
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return the media connection port number of the peer
+     */
+    public int getPeerMediaPort(String type) {
+        try {
+            MediaDescription md = getMediaDescription(type);
+            return md.getMedia().getMediaPort();
+        } catch (SdpException e) {
+            // should not occur
+            return -1;
+        }
+    }
+
+    private boolean containsAttribute(String type, String name) {
+        if (name == null) return false;
+        MediaDescription md = getMediaDescription(type);
+        Vector<AttributeField> v = (Vector<AttributeField>)
+                md.getAttributeFields();
+        for (AttributeField field : v) {
+            if (name.equals(field.getAttribute().getName())) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the media is "sendonly".
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return true if the media is "sendonly"
+     */
+    public boolean isSendOnly(String type) {
+        boolean answer = containsAttribute(type, SENDONLY);
+        Log.d(TAG, "   sendonly? " + answer);
+        return answer;
+    }
+
+    /**
+     * Checks if the media is "recvonly".
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return true if the media is "recvonly"
+     */
+    public boolean isReceiveOnly(String type) {
+        boolean answer = containsAttribute(type, RECVONLY);
+        Log.d(TAG, "   recvonly? " + answer);
+        return answer;
+    }
+
+    /**
+     * Checks if the media is in sending; i.e., not "recvonly" and not
+     * "inactive".
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return true if the media is sending
+     */
+    public boolean isSending(String type) {
+        boolean answer = !containsAttribute(type, RECVONLY)
+                && !containsAttribute(type, INACTIVE);
+
+        Log.d(TAG, "   sending? " + answer);
+        return answer;
+    }
+
+    /**
+     * Checks if the media is in receiving; i.e., not "sendonly" and not
+     * "inactive".
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return true if the media is receiving
+     */
+    public boolean isReceiving(String type) {
+        boolean answer = !containsAttribute(type, SENDONLY)
+                && !containsAttribute(type, INACTIVE);
+        Log.d(TAG, "   receiving? " + answer);
+        return answer;
+    }
+
+    private AudioCodec parseAudioCodec(String rtpmap, int ptime) {
+        String[] ss = rtpmap.split(" ");
+        int payloadType = Integer.parseInt(ss[0]);
+
+        ss = ss[1].split("/");
+        String name = ss[0];
+        int sampleRate = Integer.parseInt(ss[1]);
+        int channelCount = 1;
+        if (ss.length > 2) channelCount = Integer.parseInt(ss[2]);
+        int sampleCount = sampleRate / (1000 / ptime) * channelCount;
+        return new AudioCodec(payloadType, name, sampleRate, sampleCount);
+    }
+
+    /**
+     * Gets the list of audio codecs in this session description.
+     *
+     * @return the list of audio codecs in this session description
+     */
+    public List<AudioCodec> getAudioCodecs() {
+        MediaDescription md = getMediaDescription(AUDIO);
+        if (md == null) return new ArrayList<AudioCodec>();
+
+        // FIXME: what happens if ptime is missing
+        int ptime = 20;
+        try {
+            String value = md.getAttribute(PTIME);
+            if (value != null) ptime = Integer.parseInt(value);
+        } catch (Throwable t) {
+            Log.w(TAG, "getCodecs(): ignored: " + t);
+        }
+
+        List<AudioCodec> codecs = new ArrayList<AudioCodec>();
+        Vector<AttributeField> v = (Vector<AttributeField>)
+                md.getAttributeFields();
+        for (AttributeField field : v) {
+            try {
+                if (RTPMAP.equals(field.getName())) {
+                    AudioCodec codec = parseAudioCodec(field.getValue(), ptime);
+                    if (codec != null) codecs.add(codec);
+                }
+            } catch (Throwable t) {
+                Log.w(TAG, "getCodecs(): ignored: " + t);
+            }
+        }
+        return codecs;
+    }
+
+    /**
+     * Gets the media description of the specified type.
+     *
+     * @param type the media type; e.g., "AUDIO"
+     * @return the media description of the specified type
+     */
+    public MediaDescription getMediaDescription(String type) {
+        MediaDescription[] all = getMediaDescriptions();
+        if ((all == null) || (all.length == 0)) return null;
+        for (MediaDescription md : all) {
+            String t = md.getMedia().getMedia();
+            if (t.equalsIgnoreCase(type)) return md;
+        }
+        return null;
+    }
+
+    /**
+     * Gets all the media descriptions in this session description.
+     *
+     * @return all the media descriptions in this session description
+     */
+    public MediaDescription[] getMediaDescriptions() {
+        try {
+            Vector<MediaDescription> descriptions = (Vector<MediaDescription>)
+                    mSessionDescription.getMediaDescriptions(false);
+            MediaDescription[] all = new MediaDescription[descriptions.size()];
+            return descriptions.toArray(all);
+        } catch (SdpException e) {
+            Log.e(TAG, "getMediaDescriptions", e);
+        }
+        return null;
+    }
+
+    @Override
+    public String getType() {
+        return "sdp";
+    }
+
+    @Override
+    public byte[] getContent() {
+          return mSessionDescription.toString().getBytes();
+    }
+
+    @Override
+    public String toString() {
+        return mSessionDescription.toString();
+    }
+}
diff --git a/voip/java/android/net/sip/SessionDescription.aidl b/voip/java/android/net/sip/SessionDescription.aidl
new file mode 100644
index 0000000..a120d16
--- /dev/null
+++ b/voip/java/android/net/sip/SessionDescription.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.net.sip;
+
+parcelable SessionDescription;
diff --git a/voip/java/android/net/sip/SessionDescription.java b/voip/java/android/net/sip/SessionDescription.java
new file mode 100644
index 0000000..d476f0b
--- /dev/null
+++ b/voip/java/android/net/sip/SessionDescription.java
@@ -0,0 +1,83 @@
+/*
+ * 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 android.net.sip;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Abstract class of a session description.
+ * @hide
+ */
+public abstract class SessionDescription implements Parcelable {
+    /** @hide */
+    public static final Parcelable.Creator<SessionDescription> CREATOR =
+            new Parcelable.Creator<SessionDescription>() {
+                public SessionDescription createFromParcel(Parcel in) {
+                    return new SessionDescriptionImpl(in);
+                }
+
+                public SessionDescription[] newArray(int size) {
+                    return new SessionDescriptionImpl[size];
+                }
+            };
+
+    /**
+     * Gets the type of the session description; e.g., "SDP".
+     *
+     * @return the session description type
+     */
+    public abstract String getType();
+
+    /**
+     * Gets the raw content of the session description.
+     *
+     * @return the content of the session description
+     */
+    public abstract byte[] getContent();
+
+    /** @hide */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(getType());
+        out.writeByteArray(getContent());
+    }
+
+    /** @hide */
+    public int describeContents() {
+        return 0;
+    }
+
+    private static class SessionDescriptionImpl extends SessionDescription {
+        private String mType;
+        private byte[] mContent;
+
+        SessionDescriptionImpl(Parcel in) {
+            mType = in.readString();
+            mContent = in.createByteArray();
+        }
+
+        @Override
+        public String getType() {
+            return mType;
+        }
+
+        @Override
+        public byte[] getContent() {
+            return mContent;
+        }
+    }
+}
diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java
new file mode 100644
index 0000000..abdc9d7
--- /dev/null
+++ b/voip/java/android/net/sip/SipAudioCall.java
@@ -0,0 +1,306 @@
+/*
+ * 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 android.net.sip;
+
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.os.Message;
+
+import javax.sip.SipException;
+
+/**
+ * Interface for making audio calls over SIP.
+ * @hide
+ */
+public interface SipAudioCall {
+    /** Listener class for all event callbacks. */
+    public interface Listener {
+        /**
+         * Called when the call object is ready to make another call.
+         *
+         * @param call the call object that is ready to make another call
+         */
+        void onReadyToCall(SipAudioCall call);
+
+        /**
+         * Called when a request is sent out to initiate a new call.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onCalling(SipAudioCall call);
+
+        /**
+         * Called when a new call comes in.
+         *
+         * @param call the call object that carries out the audio call
+         * @param caller the SIP profile of the caller
+         */
+        void onRinging(SipAudioCall call, SipProfile caller);
+
+        /**
+         * Called when a RINGING response is received for the INVITE request sent
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onRingingBack(SipAudioCall call);
+
+        /**
+         * Called when the session is established.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onCallEstablished(SipAudioCall call);
+
+        /**
+         * Called when the session is terminated.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onCallEnded(SipAudioCall call);
+
+        /**
+         * Called when the peer is busy during session initialization.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onCallBusy(SipAudioCall call);
+
+        /**
+         * Called when the call is on hold.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        void onCallHeld(SipAudioCall call);
+
+        /**
+         * Called when an error occurs.
+         *
+         * @param call the call object that carries out the audio call
+         * @param errorMessage error message
+         */
+        void onError(SipAudioCall call, String errorMessage);
+    }
+
+    /**
+     * The adapter class for {@link SipAudioCall#Listener}. The default
+     * implementation of all callback methods is no-op.
+     */
+    public class Adapter implements Listener {
+        protected void onChanged(SipAudioCall call) {
+        }
+        public void onReadyToCall(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onCalling(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onRinging(SipAudioCall call, SipProfile caller) {
+            onChanged(call);
+        }
+        public void onRingingBack(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onCallEstablished(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onCallEnded(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onCallBusy(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onCallHeld(SipAudioCall call) {
+            onChanged(call);
+        }
+        public void onError(SipAudioCall call, String errorMessage) {
+            onChanged(call);
+        }
+    }
+
+    /**
+     * Sets the listener to listen to the audio call events. The method calls
+     * {@link #setListener(Listener, false)}.
+     *
+     * @param listener to listen to the audio call events of this object
+     * @see #setListener(Listener, boolean)
+     */
+    void setListener(Listener listener);
+
+    /**
+     * Sets the listener to listen to the audio call events. A
+     * {@link SipAudioCall} can only hold one listener at a time. Subsequent
+     * calls to this method override the previous listener.
+     *
+     * @param listener to listen to the audio call events of this object
+     * @param callbackImmediately set to true if the caller wants to be called
+     *      back immediately on the current state
+     */
+    void setListener(Listener listener, boolean callbackImmediately);
+
+    /**
+     * Closes this object. The object is not usable after being closed.
+     */
+    void close();
+
+    /**
+     * Initiates an audio call to the specified profile.
+     *
+     * @param callee the SIP profile to make the call to
+     * @param sipManager the {@link SipManager} object to help make call with
+     */
+    void makeCall(SipProfile callee, SipManager sipManager) throws SipException;
+
+    /**
+     * Attaches an incoming call to this call object.
+     *
+     * @param session the session that receives the incoming call
+     * @param sdp the session description of the incoming call
+     */
+    void attachCall(ISipSession session, SdpSessionDescription sdp)
+            throws SipException;
+
+    /** Ends a call. */
+    void endCall() throws SipException;
+
+    /**
+     * Puts a call on hold.  When succeeds,
+     * {@link #Listener#onCallHeld(SipAudioCall)} is called.
+     */
+    void holdCall() throws SipException;
+
+    /** Answers a call. */
+    void answerCall() throws SipException;
+
+    /**
+     * Continues a call that's on hold. When succeeds,
+     * {@link #Listener#onCallEstablished(SipAudioCall)} is called.
+     */
+    void continueCall() throws SipException;
+
+    /** Puts the device to in-call mode. */
+    void setInCallMode();
+
+    /** Puts the device to speaker mode. */
+    void setSpeakerMode();
+
+    /** Toggles mute. */
+    void toggleMute();
+
+    /**
+     * Checks if the call is on hold.
+     *
+     * @return true if the call is on hold
+     */
+    boolean isOnHold();
+
+    /**
+     * Checks if the call is muted.
+     *
+     * @return true if the call is muted
+     */
+    boolean isMuted();
+
+    /**
+     * Sends a DTMF code.
+     *
+     * @param code the DTMF code to send
+     */
+    void sendDtmf(int code);
+
+    /**
+     * Sends a DTMF code.
+     *
+     * @param code the DTMF code to send
+     * @param result the result message to send when done
+     */
+    void sendDtmf(int code, Message result);
+
+    /**
+     * Gets the {@link AudioStream} object used in this call. The object
+     * represents the RTP stream that carries the audio data to and from the
+     * peer. The object may not be created before the call is established. And
+     * it is undefined after the call ends or the {@link #close} method is
+     * called.
+     *
+     * @return the {@link AudioStream} object or null if the RTP stream has not
+     *      yet been set up
+     */
+    AudioStream getAudioStream();
+
+    /**
+     * Gets the {@link AudioGroup} object which the {@link AudioStream} object
+     * joins. The group object may not exist before the call is established.
+     * Also, the {@code AudioStream} may change its group during a call (e.g.,
+     * after the call is held/un-held). Finally, the {@code AudioGroup} object
+     * returned by this method is undefined after the call ends or the
+     * {@link #close} method is called.
+     *
+     * @return the {@link AudioGroup} object or null if the RTP stream has not
+     *      yet been set up
+     * @see #getAudioStream
+     */
+    AudioGroup getAudioGroup();
+
+    /**
+     * Checks if the call is established.
+     *
+     * @return true if the call is established
+     */
+    boolean isInCall();
+
+    /**
+     * Gets the local SIP profile.
+     *
+     * @return the local SIP profile
+     */
+    SipProfile getLocalProfile();
+
+    /**
+     * Gets the peer's SIP profile.
+     *
+     * @return the peer's SIP profile
+     */
+    SipProfile getPeerProfile();
+
+    /**
+     * Gets the state of the {@link ISipSession} that carries this call.
+     *
+     * @return the session state
+     */
+    SipSessionState getState();
+
+    /**
+     * Gets the {@link ISipSession} that carries this call.
+     *
+     * @return the session object that carries this call
+     */
+    ISipSession getSipSession();
+
+    /**
+     * Enables/disables the ring-back tone.
+     *
+     * @param enabled true to enable; false to disable
+     */
+    void setRingbackToneEnabled(boolean enabled);
+
+    /**
+     * Enables/disables the ring tone.
+     *
+     * @param enabled true to enable; false to disable
+     */
+    void setRingtoneEnabled(boolean enabled);
+}
diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java
new file mode 100644
index 0000000..57e0bd2
--- /dev/null
+++ b/voip/java/android/net/sip/SipAudioCallImpl.java
@@ -0,0 +1,701 @@
+/*
+ * 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 android.net.sip;
+
+import gov.nist.javax.sdp.fields.SDPKeywords;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.net.sip.ISipSession;
+import android.net.sip.SdpSessionDescription;
+import android.net.sip.SessionDescription;
+import android.net.sip.SipAudioCall;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSessionAdapter;
+import android.net.sip.SipSessionState;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.sdp.SdpException;
+import javax.sip.SipException;
+
+/**
+ * Class that handles an audio call over SIP. 
+ */
+/** @hide */
+public class SipAudioCallImpl extends SipSessionAdapter
+        implements SipAudioCall {
+    private static final String TAG = SipAudioCallImpl.class.getSimpleName();
+    private static final boolean RELEASE_SOCKET = true;
+    private static final boolean DONT_RELEASE_SOCKET = false;
+    private static final String AUDIO = "audio";
+    private static final int DTMF = 101;
+
+    private Context mContext;
+    private SipProfile mLocalProfile;
+    private SipAudioCall.Listener mListener;
+    private ISipSession mSipSession;
+    private SdpSessionDescription mPeerSd;
+
+    private AudioStream mRtpSession;
+    private SdpSessionDescription.AudioCodec mCodec;
+    private long mSessionId = -1L; // SDP session ID
+    private boolean mInCall = false;
+    private boolean mMuted = false;
+    private boolean mHold = false;
+
+    private boolean mRingbackToneEnabled = true;
+    private boolean mRingtoneEnabled = true;
+    private Ringtone mRingtone;
+    private ToneGenerator mRingbackTone;
+
+    private SipProfile mPendingCallRequest;
+
+    public SipAudioCallImpl(Context context, SipProfile localProfile) {
+        mContext = context;
+        mLocalProfile = localProfile;
+    }
+
+    public void setListener(SipAudioCall.Listener listener) {
+        setListener(listener, false);
+    }
+
+    public void setListener(SipAudioCall.Listener listener,
+            boolean callbackImmediately) {
+        mListener = listener;
+        if ((listener == null) || !callbackImmediately) return;
+        try {
+            SipSessionState state = getState();
+            switch (state) {
+            case READY_TO_CALL:
+                listener.onReadyToCall(this);
+                break;
+            case INCOMING_CALL:
+                listener.onRinging(this, getPeerProfile(mSipSession));
+                startRinging();
+                break;
+            case OUTGOING_CALL:
+                listener.onCalling(this);
+                break;
+            default:
+                listener.onError(this, "wrong state to attach call: " + state);
+            }
+        } catch (Throwable t) {
+            Log.e(TAG, "setListener()", t);
+        }
+    }
+
+    public synchronized boolean isInCall() {
+        return mInCall;
+    }
+
+    public synchronized boolean isOnHold() {
+        return mHold;
+    }
+
+    public void close() {
+        close(true);
+    }
+
+    private synchronized void close(boolean closeRtp) {
+        if (closeRtp) stopCall(RELEASE_SOCKET);
+        stopRingbackTone();
+        stopRinging();
+        mSipSession = null;
+        mInCall = false;
+        mHold = false;
+        mSessionId = -1L;
+    }
+
+    public synchronized SipProfile getLocalProfile() {
+        return mLocalProfile;
+    }
+
+    public synchronized SipProfile getPeerProfile() {
+        try {
+            return (mSipSession == null) ? null : mSipSession.getPeerProfile();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    public synchronized SipSessionState getState() {
+        if (mSipSession == null) return SipSessionState.READY_TO_CALL;
+        try {
+            return Enum.valueOf(SipSessionState.class, mSipSession.getState());
+        } catch (RemoteException e) {
+            return SipSessionState.REMOTE_ERROR;
+        }
+    }
+
+
+    public synchronized ISipSession getSipSession() {
+        return mSipSession;
+    }
+
+    @Override
+    public void onCalling(ISipSession session) {
+        Log.d(TAG, "calling... " + session);
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onCalling(SipAudioCallImpl.this);
+            } catch (Throwable t) {
+                Log.e(TAG, "onCalling()", t);
+            }
+        }
+    }
+
+    @Override
+    public void onRingingBack(ISipSession session) {
+        Log.d(TAG, "sip call ringing back: " + session);
+        if (!mInCall) startRingbackTone();
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onRingingBack(SipAudioCallImpl.this);
+            } catch (Throwable t) {
+                Log.e(TAG, "onRingingBack()", t);
+            }
+        }
+    }
+
+    @Override
+    public synchronized void onRinging(ISipSession session,
+            SipProfile peerProfile, byte[] sessionDescription) {
+        try {
+            if ((mSipSession == null) || !mInCall
+                    || !session.getCallId().equals(mSipSession.getCallId())) {
+                // should not happen
+                session.endCall();
+                return;
+            }
+
+            // session changing request
+            try {
+                mPeerSd = new SdpSessionDescription(sessionDescription);
+                answerCall();
+            } catch (Throwable e) {
+                Log.e(TAG, "onRinging()", e);
+                session.endCall();
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "onRinging()", e);
+        }
+    }
+
+    private synchronized void establishCall(byte[] sessionDescription) {
+        stopRingbackTone();
+        stopRinging();
+        try {
+            SdpSessionDescription sd =
+                    new SdpSessionDescription(sessionDescription);
+            Log.d(TAG, "sip call established: " + sd);
+            startCall(sd);
+            mInCall = true;
+        } catch (SdpException e) {
+            Log.e(TAG, "createSessionDescription()", e);
+        }
+    }
+
+    @Override
+    public void onCallEstablished(ISipSession session,
+            byte[] sessionDescription) {
+        establishCall(sessionDescription);
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                if (mHold) {
+                    listener.onCallHeld(SipAudioCallImpl.this);
+                } else {
+                    listener.onCallEstablished(SipAudioCallImpl.this);
+                }
+            } catch (Throwable t) {
+                Log.e(TAG, "onCallEstablished()", t);
+            }
+        }
+    }
+
+    @Override
+    public void onCallEnded(ISipSession session) {
+        Log.d(TAG, "sip call ended: " + session);
+        close();
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onCallEnded(SipAudioCallImpl.this);
+            } catch (Throwable t) {
+                Log.e(TAG, "onCallEnded()", t);
+            }
+        }
+    }
+
+    @Override
+    public void onCallBusy(ISipSession session) {
+        Log.d(TAG, "sip call busy: " + session);
+        close(false);
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onCallBusy(SipAudioCallImpl.this);
+            } catch (Throwable t) {
+                Log.e(TAG, "onCallBusy()", t);
+            }
+        }
+    }
+
+    @Override
+    public void onCallChangeFailed(ISipSession session,
+            String className, String message) {
+        Log.d(TAG, "sip call change failed: " + message);
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onError(SipAudioCallImpl.this,
+                        className + ": " + message);
+            } catch (Throwable t) {
+                Log.e(TAG, "onCallBusy()", t);
+            }
+        }
+    }
+
+    @Override
+    public void onError(ISipSession session, String className,
+            String message) {
+        Log.d(TAG, "sip session error: " + className + ": " + message);
+        synchronized (this) {
+            if (!isInCall()) close(true);
+        }
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onError(SipAudioCallImpl.this,
+                        className + ": " + message);
+            } catch (Throwable t) {
+                Log.e(TAG, "onError()", t);
+            }
+        }
+    }
+
+    public synchronized void attachCall(ISipSession session,
+            SdpSessionDescription sdp) throws SipException {
+        mSipSession = session;
+        mPeerSd = sdp;
+        try {
+            session.setListener(this);
+        } catch (Throwable e) {
+            Log.e(TAG, "attachCall()", e);
+            throwSipException(e);
+        }
+    }
+
+    public synchronized void makeCall(SipProfile peerProfile,
+            SipManager sipManager) throws SipException {
+        try {
+            mSipSession = sipManager.createSipSession(mLocalProfile, this);
+            if (mSipSession == null) {
+                throw new SipException(
+                        "Failed to create SipSession; network available?");
+            }
+            mSipSession.makeCall(peerProfile, createOfferSessionDescription());
+        } catch (Throwable e) {
+            if (e instanceof SipException) {
+                throw (SipException) e;
+            } else {
+                throwSipException(e);
+            }
+        }
+    }
+
+    public synchronized void endCall() throws SipException {
+        try {
+            stopRinging();
+            if (mSipSession != null) mSipSession.endCall();
+            stopCall(true);
+        } catch (Throwable e) {
+            throwSipException(e);
+        }
+    }
+
+    public synchronized void holdCall() throws SipException {
+        if (mHold) return;
+        try {
+            mSipSession.changeCall(createHoldSessionDescription());
+            mHold = true;
+        } catch (Throwable e) {
+            throwSipException(e);
+        }
+
+        AudioGroup audioGroup = getAudioGroup();
+        if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+    }
+
+    public synchronized void answerCall() throws SipException {
+        try {
+            stopRinging();
+            mSipSession.answerCall(createAnswerSessionDescription());
+        } catch (Throwable e) {
+            Log.e(TAG, "answerCall()", e);
+            throwSipException(e);
+        }
+    }
+
+    public synchronized void continueCall() throws SipException {
+        if (!mHold) return;
+        try {
+            mHold = false;
+            mSipSession.changeCall(createContinueSessionDescription());
+        } catch (Throwable e) {
+            throwSipException(e);
+        }
+
+        AudioGroup audioGroup = getAudioGroup();
+        if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_NORMAL);
+    }
+
+    private SessionDescription createOfferSessionDescription() {
+        AudioCodec[] codecs = AudioCodec.getSystemSupportedCodecs();
+        return createSdpBuilder(true, convert(codecs)).build();
+    }
+
+    private SessionDescription createAnswerSessionDescription() {
+        try {
+            // choose an acceptable media from mPeerSd to answer
+            SdpSessionDescription.AudioCodec codec = getCodec(mPeerSd);
+            SdpSessionDescription.Builder sdpBuilder =
+                    createSdpBuilder(false, codec);
+            if (mPeerSd.isSendOnly(AUDIO)) {
+                sdpBuilder.addMediaAttribute(AUDIO, "recvonly", (String) null);
+            } else if (mPeerSd.isReceiveOnly(AUDIO)) {
+                sdpBuilder.addMediaAttribute(AUDIO, "sendonly", (String) null);
+            }
+            return sdpBuilder.build();
+        } catch (SdpException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private SessionDescription createHoldSessionDescription() {
+        try {
+            return createSdpBuilder(false, mCodec)
+                    .addMediaAttribute(AUDIO, "sendonly", (String) null)
+                    .build();
+        } catch (SdpException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private SessionDescription createContinueSessionDescription() {
+        return createSdpBuilder(true, mCodec).build();
+    }
+
+    private String getMediaDescription(SdpSessionDescription.AudioCodec codec) {
+        return String.format("%d %s/%d", codec.payloadType, codec.name,
+                codec.sampleRate);
+    }
+
+    private long getSessionId() {
+        if (mSessionId < 0) {
+            mSessionId = System.currentTimeMillis();
+        }
+        return mSessionId;
+    }
+
+    private SdpSessionDescription.Builder createSdpBuilder(
+            boolean addTelephoneEvent,
+            SdpSessionDescription.AudioCodec... codecs) {
+        String localIp = getLocalIp();
+        SdpSessionDescription.Builder sdpBuilder;
+        try {
+            long sessionVersion = System.currentTimeMillis();
+            sdpBuilder = new SdpSessionDescription.Builder("SIP Call")
+                    .setOrigin(mLocalProfile, getSessionId(), sessionVersion,
+                            SDPKeywords.IN, SDPKeywords.IPV4, localIp)
+                    .setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4,
+                            localIp);
+            List<Integer> codecIds = new ArrayList<Integer>();
+            for (SdpSessionDescription.AudioCodec codec : codecs) {
+                codecIds.add(codec.payloadType);
+            }
+            if (addTelephoneEvent) codecIds.add(DTMF);
+            sdpBuilder.addMedia(AUDIO, getLocalMediaPort(), 1, "RTP/AVP",
+                    codecIds.toArray(new Integer[codecIds.size()]));
+            for (SdpSessionDescription.AudioCodec codec : codecs) {
+                sdpBuilder.addMediaAttribute(AUDIO, "rtpmap",
+                        getMediaDescription(codec));
+            }
+            if (addTelephoneEvent) {
+                sdpBuilder.addMediaAttribute(AUDIO, "rtpmap",
+                        DTMF + " telephone-event/8000");
+            }
+            // FIXME: deal with vbr codec
+            sdpBuilder.addMediaAttribute(AUDIO, "ptime", "20");
+        } catch (SdpException e) {
+            throw new RuntimeException(e);
+        }
+        return sdpBuilder;
+    }
+
+    public synchronized void toggleMute() {
+        AudioGroup audioGroup = getAudioGroup();
+        if (audioGroup != null) {
+            audioGroup.setMode(
+                    mMuted ? AudioGroup.MODE_NORMAL : AudioGroup.MODE_MUTED);
+            mMuted = !mMuted;
+        }
+    }
+
+    public synchronized boolean isMuted() {
+        return mMuted;
+    }
+
+    public synchronized void setInCallMode() {
+        ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                .setSpeakerphoneOn(false);
+        ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                .setMode(AudioManager.MODE_NORMAL);
+    }
+
+    public synchronized void setSpeakerMode() {
+        ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                .setSpeakerphoneOn(true);
+    }
+
+    public void sendDtmf(int code) {
+        sendDtmf(code, null);
+    }
+
+    public synchronized void sendDtmf(int code, Message result) {
+        AudioGroup audioGroup = getAudioGroup();
+        if ((audioGroup != null) && (mSipSession != null)
+                && (SipSessionState.IN_CALL == getState())) {
+            Log.v(TAG, "send DTMF: " + code);
+            audioGroup.sendDtmf(code);
+        }
+        if (result != null) result.sendToTarget();
+    }
+
+    public synchronized AudioStream getAudioStream() {
+        return mRtpSession;
+    }
+
+    public synchronized AudioGroup getAudioGroup() {
+        return ((mRtpSession == null) ? null : mRtpSession.getAudioGroup());
+    }
+
+    private SdpSessionDescription.AudioCodec getCodec(SdpSessionDescription sd) {
+        HashMap<String, AudioCodec> acceptableCodecs =
+                new HashMap<String, AudioCodec>();
+        for (AudioCodec codec : AudioCodec.getSystemSupportedCodecs()) {
+            acceptableCodecs.put(codec.name, codec);
+        }
+        for (SdpSessionDescription.AudioCodec codec : sd.getAudioCodecs()) {
+            AudioCodec matchedCodec = acceptableCodecs.get(codec.name);
+            if (matchedCodec != null) return codec;
+        }
+        Log.w(TAG, "no common codec is found, use PCM/0");
+        return convert(AudioCodec.ULAW);
+    }
+
+    private AudioCodec convert(SdpSessionDescription.AudioCodec codec) {
+        AudioCodec c = AudioCodec.getSystemSupportedCodec(codec.name);
+        return ((c == null) ? AudioCodec.ULAW : c);
+    }
+
+    private SdpSessionDescription.AudioCodec convert(AudioCodec codec) {
+        return new SdpSessionDescription.AudioCodec(codec.defaultType,
+                codec.name, codec.sampleRate, codec.sampleCount);
+    }
+
+    private SdpSessionDescription.AudioCodec[] convert(AudioCodec[] codecs) {
+        SdpSessionDescription.AudioCodec[] copies =
+                new SdpSessionDescription.AudioCodec[codecs.length];
+        for (int i = 0, len = codecs.length; i < len; i++) {
+            copies[i] = convert(codecs[i]);
+        }
+        return copies;
+    }
+
+    private void startCall(SdpSessionDescription peerSd) {
+        stopCall(DONT_RELEASE_SOCKET);
+
+        mPeerSd = peerSd;
+        String peerMediaAddress = peerSd.getPeerMediaAddress(AUDIO);
+        // TODO: handle multiple media fields
+        int peerMediaPort = peerSd.getPeerMediaPort(AUDIO);
+        Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort);
+
+        int localPort = getLocalMediaPort();
+        int sampleRate = 8000;
+        int frameSize = sampleRate / 50; // 160
+        try {
+            // TODO: get sample rate from sdp
+            mCodec = getCodec(peerSd);
+
+            AudioStream audioStream = mRtpSession;
+            audioStream.associate(InetAddress.getByName(peerMediaAddress),
+                    peerMediaPort);
+            audioStream.setCodec(convert(mCodec), mCodec.payloadType);
+            audioStream.setDtmfType(DTMF);
+            Log.d(TAG, "start media: localPort=" + localPort + ", peer="
+                    + peerMediaAddress + ":" + peerMediaPort);
+
+            audioStream.setMode(RtpStream.MODE_NORMAL);
+            if (!mHold) {
+                // FIXME: won't work if peer is not sending nor receiving
+                if (!peerSd.isSending(AUDIO)) {
+                    Log.d(TAG, "   not receiving");
+                    audioStream.setMode(RtpStream.MODE_SEND_ONLY);
+                }
+                if (!peerSd.isReceiving(AUDIO)) {
+                    Log.d(TAG, "   not sending");
+                    audioStream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+                }
+            }
+            setInCallMode();
+
+            AudioGroup audioGroup = new AudioGroup();
+            audioStream.join(audioGroup);
+            if (mHold) {
+                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+            } else if (mMuted) {
+                audioGroup.setMode(AudioGroup.MODE_MUTED);
+            } else {
+                audioGroup.setMode(AudioGroup.MODE_NORMAL);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "call()", e);
+        }
+    }
+
+    private void stopCall(boolean releaseSocket) {
+        Log.d(TAG, "stop audiocall");
+        if (mRtpSession != null) {
+            mRtpSession.join(null);
+
+            if (releaseSocket) {
+                mRtpSession.release();
+                mRtpSession = null;
+            }
+        }
+        setInCallMode();
+    }
+
+    private int getLocalMediaPort() {
+        if (mRtpSession != null) return mRtpSession.getLocalPort();
+        try {
+            AudioStream s = mRtpSession =
+                    new AudioStream(InetAddress.getByName(getLocalIp()));
+            return s.getLocalPort();
+        } catch (IOException e) {
+            Log.w(TAG, "getLocalMediaPort(): " + e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private String getLocalIp() {
+        try {
+            return mSipSession.getLocalIp();
+        } catch (RemoteException e) {
+            // FIXME
+            return "127.0.0.1";
+        }
+    }
+
+    public synchronized void setRingbackToneEnabled(boolean enabled) {
+        mRingbackToneEnabled = enabled;
+    }
+
+    public synchronized void setRingtoneEnabled(boolean enabled) {
+        mRingtoneEnabled = enabled;
+    }
+
+    private void startRingbackTone() {
+        if (!mRingbackToneEnabled) return;
+        if (mRingbackTone == null) {
+            // The volume relative to other sounds in the stream
+            int toneVolume = 80;
+            mRingbackTone = new ToneGenerator(
+                    AudioManager.STREAM_VOICE_CALL, toneVolume);
+        }
+        mRingbackTone.startTone(ToneGenerator.TONE_CDMA_LOW_PBX_L);
+    }
+
+    private void stopRingbackTone() {
+        if (mRingbackTone != null) {
+            mRingbackTone.stopTone();
+            mRingbackTone.release();
+            mRingbackTone = null;
+        }
+    }
+
+    private void startRinging() {
+        if (!mRingtoneEnabled) return;
+        ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE))
+                .vibrate(new long[] {0, 1000, 1000}, 1);
+        AudioManager am = (AudioManager)
+                mContext.getSystemService(Context.AUDIO_SERVICE);
+        if (am.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+            String ringtoneUri =
+                    Settings.System.DEFAULT_RINGTONE_URI.toString();
+            mRingtone = RingtoneManager.getRingtone(mContext,
+                    Uri.parse(ringtoneUri));
+            mRingtone.play();
+        }
+    }
+
+    private void stopRinging() {
+        ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE))
+                .cancel();
+        if (mRingtone != null) mRingtone.stop();
+    }
+
+    private void throwSipException(Throwable throwable) throws SipException {
+        if (throwable instanceof SipException) {
+            throw (SipException) throwable;
+        } else {
+            throw new SipException("", throwable);
+        }
+    }
+
+    private SipProfile getPeerProfile(ISipSession session) {
+        try {
+            return session.getPeerProfile();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+}
diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java
new file mode 100644
index 0000000..f28b41c
--- /dev/null
+++ b/voip/java/android/net/sip/SipManager.java
@@ -0,0 +1,499 @@
+/*
+ * 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 android.net.sip;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import java.text.ParseException;
+import javax.sip.SipException;
+
+/**
+ * The class provides API for various SIP related tasks. Specifically, the API
+ * allows the application to:
+ * <ul>
+ * <li>register a {@link SipProfile} to have the background SIP service listen
+ *      to incoming calls and broadcast them with registered command string. See
+ *      {@link #open(SipProfile, String, SipRegistrationListener)},
+ *      {@link #open(SipProfile)}, {@link #close(String)},
+ *      {@link #isOpened(String)} and {@link isRegistered(String)}. It also
+ *      facilitates handling of the incoming call broadcast intent. See
+ *      {@link #isIncomingCallIntent(Intent)}, {@link #getCallId(Intent)},
+ *      {@link #getOfferSessionDescription(Intent)} and
+ *      {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.</li>
+ * <li>make/take SIP-based audio calls. See
+ *      {@link #makeAudioCall(Context, SipProfile, SipProfile, SipAudioCall.Listener)}
+ *      and {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener}.</li>
+ * <li>register/unregister with a SIP service provider. See
+ *      {@link #register(SipProfile, int, ISipSessionListener)} and
+ *      {@link #unregister(SipProfile, ISipSessionListener)}.</li>
+ * <li>process SIP events directly with a {@link ISipSession} created by
+ *      {@link createSipSession(SipProfile, ISipSessionListener)}.</li>
+ * </ul>
+ * @hide
+ */
+public class SipManager {
+    /** @hide */
+    public static final String SIP_INCOMING_CALL_ACTION =
+            "com.android.phone.SIP_INCOMING_CALL";
+    /** @hide */
+    public static final String SIP_ADD_PHONE_ACTION =
+            "com.android.phone.SIP_ADD_PHONE";
+    /** @hide */
+    public static final String SIP_REMOVE_PHONE_ACTION =
+            "com.android.phone.SIP_REMOVE_PHONE";
+    /** @hide */
+    public static final String LOCAL_URI_KEY = "LOCAL SIPURI";
+
+    private static final String CALL_ID_KEY = "CallID";
+    private static final String OFFER_SD_KEY = "OfferSD";
+
+    private ISipService mSipService;
+
+    // Will be removed once the SIP service is integrated into framework
+    private BinderHelper<ISipService> mBinderHelper;
+
+    /**
+     * Creates a manager instance and initializes the background SIP service.
+     * Will be removed once the SIP service is integrated into framework.
+     *
+     * @param context context to start the SIP service
+     * @return the manager instance
+     */
+    public static SipManager getInstance(final Context context) {
+        final SipManager manager = new SipManager();
+        manager.createSipService(context);
+        return manager;
+    }
+
+    private SipManager() {
+    }
+
+    private void createSipService(Context context) {
+        if (mSipService != null) return;
+        IBinder b = ServiceManager.getService(Context.SIP_SERVICE);
+        mSipService = ISipService.Stub.asInterface(b);
+    }
+
+    /**
+     * Opens the profile for making calls and/or receiving calls. Subsequent
+     * SIP calls can be made through the default phone UI. The caller may also
+     * make subsequent calls through
+     * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}.
+     * If the receiving-call option is enabled in the profile, the SIP service
+     * will register the profile to the corresponding server periodically in
+     * order to receive calls from the server.
+     *
+     * @param localProfile the SIP profile to make calls from
+     * @throws SipException if the profile contains incorrect settings or
+     *      calling the SIP service results in an error
+     */
+    public void open(SipProfile localProfile) throws SipException {
+        try {
+            mSipService.open(localProfile);
+        } catch (RemoteException e) {
+            throw new SipException("open()", e);
+        }
+    }
+
+    /**
+     * Opens the profile for making calls and/or receiving calls. Subsequent
+     * SIP calls can be made through the default phone UI. The caller may also
+     * make subsequent calls through
+     * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}.
+     * If the receiving-call option is enabled in the profile, the SIP service
+     * will register the profile to the corresponding server periodically in
+     * order to receive calls from the server.
+     *
+     * @param localProfile the SIP profile to receive incoming calls for
+     * @param incomingCallBroadcastAction the action to be broadcast when an
+     *      incoming call is received
+     * @param listener to listen to registration events; can be null
+     * @throws SipException if the profile contains incorrect settings or
+     *      calling the SIP service results in an error
+     */
+    public void open(SipProfile localProfile,
+            String incomingCallBroadcastAction,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            mSipService.open3(localProfile, incomingCallBroadcastAction,
+                    createRelay(listener));
+        } catch (RemoteException e) {
+            throw new SipException("open()", e);
+        }
+    }
+
+    /**
+     * Sets the listener to listen to registration events. No effect if the
+     * profile has not been opened to receive calls
+     * (see {@link #open(SipProfile, String, SipRegistrationListener)} and
+     * {@link #open(SipProfile)}).
+     *
+     * @param localProfileUri the URI of the profile
+     * @param listener to listen to registration events; can be null
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void setRegistrationListener(String localProfileUri,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            mSipService.setRegistrationListener(
+                    localProfileUri, createRelay(listener));
+        } catch (RemoteException e) {
+            throw new SipException("setRegistrationListener()", e);
+        }
+    }
+
+    /**
+     * Closes the specified profile to not make/receive calls. All the resources
+     * that were allocated to the profile are also released.
+     *
+     * @param localProfileUri the URI of the profile to close
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void close(String localProfileUri) throws SipException {
+        try {
+            mSipService.close(localProfileUri);
+        } catch (RemoteException e) {
+            throw new SipException("close()", e);
+        }
+    }
+
+    /**
+     * Checks if the specified profile is enabled to receive calls.
+     *
+     * @param localProfileUri the URI of the profile in question
+     * @return true if the profile is enabled to receive calls
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public boolean isOpened(String localProfileUri) throws SipException {
+        try {
+            return mSipService.isOpened(localProfileUri);
+        } catch (RemoteException e) {
+            throw new SipException("isOpened()", e);
+        }
+    }
+
+    /**
+     * Checks if the specified profile is registered to the server for
+     * receiving calls.
+     *
+     * @param localProfileUri the URI of the profile in question
+     * @return true if the profile is registered to the server
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public boolean isRegistered(String localProfileUri) throws SipException {
+        try {
+            return mSipService.isRegistered(localProfileUri);
+        } catch (RemoteException e) {
+            throw new SipException("isRegistered()", e);
+        }
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to make a call.
+     *
+     * @param context context to create a {@link SipAudioCall} object
+     * @param localProfile the SIP profile to make the call from
+     * @param peerProfile the SIP profile to make the call to
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public SipAudioCall makeAudioCall(Context context, SipProfile localProfile,
+            SipProfile peerProfile, SipAudioCall.Listener listener)
+            throws SipException {
+        SipAudioCall call = new SipAudioCallImpl(context, localProfile);
+        call.setListener(listener);
+        call.makeCall(peerProfile, this);
+        return call;
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to make a call. To use this method, one
+     * must call {@link #open(SipProfile)} first.
+     *
+     * @param context context to create a {@link SipAudioCall} object
+     * @param localProfileUri URI of the SIP profile to make the call from
+     * @param peerProfileUri URI of the SIP profile to make the call to
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public SipAudioCall makeAudioCall(Context context, String localProfileUri,
+            String peerProfileUri, SipAudioCall.Listener listener)
+            throws SipException {
+        try {
+            return makeAudioCall(context,
+                    new SipProfile.Builder(localProfileUri).build(),
+                    new SipProfile.Builder(peerProfileUri).build(), listener);
+        } catch (ParseException e) {
+            throw new SipException("build SipProfile", e);
+        }
+    }
+
+    /**
+     * The method calls {@code takeAudioCall(context, incomingCallIntent,
+     * listener, true}.
+     *
+     * @see #takeAudioCall(Context, Intent, SipAudioCall.Listener, boolean)
+     */
+    public SipAudioCall takeAudioCall(Context context,
+            Intent incomingCallIntent, SipAudioCall.Listener listener)
+            throws SipException {
+        return takeAudioCall(context, incomingCallIntent, listener, true);
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to take an incoming call. Before the call
+     * is returned, the listener will receive a
+     * {@link SipAudioCall#Listener.onRinging(SipAudioCall, SipProfile)}
+     * callback.
+     *
+     * @param context context to create a {@link SipAudioCall} object
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public SipAudioCall takeAudioCall(Context context,
+            Intent incomingCallIntent, SipAudioCall.Listener listener,
+            boolean ringtoneEnabled) throws SipException {
+        if (incomingCallIntent == null) return null;
+
+        String callId = getCallId(incomingCallIntent);
+        if (callId == null) {
+            throw new SipException("Call ID missing in incoming call intent");
+        }
+
+        byte[] offerSd = getOfferSessionDescription(incomingCallIntent);
+        if (offerSd == null) {
+            throw new SipException("Session description missing in incoming "
+                    + "call intent");
+        }
+
+        try {
+            SdpSessionDescription sdp = new SdpSessionDescription(offerSd);
+
+            ISipSession session = mSipService.getPendingSession(callId);
+            if (session == null) return null;
+            SipAudioCall call = new SipAudioCallImpl(
+                    context, session.getLocalProfile());
+            call.setRingtoneEnabled(ringtoneEnabled);
+            call.attachCall(session, sdp);
+            call.setListener(listener);
+            return call;
+        } catch (Throwable t) {
+            throw new SipException("takeAudioCall()", t);
+        }
+    }
+
+    /**
+     * Checks if the intent is an incoming call broadcast intent.
+     *
+     * @param intent the intent in question
+     * @return true if the intent is an incoming call broadcast intent
+     */
+    public static boolean isIncomingCallIntent(Intent intent) {
+        if (intent == null) return false;
+        String callId = getCallId(intent);
+        byte[] offerSd = getOfferSessionDescription(intent);
+        return ((callId != null) && (offerSd != null));
+    }
+
+    /**
+     * Gets the call ID from the specified incoming call broadcast intent.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the call ID or null if the intent does not contain it
+     */
+    public static String getCallId(Intent incomingCallIntent) {
+        return incomingCallIntent.getStringExtra(CALL_ID_KEY);
+    }
+
+    /**
+     * Gets the offer session description from the specified incoming call
+     * broadcast intent.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the offer session description or null if the intent does not
+     *      have it
+     */
+    public static byte[] getOfferSessionDescription(Intent incomingCallIntent) {
+        return incomingCallIntent.getByteArrayExtra(OFFER_SD_KEY);
+    }
+
+    /**
+     * Creates an incoming call broadcast intent.
+     *
+     * @param action the action string to broadcast
+     * @param callId the call ID of the incoming call
+     * @param sessionDescription the session description of the incoming call
+     * @return the incoming call intent
+     * @hide
+     */
+    public static Intent createIncomingCallBroadcast(String action,
+            String callId, byte[] sessionDescription) {
+        Intent intent = new Intent(action);
+        intent.putExtra(CALL_ID_KEY, callId);
+        intent.putExtra(OFFER_SD_KEY, sessionDescription);
+        return intent;
+    }
+
+    /**
+     * Registers the profile to the corresponding server for receiving calls.
+     * {@link #open(SipProfile, String, SipRegistrationListener)} is still
+     * needed to be called at least once in order for the SIP service to
+     * broadcast an intent when an incoming call is received.
+     *
+     * @param localProfile the SIP profile to register with
+     * @param expiryTime registration expiration time (in second)
+     * @param listener to listen to the registration events
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void register(SipProfile localProfile, int expiryTime,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            ISipSession session = mSipService.createSession(
+                    localProfile, createRelay(listener));
+            session.register(expiryTime);
+        } catch (RemoteException e) {
+            throw new SipException("register()", e);
+        }
+    }
+
+    /**
+     * Unregisters the profile from the corresponding server for not receiving
+     * further calls.
+     *
+     * @param localProfile the SIP profile to register with
+     * @param listener to listen to the registration events
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void unregister(SipProfile localProfile,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            ISipSession session = mSipService.createSession(
+                    localProfile, createRelay(listener));
+            session.unregister();
+        } catch (RemoteException e) {
+            throw new SipException("unregister()", e);
+        }
+    }
+
+    /**
+     * Gets the {@link ISipSession} that handles the incoming call. For audio
+     * calls, consider to use {@link SipAudioCall} to handle the incoming call.
+     * See {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.
+     * Note that the method may be called only once for the same intent. For
+     * subsequent calls on the same intent, the method returns null.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the session object that handles the incoming call
+     */
+    public ISipSession getSessionFor(Intent incomingCallIntent)
+            throws SipException {
+        try {
+            String callId = getCallId(incomingCallIntent);
+            return mSipService.getPendingSession(callId);
+        } catch (RemoteException e) {
+            throw new SipException("getSessionFor()", e);
+        }
+    }
+
+    private static ISipSessionListener createRelay(
+            SipRegistrationListener listener) {
+        return ((listener == null) ? null : new ListenerRelay(listener));
+    }
+
+    /**
+     * Creates a {@link ISipSession} with the specified profile. Use other
+     * methods, if applicable, instead of interacting with {@link ISipSession}
+     * directly.
+     *
+     * @param localProfile the SIP profile the session is associated with
+     * @param listener to listen to SIP session events
+     */
+    public ISipSession createSipSession(SipProfile localProfile,
+            ISipSessionListener listener) throws SipException {
+        try {
+            return mSipService.createSession(localProfile, listener);
+        } catch (RemoteException e) {
+            throw new SipException("createSipSession()", e);
+        }
+    }
+
+    /**
+     * Gets the list of profiles hosted by the SIP service. The user information
+     * (username, password and display name) are crossed out.
+     * @hide
+     */
+    public SipProfile[] getListOfProfiles() {
+        try {
+            return mSipService.getListOfProfiles();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    private static class ListenerRelay extends SipSessionAdapter {
+        private SipRegistrationListener mListener;
+
+        // listener must not be null
+        public ListenerRelay(SipRegistrationListener listener) {
+            mListener = listener;
+        }
+
+        private String getUri(ISipSession session) {
+            try {
+                return session.getLocalProfile().getUriString();
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public void onRegistering(ISipSession session) {
+            mListener.onRegistering(getUri(session));
+        }
+
+        @Override
+        public void onRegistrationDone(ISipSession session, int duration) {
+            long expiryTime = duration;
+            if (duration > 0) expiryTime += System.currentTimeMillis();
+            mListener.onRegistrationDone(getUri(session), expiryTime);
+        }
+
+        @Override
+        public void onRegistrationFailed(ISipSession session, String className,
+                String message) {
+            mListener.onRegistrationFailed(getUri(session), className, message);
+        }
+
+        @Override
+        public void onRegistrationTimeout(ISipSession session) {
+            mListener.onRegistrationFailed(getUri(session),
+                    SipException.class.getName(), "registration timed out");
+        }
+    }
+}
diff --git a/voip/java/android/net/sip/SipProfile.aidl b/voip/java/android/net/sip/SipProfile.aidl
new file mode 100644
index 0000000..3b6f68f
--- /dev/null
+++ b/voip/java/android/net/sip/SipProfile.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.net.sip;
+
+parcelable SipProfile;
diff --git a/voip/java/android/net/sip/SipProfile.java b/voip/java/android/net/sip/SipProfile.java
new file mode 100644
index 0000000..e71c293
--- /dev/null
+++ b/voip/java/android/net/sip/SipProfile.java
@@ -0,0 +1,401 @@
+/*
+ * 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 android.net.sip;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.PeerUnavailableException;
+import javax.sip.SipFactory;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.address.URI;
+
+/**
+ * Class containing a SIP account, domain and server information.
+ * @hide
+ */
+public class SipProfile implements Parcelable, Serializable {
+    private static final long serialVersionUID = 1L;
+    private static final int DEFAULT_PORT = 5060;
+    private Address mAddress;
+    private String mProxyAddress;
+    private String mPassword;
+    private String mDomain;
+    private String mProtocol = ListeningPoint.UDP;
+    private String mProfileName;
+    private boolean mSendKeepAlive = false;
+    private boolean mAutoRegistration = true;
+
+    /** @hide */
+    public static final Parcelable.Creator<SipProfile> CREATOR =
+            new Parcelable.Creator<SipProfile>() {
+                public SipProfile createFromParcel(Parcel in) {
+                    return new SipProfile(in);
+                }
+
+                public SipProfile[] newArray(int size) {
+                    return new SipProfile[size];
+                }
+            };
+
+    /**
+     * Class to help create a {@link SipProfile}.
+     */
+    public static class Builder {
+        private AddressFactory mAddressFactory;
+        private SipProfile mProfile = new SipProfile();
+        private SipURI mUri;
+        private String mDisplayName;
+        private String mProxyAddress;
+
+        {
+            try {
+                mAddressFactory =
+                        SipFactory.getInstance().createAddressFactory();
+            } catch (PeerUnavailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Constructor.
+         *
+         * @param uriString the URI string as "sip:<user_name>@<domain>"
+         * @throws ParseException if the string is not a valid URI
+         */
+        public Builder(String uriString) throws ParseException {
+            if (uriString == null) {
+                throw new NullPointerException("uriString cannot be null");
+            }
+            URI uri = mAddressFactory.createURI(fix(uriString));
+            if (uri instanceof SipURI) {
+                mUri = (SipURI) uri;
+            } else {
+                throw new ParseException(uriString + " is not a SIP URI", 0);
+            }
+            mProfile.mDomain = mUri.getHost();
+        }
+
+        /**
+         * Constructor.
+         *
+         * @param username username of the SIP account
+         * @param serverDomain the SIP server domain; if the network address
+         *      is different from the domain, use
+         *      {@link #setOutboundProxy(String)} to set server address
+         * @throws ParseException if the parameters are not valid
+         */
+        public Builder(String username, String serverDomain)
+                throws ParseException {
+            if ((username == null) || (serverDomain == null)) {
+                throw new NullPointerException(
+                        "username and serverDomain cannot be null");
+            }
+            mUri = mAddressFactory.createSipURI(username, serverDomain);
+            mProfile.mDomain = serverDomain;
+        }
+
+        private String fix(String uriString) {
+            return (uriString.trim().toLowerCase().startsWith("sip:")
+                    ? uriString
+                    : "sip:" + uriString);
+        }
+
+        /**
+         * Sets the name of the profile. This name is given by user.
+         *
+         * @param name name of the profile
+         * @return this builder object
+         */
+        public Builder setProfileName(String name) {
+            mProfile.mProfileName = name;
+            return this;
+        }
+
+        /**
+         * Sets the password of the SIP account
+         *
+         * @param password password of the SIP account
+         * @return this builder object
+         */
+        public Builder setPassword(String password) {
+            mUri.setUserPassword(password);
+            return this;
+        }
+
+        /**
+         * Sets the port number of the server. By default, it is 5060.
+         *
+         * @param port port number of the server
+         * @return this builder object
+         * @throws InvalidArgumentException if the port number is out of range
+         */
+        public Builder setPort(int port) throws InvalidArgumentException {
+            mUri.setPort(port);
+            return this;
+        }
+
+        /**
+         * Sets the protocol used to connect to the SIP server. Currently,
+         * only "UDP" and "TCP" are supported.
+         *
+         * @param protocol the protocol string
+         * @return this builder object
+         * @throws InvalidArgumentException if the protocol is not recognized
+         */
+        public Builder setProtocol(String protocol)
+                throws InvalidArgumentException {
+            if (protocol == null) {
+                throw new NullPointerException("protocol cannot be null");
+            }
+            protocol = protocol.toUpperCase();
+            if (!protocol.equals("UDP") && !protocol.equals("TCP")) {
+                throw new InvalidArgumentException(
+                        "unsupported protocol: " + protocol);
+            }
+            mProfile.mProtocol = protocol;
+            return this;
+        }
+
+        /**
+         * Sets the outbound proxy of the SIP server.
+         *
+         * @param outboundProxy the network address of the outbound proxy
+         * @return this builder object
+         */
+        public Builder setOutboundProxy(String outboundProxy) {
+            mProxyAddress = outboundProxy;
+            return this;
+        }
+
+        /**
+         * Sets the display name of the user.
+         *
+         * @param displayName display name of the user
+         * @return this builder object
+         */
+        public Builder setDisplayName(String displayName) {
+            mDisplayName = displayName;
+            return this;
+        }
+
+        /**
+         * Sets the send keep-alive flag.
+         *
+         * @param flag true if sending keep-alive message is required,
+         *      false otherwise
+         * @return this builder object
+         */
+        public Builder setSendKeepAlive(boolean flag) {
+            mProfile.mSendKeepAlive = flag;
+            return this;
+        }
+
+
+        /**
+         * Sets the auto. registration flag.
+         *
+         * @param flag true if the profile will be registered automatically,
+         *      false otherwise
+         * @return this builder object
+         */
+        public Builder setAutoRegistration(boolean flag) {
+            mProfile.mAutoRegistration = flag;
+            return this;
+        }
+
+        /**
+         * Builds and returns the SIP profile object.
+         *
+         * @return the profile object created
+         */
+        public SipProfile build() {
+            // remove password from URI
+            mProfile.mPassword = mUri.getUserPassword();
+            mUri.setUserPassword(null);
+            try {
+                mProfile.mAddress = mAddressFactory.createAddress(
+                        mDisplayName, mUri);
+                if (!TextUtils.isEmpty(mProxyAddress)) {
+                    SipURI uri = (SipURI)
+                            mAddressFactory.createURI(fix(mProxyAddress));
+                    mProfile.mProxyAddress = uri.getHost();
+                }
+            } catch (ParseException e) {
+                // must not occur
+                throw new RuntimeException(e);
+            }
+            return mProfile;
+        }
+    }
+
+    private SipProfile() {
+    }
+
+    private SipProfile(Parcel in) {
+        mAddress = (Address) in.readSerializable();
+        mProxyAddress = in.readString();
+        mPassword = in.readString();
+        mDomain = in.readString();
+        mProtocol = in.readString();
+        mProfileName = in.readString();
+        mSendKeepAlive = (in.readInt() == 0) ? false : true;
+        mAutoRegistration = (in.readInt() == 0) ? false : true;
+    }
+
+    /** @hide */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeSerializable(mAddress);
+        out.writeString(mProxyAddress);
+        out.writeString(mPassword);
+        out.writeString(mDomain);
+        out.writeString(mProtocol);
+        out.writeString(mProfileName);
+        out.writeInt(mSendKeepAlive ? 1 : 0);
+        out.writeInt(mAutoRegistration ? 1 : 0);
+    }
+
+    /** @hide */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Gets the SIP URI of this profile.
+     *
+     * @return the SIP URI of this profile
+     */
+    public SipURI getUri() {
+        return (SipURI) mAddress.getURI();
+    }
+
+    /**
+     * Gets the SIP URI string of this profile.
+     *
+     * @return the SIP URI string of this profile
+     */
+    public String getUriString() {
+        return mAddress.getURI().toString();
+    }
+
+    /**
+     * Gets the SIP address of this profile.
+     *
+     * @return the SIP address of this profile
+     */
+    public Address getSipAddress() {
+        return mAddress;
+    }
+
+    /**
+     * Gets the display name of the user.
+     *
+     * @return the display name of the user
+     */
+    public String getDisplayName() {
+        return mAddress.getDisplayName();
+    }
+
+    /**
+     * Gets the username.
+     *
+     * @return the username
+     */
+    public String getUserName() {
+        return getUri().getUser();
+    }
+
+    /**
+     * Gets the password.
+     *
+     * @return the password
+     */
+    public String getPassword() {
+        return mPassword;
+    }
+
+    /**
+     * Gets the SIP domain.
+     *
+     * @return the SIP domain
+     */
+    public String getSipDomain() {
+        return mDomain;
+    }
+
+    /**
+     * Gets the port number of the SIP server.
+     *
+     * @return the port number of the SIP server
+     */
+    public int getPort() {
+        int port = getUri().getPort();
+        return (port == -1) ? DEFAULT_PORT : port;
+    }
+
+    /**
+     * Gets the protocol used to connect to the server.
+     *
+     * @return the protocol
+     */
+    public String getProtocol() {
+        return mProtocol;
+    }
+
+    /**
+     * Gets the network address of the server outbound proxy.
+     *
+     * @return the network address of the server outbound proxy
+     */
+    public String getProxyAddress() {
+        return mProxyAddress;
+    }
+
+    /**
+     * Gets the (user-defined) name of the profile.
+     *
+     * @return name of the profile
+     */
+    public String getProfileName() {
+        return mProfileName;
+    }
+
+    /**
+     * Gets the flag of 'Sending keep-alive'.
+     *
+     * @return the flag of sending SIP keep-alive messages.
+     */
+    public boolean getSendKeepAlive() {
+        return mSendKeepAlive;
+    }
+
+    /**
+     * Gets the flag of 'Auto Registration'.
+     *
+     * @return the flag of registering the profile automatically.
+     */
+    public boolean getAutoRegistration() {
+        return mAutoRegistration;
+    }
+}
diff --git a/voip/java/android/net/sip/SipRegistrationListener.java b/voip/java/android/net/sip/SipRegistrationListener.java
new file mode 100644
index 0000000..63faaf8
--- /dev/null
+++ b/voip/java/android/net/sip/SipRegistrationListener.java
@@ -0,0 +1,48 @@
+/*
+ * 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 android.net.sip;
+
+/**
+ * Listener class to listen to SIP registration events.
+ * @hide
+ */
+public interface SipRegistrationListener {
+    /**
+     * Called when a registration request is sent.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     */
+    void onRegistering(String localProfileUri);
+
+    /**
+     * Called when registration is successfully done.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     * @param expiryTime duration in second before the registration expires
+     */
+    void onRegistrationDone(String localProfileUri, long expiryTime);
+
+    /**
+     * Called when the registration fails.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     * @param errorClass name of the exception class
+     * @param errorMessage error message
+     */
+    void onRegistrationFailed(String localProfileUri, String errorClass,
+            String errorMessage);
+}
diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java
new file mode 100644
index 0000000..cfb71d7
--- /dev/null
+++ b/voip/java/android/net/sip/SipSessionAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * 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 android.net.sip;
+
+/**
+ * Adapter class for {@link ISipSessionListener}. Default implementation of all
+ * callback methods is no-op.
+ * @hide
+ */
+public class SipSessionAdapter extends ISipSessionListener.Stub {
+    public void onCalling(ISipSession session) {
+    }
+
+    public void onRinging(ISipSession session, SipProfile caller,
+            byte[] sessionDescription) {
+    }
+
+    public void onRingingBack(ISipSession session) {
+    }
+
+    public void onCallEstablished(
+            ISipSession session, byte[] sessionDescription) {
+    }
+
+    public void onCallEnded(ISipSession session) {
+    }
+
+    public void onCallBusy(ISipSession session) {
+    }
+
+    public void onCallChanged(ISipSession session, byte[] sessionDescription) {
+    }
+
+    public void onCallChangeFailed(ISipSession session, String className,
+            String message) {
+    }
+
+    public void onError(ISipSession session, String className, String message) {
+    }
+
+    public void onRegistering(ISipSession session) {
+    }
+
+    public void onRegistrationDone(ISipSession session, int duration) {
+    }
+
+    public void onRegistrationFailed(ISipSession session, String className,
+            String message) {
+    }
+
+    public void onRegistrationTimeout(ISipSession session) {
+    }
+}
diff --git a/voip/java/android/net/sip/SipSessionState.java b/voip/java/android/net/sip/SipSessionState.java
new file mode 100644
index 0000000..5bab112
--- /dev/null
+++ b/voip/java/android/net/sip/SipSessionState.java
@@ -0,0 +1,66 @@
+/*
+ * 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 android.net.sip;
+
+/**
+ * Defines {@link ISipSession} states.
+ * @hide
+ */
+public enum SipSessionState {
+    /** When session is ready to initiate a call or transaction. */
+    READY_TO_CALL,
+
+    /** When the registration request is sent out. */
+    REGISTERING,
+
+    /** When the unregistration request is sent out. */
+    DEREGISTERING,
+
+    /** When an INVITE request is received. */
+    INCOMING_CALL,
+
+    /** When an OK response is sent for the INVITE request received. */
+    INCOMING_CALL_ANSWERING,
+
+    /** When an INVITE request is sent. */
+    OUTGOING_CALL,
+
+    /** When a RINGING response is received for the INVITE request sent. */
+    OUTGOING_CALL_RING_BACK,
+
+    /** When a CANCEL request is sent for the INVITE request sent. */
+    OUTGOING_CALL_CANCELING,
+
+    /** When a call is established. */
+    IN_CALL,
+
+    /** Some error occurs when making a remote call to {@link ISipSession}. */
+    REMOTE_ERROR,
+
+    /** When an OPTIONS request is sent. */
+    PINGING;
+
+    /**
+     * Checks if the specified string represents the same state as this object.
+     *
+     * @return true if the specified string represents the same state as this
+     *      object
+     */
+    public boolean equals(String state) {
+        return toString().equals(state);
+    }
+}