Merge "Suppress the recording sound in the recorded video" into gingerbread
diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java
index 2714de5..b459320 100644
--- a/core/java/android/app/AlertDialog.java
+++ b/core/java/android/app/AlertDialog.java
@@ -428,7 +428,7 @@
         }
         
         /**
-         * Sets whether the dialog is cancelable or not default is true.
+         * Sets whether the dialog is cancelable or not.  Default is true.
          *
          * @return This Builder object to allow for chaining of calls to set methods
          */
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index cdc9bbb..a0e9d02 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -1409,9 +1409,13 @@
      * Convenience method for inserting a row into the database.
      *
      * @param table the table to insert the row into
-     * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
-     *            so if initialValues is empty this column will explicitly be
-     *            assigned a NULL value
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>values</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>values</code> is empty.
      * @param values this map contains the initial column values for the
      *            row. The keys should be the column names and the values the
      *            column values
@@ -1430,9 +1434,13 @@
      * Convenience method for inserting a row into the database.
      *
      * @param table the table to insert the row into
-     * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
-     *            so if initialValues is empty this column will explicitly be
-     *            assigned a NULL value
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>values</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>values</code> is empty.
      * @param values this map contains the initial column values for the
      *            row. The keys should be the column names and the values the
      *            column values
@@ -1448,11 +1456,15 @@
      * Convenience method for replacing a row in the database.
      *
      * @param table the table in which to replace the row
-     * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
-     *            so if initialValues is empty this row will explicitly be
-     *            assigned a NULL value
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
      * @param initialValues this map contains the initial column values for
-     *   the row. The key
+     *   the row.
      * @return the row ID of the newly inserted row, or -1 if an error occurred
      */
     public long replace(String table, String nullColumnHack, ContentValues initialValues) {
@@ -1469,9 +1481,13 @@
      * Convenience method for replacing a row in the database.
      *
      * @param table the table in which to replace the row
-     * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
-     *            so if initialValues is empty this row will explicitly be
-     *            assigned a NULL value
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
      * @param initialValues this map contains the initial column values for
      *   the row. The key
      * @throws SQLException
@@ -1487,9 +1503,13 @@
      * General method for inserting a row into the database.
      *
      * @param table the table to insert the row into
-     * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
-     *            so if initialValues is empty this column will explicitly be
-     *            assigned a NULL value
+     * @param nullColumnHack optional; may be <code>null</code>.
+     *            SQL doesn't allow inserting a completely empty row without
+     *            naming at least one column name.  If your provided <code>initialValues</code> is
+     *            empty, no column names are known and an empty row can't be inserted.
+     *            If not set to null, the <code>nullColumnHack</code> parameter
+     *            provides the name of nullable column name to explicitly insert a NULL into
+     *            in the case where your <code>initialValues</code> is empty.
      * @param initialValues this map contains the initial column values for the
      *            row. The keys should be the column names and the values the
      *            column values
@@ -1726,10 +1746,10 @@
 
     /**
      * Execute a single SQL statement that is not a query. For example, CREATE
-     * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
-     * supported. it takes a write lock
+     * TABLE, DELETE, INSERT, etc. Multiple statements separated by semicolons are not
+     * supported.  Takes a write lock.
      *
-     * @throws SQLException If the SQL string is invalid for some reason
+     * @throws SQLException if the SQL string is invalid
      */
     public void execSQL(String sql) throws SQLException {
         BlockGuard.getThreadPolicy().onWriteToDisk();
@@ -1760,12 +1780,12 @@
 
     /**
      * Execute a single SQL statement that is not a query. For example, CREATE
-     * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
-     * supported. it takes a write lock,
+     * TABLE, DELETE, INSERT, etc. Multiple statements separated by semicolons are not
+     * supported.  Takes a write lock.
      *
      * @param sql
      * @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
-     * @throws SQLException If the SQL string is invalid for some reason
+     * @throws SQLException if the SQL string is invalid
      */
     public void execSQL(String sql, Object[] bindArgs) throws SQLException {
         BlockGuard.getThreadPolicy().onWriteToDisk();
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index f3b2c81..dee1d03 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -183,10 +183,10 @@
          * the right of the screen, the value should be 270.
          *
          * @see #setDisplayOrientation(int)
-         * @see #setRotation(int)
-         * @see #setPreviewSize(int, int)
-         * @see #setPictureSize(int, int)
-         * @see #setJpegThumbnailSize(int, int)
+         * @see Parameters#setRotation(int)
+         * @see Parameters#setPreviewSize(int, int)
+         * @see Parameters#setPictureSize(int, int)
+         * @see Parameters#setJpegThumbnailSize(int, int)
          */
         public int orientation;
     };
@@ -609,9 +609,10 @@
     public interface AutoFocusCallback
     {
         /**
-         * Called when the camera auto focus completes.  If the camera does not
-         * support auto-focus and autoFocus is called, onAutoFocus will be
-         * called immediately with success.
+         * Called when the camera auto focus completes.  If the camera
+         * does not support auto-focus and autoFocus is called,
+         * onAutoFocus will be called immediately with a fake value of
+         * <code>success</code> set to <code>true</code>.
          *
          * @param success true if focus was successful, false if otherwise
          * @param camera  the Camera service object
@@ -785,12 +786,12 @@
      * is, the image is reflected along the central vertical axis of the camera
      * sensor. So the users can see themselves as looking into a mirror.
      *
-     * This does not affect the order of byte array passed in {@link
+     * <p>This does not affect the order of byte array passed in {@link
      * PreviewCallback#onPreviewFrame}, JPEG pictures, or recorded videos. This
      * method is not allowed to be called during preview.
      *
-     * If you want to make the camera image show in the same orientation as
-     * the display, you can use the following code.<p>
+     * <p>If you want to make the camera image show in the same orientation as
+     * the display, you can use the following code.
      * <pre>
      * public static void setCameraDisplayOrientation(Activity activity,
      *         int cameraId, android.hardware.Camera camera) {
@@ -1767,26 +1768,27 @@
          * the orientation in the EXIF header will be missing or 1 (row #0 is
          * top and column #0 is left side).
          *
-         * If applications want to rotate the picture to match the orientation
+         * <p>If applications want to rotate the picture to match the orientation
          * of what users see, apps should use {@link
          * android.view.OrientationEventListener} and {@link CameraInfo}.
          * The value from OrientationEventListener is relative to the natural
          * orientation of the device. CameraInfo.orientation is the angle
-         * between camera orientation and natural device orientation. The sum or
+         * between camera orientation and natural device orientation. The sum
          * of the two is the rotation angle for back-facing camera. The
          * difference of the two is the rotation angle for front-facing camera.
          * Note that the JPEG pictures of front-facing cameras are not mirrored
          * as in preview display.
          *
-         * For example, suppose the natural orientation of the device is
+         * <p>For example, suppose the natural orientation of the device is
          * portrait. The device is rotated 270 degrees clockwise, so the device
          * orientation is 270. Suppose a back-facing camera sensor is mounted in
          * landscape and the top side of the camera sensor is aligned with the
          * right edge of the display in natural orientation. So the camera
          * orientation is 90. The rotation should be set to 0 (270 + 90).
          *
-         * The reference code is as follows.
+         * <p>The reference code is as follows.
          *
+	 * <pre>
          * public void public void onOrientationChanged(int orientation) {
          *     if (orientation == ORIENTATION_UNKNOWN) return;
          *     android.hardware.Camera.CameraInfo info =
@@ -1801,6 +1803,7 @@
          *     }
          *     mParameters.setRotation(rotation);
          * }
+	 * </pre>
          *
          * @param rotation The rotation angle in degrees relative to the
          *                 orientation of the camera. Rotation can only be 0,
diff --git a/core/java/android/nfc/technology/IsoDep.java b/core/java/android/nfc/technology/IsoDep.java
index 118bff7..32a7542 100644
--- a/core/java/android/nfc/technology/IsoDep.java
+++ b/core/java/android/nfc/technology/IsoDep.java
@@ -39,18 +39,18 @@
  */
 public final class IsoDep extends BasicTagTechnology {
     /** @hide */
-    public static final String EXTRA_ATTRIB = "attrib";
+    public static final String EXTRA_HI_LAYER_RESP = "hiresp";
     /** @hide */
     public static final String EXTRA_HIST_BYTES = "histbytes";
 
-    private byte[] mAttrib = null;
+    private byte[] mHiLayerResponse = null;
     private byte[] mHistBytes = null;
 
     public IsoDep(NfcAdapter adapter, Tag tag, Bundle extras)
             throws RemoteException {
         super(adapter, tag, TagTechnology.ISO_DEP);
         if (extras != null) {
-            mAttrib = extras.getByteArray(EXTRA_ATTRIB);
+            mHiLayerResponse = extras.getByteArray(EXTRA_HI_LAYER_RESP);
             mHistBytes = extras.getByteArray(EXTRA_HIST_BYTES);
         }
     }
@@ -63,20 +63,5 @@
     /**
      * 3B only
      */
-    public byte[] getAttrib() { return mAttrib; }
-
-    /**
-     * Attempts to select the given application on the tag. Note that this only works
-     * if the tag supports ISO7816-4, which not all IsoDep tags support. If the tag doesn't
-     * support ISO7816-4 this will throw {@link UnsupportedOperationException}.
-     *
-     * This method requires that you call {@link #connect} before calling it.
-     *
-     * @throws IOException, UnsupportedOperationException
-     */
-    public void selectAid(byte[] aid) throws IOException, UnsupportedOperationException {
-        checkConnected();
-
-        throw new UnsupportedOperationException();
-    }
+    public byte[] getHiLayerResponse() { return mHiLayerResponse; }
 }
diff --git a/core/java/android/nfc/technology/MifareClassic.java b/core/java/android/nfc/technology/MifareClassic.java
index 8a9ebf1..799f0a78 100644
--- a/core/java/android/nfc/technology/MifareClassic.java
+++ b/core/java/android/nfc/technology/MifareClassic.java
@@ -205,6 +205,15 @@
         return getBlockCount(sector) * 16;
     }
 
+    public int getTotalBlockCount() {
+        int totalBlocks = 0;
+        for (int sec = 0; sec < getSectorCount(); sec++) {
+            totalBlocks += getSectorSize(sec);
+        }
+
+        return totalBlocks;
+    }
+
     public int getBlockCount(int sector) {
         if (sector >= getSectorCount()) {
             throw new IllegalArgumentException("this card only has " + getSectorCount() +
@@ -343,9 +352,27 @@
         checkConnected();
 
         byte addr = (byte) block;
-        byte[] incr_cmd = { (byte) 0xC0, (byte) block };
+        byte[] decr_cmd = { (byte) 0xC0, (byte) block };
 
-        transceive(incr_cmd);
+        transceive(decr_cmd);
+    }
+
+    public void transfer(int block) throws IOException {
+        checkConnected();
+
+        byte addr = (byte) block;
+        byte[] trans_cmd = { (byte) 0xB0, (byte) block };
+
+        transceive(trans_cmd);
+    }
+
+    public void restore(int block) throws IOException {
+        checkConnected();
+
+        byte addr = (byte) block;
+        byte[] rest_cmd = { (byte) 0xC2, (byte) block };
+
+        transceive(rest_cmd);
     }
 
     /**
diff --git a/core/java/android/nfc/technology/Ndef.java b/core/java/android/nfc/technology/Ndef.java
index 53db0c5..05872fe 100644
--- a/core/java/android/nfc/technology/Ndef.java
+++ b/core/java/android/nfc/technology/Ndef.java
@@ -54,9 +54,22 @@
     /** @hide */
     public static final String EXTRA_NDEF_CARDSTATE = "ndefcardstate";
 
+    /** @hide */
+    public static final String EXTRA_NDEF_TYPE = "ndeftype";
+
+    //TODO: consider removing OTHER entirely - and not allowing Ndef to
+    // enumerate for tag types outside of (NFC Forum 1-4, MifareClassic)
+    public static final int OTHER = -1;
+    public static final int NFC_FORUM_TYPE_1 = 1;
+    public static final int NFC_FORUM_TYPE_2 = 2;
+    public static final int NFC_FORUM_TYPE_3 = 3;
+    public static final int NFC_FORUM_TYPE_4 = 4;
+    public static final int MIFARE_CLASSIC = 105;
+
     private final int mMaxNdefSize;
     private final int mCardState;
     private final NdefMessage mNdefMsg;
+    private final int mNdefType;
 
     /**
      * Internal constructor, to be used by NfcAdapter
@@ -68,6 +81,7 @@
             mMaxNdefSize = extras.getInt(EXTRA_NDEF_MAXLENGTH);
             mCardState = extras.getInt(EXTRA_NDEF_CARDSTATE);
             mNdefMsg = extras.getParcelable(EXTRA_NDEF_MSG);
+            mNdefType = extras.getInt(EXTRA_NDEF_TYPE);
         } else {
             throw new NullPointerException("NDEF tech extras are null.");
         }
@@ -92,6 +106,24 @@
     }
 
     /**
+     * Get NDEF tag type.
+     * <p>Returns one of {@link #NFC_FORUM_TYPE_1}, {@link #NFC_FORUM_TYPE_2},
+     * {@link #NFC_FORUM_TYPE_3}, {@link #NFC_FORUM_TYPE_4},
+     * {@link #MIFARE_CLASSIC} or {@link #OTHER}.
+     * <p>Platforms of this API revision will always return one of the above
+     * values. Platforms at future API revisions may return other values, which
+     * can be treated as {@link #OTHER} by applications targeting this API.
+     * <p>Android devices with NFC support must always correctly enumerate
+     * NFC Forum tag types, and may optionally enumerate
+     * {@link #MIFARE_CLASSIC} since it requires proprietary technology.
+     * Devices that cannot enumerate {@link #MIFARE_CLASSIC} will use
+     * {@link #OTHER} instead.
+     */
+    public int getType() {
+        return mNdefType;
+    }
+
+    /**
      * Get maximum NDEF message size in bytes
      */
     public int getMaxSize() {
diff --git a/core/java/android/nfc/technology/NfcB.java b/core/java/android/nfc/technology/NfcB.java
index 64cb08a..267c94d 100644
--- a/core/java/android/nfc/technology/NfcB.java
+++ b/core/java/android/nfc/technology/NfcB.java
@@ -37,19 +37,34 @@
  */
 public final class NfcB extends BasicTagTechnology {
     /** @hide */
-    public static final String EXTRA_ATQB = "atqb";
+    public static final String EXTRA_APPDATA = "appdata";
+    /** @hide */
+    public static final String EXTRA_PROTINFO = "protinfo";
 
-    private byte[] mAtqb;
+    private byte[] mAppData;
+    private byte[] mProtInfo;
 
     public NfcB(NfcAdapter adapter, Tag tag, Bundle extras)
             throws RemoteException {
         super(adapter, tag, TagTechnology.NFC_B);
+        mAppData = extras.getByteArray(EXTRA_APPDATA);
+        mProtInfo = extras.getByteArray(EXTRA_PROTINFO);
     }
 
     /**
-     * Returns the ATQB/SENSB_RES bytes discovered at tag discovery.
+     * Returns the Application Data bytes from the ATQB/SENSB_RES
+     * bytes discovered at tag discovery.
      */
-    public byte[] getAtqb() {
-        return mAtqb;
+    public byte[] getApplicationData() {
+        return mAppData;
     }
+
+    /**
+     * Returns the Protocol Info bytes from the ATQB/SENSB_RES
+     * bytes discovered at tag discovery.
+     */
+    public byte[] getProtocolInfo() {
+        return mProtInfo;
+    }
+
 }
diff --git a/core/java/android/nfc/technology/NfcV.java b/core/java/android/nfc/technology/NfcV.java
index 9b6a16a..460de6a 100644
--- a/core/java/android/nfc/technology/NfcV.java
+++ b/core/java/android/nfc/technology/NfcV.java
@@ -36,8 +36,27 @@
  * permission.
  */
 public final class NfcV extends BasicTagTechnology {
+    /** @hide */
+    public static final String EXTRA_RESP_FLAGS = "respflags";
+
+    /** @hide */
+    public static final String EXTRA_DSFID = "dsfid";
+
+    private byte mRespFlags;
+    private byte mDsfId;
+
     public NfcV(NfcAdapter adapter, Tag tag, Bundle extras)
             throws RemoteException {
         super(adapter, tag, TagTechnology.NFC_V);
+        mRespFlags = extras.getByte(EXTRA_RESP_FLAGS);
+        mDsfId = extras.getByte(EXTRA_DSFID);
+    }
+
+    public byte getResponseFlags() {
+        return mRespFlags;
+    }
+
+    public byte getDsfId() {
+        return mDsfId;
     }
 }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 6590497..1766345 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -9127,7 +9127,7 @@
          *
          * @param v The view that was clicked and held.
          *
-         * return True if the callback consumed the long click, false otherwise
+         * @return true if the callback consumed the long click, false otherwise.
          */
         boolean onLongClick(View v);
     }
diff --git a/docs/html/guide/appendix/faq/commontasks.jd b/docs/html/guide/appendix/faq/commontasks.jd
index 38a89ef..b3dc236 100644
--- a/docs/html/guide/appendix/faq/commontasks.jd
+++ b/docs/html/guide/appendix/faq/commontasks.jd
@@ -655,7 +655,7 @@
         and <code>STRIKE</code> (strikethrough).
         So, for example, in res/values/strings.xml you could declare this:<br />
         <code>&lt;resource&gt;<br />
-        &nbsp;&nbsp;&nbsp;&nbsp;&lt;string&gt;id=&quot;@+id/styled_welcome_message&quot;&gt;We
+        &nbsp;&nbsp;&nbsp;&nbsp;&lt;string&nbsp;id=&quot;@+id/styled_welcome_message&quot;&gt;We
         are &lt;b&gt;&lt;i&gt;so&lt;/i&gt;&lt;/b&gt; glad to see you.&lt;/string&gt;<br />
         &lt;/resources&gt;</code></li>
     <li>To style text on the fly, or to add highlighting or more complex styling,
diff --git a/docs/html/resources/articles/contacts.jd b/docs/html/resources/articles/contacts.jd
index bdf84e6..c837dc3 100644
--- a/docs/html/resources/articles/contacts.jd
+++ b/docs/html/resources/articles/contacts.jd
@@ -107,7 +107,7 @@
 <p>When a raw contact is added or modified, the system looks for matching
 (overlapping) raw contacts with which to aggregate it. It may not find any
 matching raw contacts, in which case it will create an aggregate contact that
-contains just the original raw contact. If it finds a single match,it creates a
+contains just the original raw contact. If it finds a single match, it creates a
 new contact that contains the two raw contacts. And it may even find multiple
 similar raw contacts, in which case it chooses the closest match. </p>
 
diff --git a/docs/html/resources/faq/commontasks.jd b/docs/html/resources/faq/commontasks.jd
index 9c32e9c..1173725 100644
--- a/docs/html/resources/faq/commontasks.jd
+++ b/docs/html/resources/faq/commontasks.jd
@@ -655,7 +655,7 @@
         and <code>STRIKE</code> (strikethrough).
         So, for example, in res/values/strings.xml you could declare this:<br />
         <code>&lt;resource&gt;<br />
-        &nbsp;&nbsp;&nbsp;&nbsp;&lt;string&gt;id=&quot;@+id/styled_welcome_message&quot;&gt;We
+        &nbsp;&nbsp;&nbsp;&nbsp;&lt;string&nbsp;id=&quot;@+id/styled_welcome_message&quot;&gt;We
         are &lt;b&gt;&lt;i&gt;so&lt;/i&gt;&lt;/b&gt; glad to see you.&lt;/string&gt;<br />
         &lt;/resources&gt;</code></li>
     <li>To style text on the fly, or to add highlighting or more complex styling,
diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java
index 3125321..9655125 100644
--- a/graphics/java/android/graphics/drawable/Drawable.java
+++ b/graphics/java/android/graphics/drawable/Drawable.java
@@ -202,7 +202,7 @@
 
     /**
      * Return a mask of the configuration parameters for which this drawable
-     * mau change, requiring that it be re-created.  The default implementation
+     * may change, requiring that it be re-created.  The default implementation
      * returns whatever was provided through
      * {@link #setChangingConfigurations(int)} or 0 by default.  Subclasses
      * may extend this to or in the changing configurations of any other
diff --git a/telephony/java/com/android/internal/telephony/sip/SipPhone.java b/telephony/java/com/android/internal/telephony/sip/SipPhone.java
index a92ac1c..72f3831 100755
--- a/telephony/java/com/android/internal/telephony/sip/SipPhone.java
+++ b/telephony/java/com/android/internal/telephony/sip/SipPhone.java
@@ -17,6 +17,7 @@
 package com.android.internal.telephony.sip;
 
 import android.content.Context;
+import android.media.AudioManager;
 import android.net.rtp.AudioGroup;
 import android.net.sip.SipAudioCall;
 import android.net.sip.SipErrorCode;
@@ -126,7 +127,7 @@
                     (ringingCall.getState() == Call.State.WAITING)) {
                 if (DEBUG) Log.d(LOG_TAG, "acceptCall");
                 // Always unmute when answering a new call
-                setMute(false);
+                ringingCall.setMute(false);
                 ringingCall.acceptCall();
             } else {
                 throw new CallStateException("phone not ringing");
@@ -170,7 +171,7 @@
             throw new CallStateException("cannot dial in current state");
         }
 
-        setMute(false);
+        foregroundCall.setMute(false);
         try {
             Connection c = foregroundCall.dial(dialString);
             return c;
@@ -288,16 +289,13 @@
 
     @Override
     public void setEchoSuppressionEnabled(boolean enabled) {
+        // TODO: Remove the enabled argument. We should check the speakerphone
+        // state with AudioManager instead of keeping a state here so the
+        // method with a state argument is redundant. Also rename the method
+        // to something like onSpeaerphoneStateChanged(). Echo suppression may
+        // not be available on every device.
         synchronized (SipPhone.class) {
-            AudioGroup audioGroup = foregroundCall.getAudioGroup();
-            if (audioGroup == null) return;
-            int mode = audioGroup.getMode();
-            audioGroup.setMode(enabled
-                    ? AudioGroup.MODE_ECHO_SUPPRESSION
-                    : AudioGroup.MODE_NORMAL);
-            if (DEBUG) Log.d(LOG_TAG, String.format(
-                    "audioGroup mode change: %d --> %d", mode,
-                    audioGroup.getMode()));
+            foregroundCall.setAudioGroupMode();
         }
     }
 
@@ -450,13 +448,33 @@
             ((SipConnection) connections.get(0)).acceptCall();
         }
 
+        private boolean isSpeakerOn() {
+            return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                    .isSpeakerphoneOn();
+        }
+
+        void setAudioGroupMode() {
+            AudioGroup audioGroup = getAudioGroup();
+            if (audioGroup == null) return;
+            int mode = audioGroup.getMode();
+            if (state == State.HOLDING) {
+                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+            } else if (getMute()) {
+                audioGroup.setMode(AudioGroup.MODE_MUTED);
+            } else if (isSpeakerOn()) {
+                audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
+            } else {
+                audioGroup.setMode(AudioGroup.MODE_NORMAL);
+            }
+            if (DEBUG) Log.d(LOG_TAG, String.format(
+                    "audioGroup mode change: %d --> %d", mode,
+                    audioGroup.getMode()));
+        }
+
         void hold() throws CallStateException {
             setState(State.HOLDING);
-            AudioGroup audioGroup = getAudioGroup();
-            if (audioGroup != null) {
-                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
-            }
             for (Connection c : connections) ((SipConnection) c).hold();
+            setAudioGroupMode();
         }
 
         void unhold() throws CallStateException {
@@ -465,19 +483,19 @@
             for (Connection c : connections) {
                 ((SipConnection) c).unhold(audioGroup);
             }
+            setAudioGroupMode();
         }
 
         void setMute(boolean muted) {
-            AudioGroup audioGroup = getAudioGroup();
-            if (audioGroup == null) return;
-            audioGroup.setMode(
-                    muted ? AudioGroup.MODE_MUTED : AudioGroup.MODE_NORMAL);
+            for (Connection c : connections) {
+                ((SipConnection) c).setMute(muted);
+            }
         }
 
         boolean getMute() {
-            AudioGroup audioGroup = getAudioGroup();
-            if (audioGroup == null) return false;
-            return (audioGroup.getMode() == AudioGroup.MODE_MUTED);
+            return connections.isEmpty()
+                    ? false
+                    : ((SipConnection) connections.get(0)).getMute();
         }
 
         void merge(SipCall that) throws CallStateException {
@@ -736,6 +754,17 @@
             }
         }
 
+        void setMute(boolean muted) {
+            if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) {
+                mSipAudioCall.toggleMute();
+            }
+        }
+
+        boolean getMute() {
+            return (mSipAudioCall == null) ? false
+                                           : mSipAudioCall.isMuted();
+        }
+
         @Override
         protected void setState(Call.State state) {
             if (state == mState) return;
diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp
index 0c8a725..60abf2a 100644
--- a/voip/jni/rtp/AudioGroup.cpp
+++ b/voip/jni/rtp/AudioGroup.cpp
@@ -63,6 +63,14 @@
 // real jitter buffer. For a stream at 8000Hz it takes 8192 bytes. These numbers
 // are chosen by experiments and each of them can be adjusted as needed.
 
+// Originally a stream does not send packets when it is receive-only or there is
+// nothing to mix. However, this causes some problems with certain firewalls and
+// proxies. A firewall might remove a port mapping when there is no outgoing
+// packet for a preiod of time, and a proxy might wait for incoming packets from
+// both sides before start forwarding. To solve these problems, we send out a
+// silence packet on the stream for every second. It should be good enough to
+// keep the stream alive with relatively low resources.
+
 // Other notes:
 // + We use elapsedRealtime() to get the time. Since we use 32bit variables
 //   instead of 64bit ones, comparison must be done by subtraction.
@@ -110,7 +118,7 @@
     int mSampleRate;
     int mSampleCount;
     int mInterval;
-    int mLogThrottle;
+    int mKeepAlive;
 
     int16_t *mBuffer;
     int mBufferMask;
@@ -262,12 +270,8 @@
     ++mSequence;
     mTimestamp += mSampleCount;
 
-    if (mMode == RECEIVE_ONLY) {
-        return;
-    }
-
     // If there is an ongoing DTMF event, send it now.
-    if (mDtmfEvent != -1) {
+    if (mMode != RECEIVE_ONLY && mDtmfEvent != -1) {
         int duration = mTimestamp - mDtmfStart;
         // Make sure duration is reasonable.
         if (duration >= 0 && duration < mSampleRate * 100) {
@@ -289,43 +293,55 @@
         mDtmfEvent = -1;
     }
 
-    // It is time to mix streams.
-    bool mixed = false;
     int32_t buffer[mSampleCount + 3];
-    memset(buffer, 0, sizeof(buffer));
-    while (chain) {
-        if (chain != this &&
-            chain->mix(buffer, tick - mInterval, tick, mSampleRate)) {
-            mixed = true;
+    int16_t samples[mSampleCount];
+    if (mMode == RECEIVE_ONLY) {
+        if ((mTick ^ mKeepAlive) >> 10 == 0) {
+            return;
         }
-        chain = chain->mNext;
-    }
-    if (!mixed) {
-        if ((mTick ^ mLogThrottle) >> 10) {
-            mLogThrottle = mTick;
+        mKeepAlive = mTick;
+        memset(samples, 0, sizeof(samples));
+    } else {
+        // Mix all other streams.
+        bool mixed = false;
+        memset(buffer, 0, sizeof(buffer));
+        while (chain) {
+            if (chain != this &&
+                chain->mix(buffer, tick - mInterval, tick, mSampleRate)) {
+                mixed = true;
+            }
+            chain = chain->mNext;
+        }
+
+        if (mixed) {
+            // Saturate into 16 bits.
+            for (int i = 0; i < mSampleCount; ++i) {
+                int32_t sample = buffer[i];
+                if (sample < -32768) {
+                    sample = -32768;
+                }
+                if (sample > 32767) {
+                    sample = 32767;
+                }
+                samples[i] = sample;
+            }
+        } else {
+            if ((mTick ^ mKeepAlive) >> 10 == 0) {
+                return;
+            }
+            mKeepAlive = mTick;
+            memset(samples, 0, sizeof(samples));
             LOGV("stream[%d] no data", mSocket);
         }
-        return;
     }
 
-    // Cook the packet and send it out.
-    int16_t samples[mSampleCount];
-    for (int i = 0; i < mSampleCount; ++i) {
-        int32_t sample = buffer[i];
-        if (sample < -32768) {
-            sample = -32768;
-        }
-        if (sample > 32767) {
-            sample = 32767;
-        }
-        samples[i] = sample;
-    }
     if (!mCodec) {
         // Special case for device stream.
         send(mSocket, samples, sizeof(samples), MSG_DONTWAIT);
         return;
     }
 
+    // Cook the packet and send it out.
     buffer[0] = htonl(mCodecMagic | mSequence);
     buffer[1] = htonl(mTimestamp);
     buffer[2] = mSsrc;
@@ -883,7 +899,7 @@
     int codecType = -1;
     char codecName[16];
     int sampleRate = -1;
-    sscanf(codecSpec, "%d %[^/]%*c%d", &codecType, codecName, &sampleRate);
+    sscanf(codecSpec, "%d %15[^/]%*c%d", &codecType, codecName, &sampleRate);
     codec = newAudioCodec(codecName);
     int sampleCount = (codec ? codec->set(sampleRate, codecSpec) : -1);
     env->ReleaseStringUTFChars(jCodecSpec, codecSpec);