am b4ce875c: (-s ours) am 74573496: Merge "Fix time and details alignment in RTL." into mnc-dev

* commit 'b4ce875c72d7ffe1a484a4b48c6c620df8ae2428':
diff --git a/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png
new file mode 100644
index 0000000..a0711d3
--- /dev/null
+++ b/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png
Binary files differ
diff --git a/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png
new file mode 100644
index 0000000..65f7329
--- /dev/null
+++ b/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png
Binary files differ
diff --git a/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png
new file mode 100644
index 0000000..7a5df52
--- /dev/null
+++ b/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png
Binary files differ
diff --git a/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png
new file mode 100644
index 0000000..7bd5b16
--- /dev/null
+++ b/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png
Binary files differ
diff --git a/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png
new file mode 100644
index 0000000..428009c
--- /dev/null
+++ b/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png
Binary files differ
diff --git a/InCallUI/res/drawable/subject_bubble.xml b/InCallUI/res/drawable/subject_bubble.xml
new file mode 100644
index 0000000..adab678
--- /dev/null
+++ b/InCallUI/res/drawable/subject_bubble.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#ffffff" />
+    <padding android:left="10dp" android:top="10dp" android:right="10dp" android:bottom="10dp" />
+    <corners android:topLeftRadius="6dp" android:topRightRadius="6dp"
+             android:bottomLeftRadius="0dp" android:bottomRightRadius="6dp"/>
+</shape>
\ No newline at end of file
diff --git a/InCallUI/res/layout/primary_call_info.xml b/InCallUI/res/layout/primary_call_info.xml
index 89943df..8f78440 100644
--- a/InCallUI/res/layout/primary_call_info.xml
+++ b/InCallUI/res/layout/primary_call_info.xml
@@ -33,6 +33,27 @@
     android:animateLayoutChanges="true"
     android:gravity="center">
 
+    <LinearLayout android:id="@+id/callSubjectLayout"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:orientation="horizontal"
+                  android:clipChildren="false"
+                  android:clipToPadding="false">
+
+        <TextView android:id="@+id/callSubject"
+                  android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:textAlignment="viewStart"
+                  android:textAppearance="?android:attr/textAppearanceSmall"
+                  android:textColor="@color/incall_call_banner_background_color"
+                  android:textSize="@dimen/call_label_text_size"
+                  android:background="@drawable/subject_bubble"
+                  android:maxLines="2"
+                  android:ellipsize="end"
+                  android:singleLine="false"
+                  android:visibility="gone" />
+    </LinearLayout>
+
     <LinearLayout android:id="@+id/callStateButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -108,6 +129,15 @@
             android:scaleType="fitCenter"
             android:visibility="gone" />
 
+        <ImageView android:id="@+id/forwardIcon"
+            android:src="@drawable/ic_forward_white_24dp"
+            android:layout_width="24dp"
+            android:layout_height="match_parent"
+            android:layout_marginEnd="8dp"
+            android:tint="@color/incall_call_banner_subtext_color"
+            android:scaleType="fitCenter"
+            android:visibility="gone" />
+
         <!-- Label (like "Mobile" or "Work", if present) and phone number, side by side -->
         <LinearLayout android:id="@+id/labelAndNumber"
             android:layout_width="wrap_content"
diff --git a/InCallUI/res/values/strings.xml b/InCallUI/res/values/strings.xml
index cadef73..e59867c 100644
--- a/InCallUI/res/values/strings.xml
+++ b/InCallUI/res/values/strings.xml
@@ -456,4 +456,12 @@
     <!-- Description of the "camera off" icon displayed when the device's camera is disabled during
          a video call. [CHAR LIMIT=NONE] -->
     <string name="camera_off_description">Camera off</string>
+
+    <!-- Used to inform the user that a call was received via a number other than the primary
+        phone number associated with their device. [CHAR LIMIT=16] -->
+    <string name="child_number">via <xliff:g id="child_number" example="650-555-1212">%s</xliff:g></string>
+
+    <!-- Used to inform the user that the note associated with an outgoing call has been sent.
+         [CHAR LIMIT=32] -->
+    <string name="note_sent">Note sent</string>
 </resources>
diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java
index a5a88ff..fc75bf0 100644
--- a/InCallUI/src/com/android/incallui/AnswerPresenter.java
+++ b/InCallUI/src/com/android/incallui/AnswerPresenter.java
@@ -108,6 +108,11 @@
         }
     }
 
+    @Override
+    public void onLastForwardedNumberChange() {
+        // no-op
+    }
+
     private boolean isVideoUpgradePending(Call call) {
         return call.getSessionModificationState()
                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java
index ee73db2..f08653e 100644
--- a/InCallUI/src/com/android/incallui/Call.java
+++ b/InCallUI/src/com/android/incallui/Call.java
@@ -18,23 +18,26 @@
 
 import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.testing.NeededForTesting;
-import com.android.incallui.CallList.Listener;
 
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Trace;
+import android.telecom.Connection;
 import android.telecom.DisconnectCause;
 import android.telecom.GatewayInfo;
 import android.telecom.InCallService.VideoCall;
+import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * Describes a single call and its state.
@@ -242,6 +245,8 @@
             };
 
     private android.telecom.Call mTelecommCall;
+    private boolean mIsEmergencyCall;
+    private Uri mHandle;
     private final String mId;
     private int mState = State.INVALID;
     private DisconnectCause mDisconnectCause;
@@ -254,6 +259,9 @@
     private int mModifyToVideoState = VideoProfile.STATE_AUDIO_ONLY;
 
     private InCallVideoCallCallback mVideoCallCallback;
+    private String mChildNumber;
+    private String mLastForwardedNumber;
+    private String mCallSubject;
 
     /**
      * Used only to create mock calls for testing
@@ -268,6 +276,7 @@
     public Call(android.telecom.Call telecommCall) {
         mTelecommCall = telecommCall;
         mId = ID_PREFIX + Integer.toString(sIdCounter++);
+
         updateFromTelecommCall();
         mTelecommCall.registerCallback(mTelecomCallCallback);
     }
@@ -314,6 +323,56 @@
                     CallList.getInstance().getCallByTelecommCall(
                             mTelecommCall.getChildren().get(i)).getId());
         }
+
+        Bundle callExtras = mTelecommCall.getDetails().getExtras();
+        if (callExtras != null) {
+            // Child address arrives when the call is first set up, so we do not need to notify the
+            // UI of this.
+            if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
+                String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS);
+                if (!Objects.equals(childNumber, mChildNumber)) {
+                    mChildNumber = childNumber;
+                }
+            }
+
+            // Last forwarded number comes in as an array of strings.  We want to choose the last
+            // item in the array.  The forwarding numbers arrive independently of when the call is
+            // originally set up, so we need to notify the the UI of the change.
+            if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) {
+                ArrayList<String> lastForwardedNumbers =
+                        callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER);
+
+                if (lastForwardedNumbers != null) {
+                    String lastForwardedNumber = null;
+                    if (!lastForwardedNumbers.isEmpty()) {
+                        lastForwardedNumber = lastForwardedNumbers.get(
+                                lastForwardedNumbers.size() - 1);
+                    }
+
+                    if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) {
+                        mLastForwardedNumber = lastForwardedNumber;
+                        CallList.getInstance().onLastForwardedNumberChange(this);
+                    }
+                }
+            }
+
+            // Call subject is present in the extras at the start of call, so we do not need to
+            // notify any other listeners of this.
+            if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) {
+                String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT);
+                if (!Objects.equals(mCallSubject, callSubject)) {
+                    mCallSubject = callSubject;
+                }
+            }
+        }
+
+        // If the handle of the call has changed, update state for the call determining if it is an
+        // emergency call.
+        Uri newHandle = mTelecommCall.getDetails().getHandle();
+        if (!Objects.equals(mHandle, newHandle)) {
+            mHandle = newHandle;
+            updateEmergencyCallState();
+        }
     }
 
     private static int translateState(int state) {
@@ -359,6 +418,10 @@
         return mTelecommCall == null ? null : mTelecommCall.getDetails().getHandle();
     }
 
+    public boolean isEmergencyCall() {
+        return mIsEmergencyCall;
+    }
+
     public int getState() {
         if (mTelecommCall != null && mTelecommCall.getParent() != null) {
             return State.CONFERENCED;
@@ -393,6 +456,27 @@
         return mTelecommCall == null ? null : mTelecommCall.getDetails().getExtras();
     }
 
+    /**
+     * @return The child number for the call, or {@code null} if none specified.
+     */
+    public String getChildNumber() {
+        return mChildNumber;
+    }
+
+    /**
+     * @return The last forwarded number for the call, or {@code null} if none specified.
+     */
+    public String getLastForwardedNumber() {
+        return mLastForwardedNumber;
+    }
+
+    /**
+     * @return The call subject, or {@code null} if none specified.
+     */
+    public String getCallSubject() {
+        return mCallSubject;
+    }
+
     /** Returns call disconnect cause, defined by {@link DisconnectCause}. */
     public DisconnectCause getDisconnectCause() {
         if (mState == State.DISCONNECTED || mState == State.IDLE) {
@@ -506,7 +590,7 @@
     public void setSessionModificationState(int state) {
         if (state == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
             Log.e(this,
-            "setSessionModificationState not to be called for RECEIVED_UPGRADE_TO_VIDEO_REQUEST");
+                    "setSessionModificationState not valid for RECEIVED_UPGRADE_TO_VIDEO_REQUEST");
             return;
         }
 
@@ -519,6 +603,16 @@
         }
     }
 
+    /**
+     * Determines if the call handle is an emergency number or not and caches the result to avoid
+     * repeated calls to isEmergencyNumber.
+     */
+    private void updateEmergencyCallState() {
+        Uri handle = mTelecommCall.getDetails().getHandle();
+        mIsEmergencyCall = PhoneNumberUtils.isEmergencyNumber(
+                handle == null ? "" : handle.getSchemeSpecificPart());
+    }
+
     private void setModifyToVideoState(int newVideoState) {
         mModifyToVideoState = newVideoState;
     }
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index 4121390..dc92ac3 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -47,6 +47,7 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
 import com.android.contacts.common.widget.FloatingActionButtonController;
@@ -117,10 +118,12 @@
     private TextView mCallStateLabel;
     private TextView mCallTypeLabel;
     private ImageView mHdAudioIcon;
+    private ImageView mForwardIcon;
     private View mCallNumberAndLabel;
     private ImageView mPhoto;
     private TextView mElapsedTime;
     private Drawable mPrimaryPhotoDrawable;
+    private TextView mCallSubject;
 
     // Container view that houses the entire primary call card, including the call buttons
     private View mPrimaryCallCardContainer;
@@ -231,6 +234,7 @@
         mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon);
         mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel);
         mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon);
+        mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon);
         mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber);
         mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel);
         mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime);
@@ -281,6 +285,7 @@
 
         mPrimaryName.setElegantTextHeight(false);
         mCallStateLabel.setElegantTextHeight(false);
+        mCallSubject = (TextView) view.findViewById(R.id.callSubject);
     }
 
     @Override
@@ -339,7 +344,7 @@
                 float videoViewTranslation = 0f;
 
                 // Translate the call card to its pre-animation state.
-                if (!mIsLandscape){
+                if (!mIsLandscape) {
                     mPrimaryCallCardContainer.setTranslationY(visible ?
                             -mPrimaryCallCardContainer.getHeight() : 0);
 
@@ -553,7 +558,12 @@
         Log.v(this, "DisconnectCause " + disconnectCause.toString());
         Log.v(this, "gateway " + connectionLabel + gatewayNumber);
 
-        if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText())) {
+        // Check if the call subject is showing -- if it is, we want to bypass showing the call
+        // state.
+        boolean isSubjectShowing = mCallSubject.getVisibility() == View.VISIBLE;
+
+        if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText()) &&
+                !isSubjectShowing) {
             // Nothing to do if the labels are the same
             if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) {
                 mCallStateLabel.clearAnimation();
@@ -562,8 +572,14 @@
             return;
         }
 
-        // Update the call state label and icon.
-        setCallStateLabel(callStateLabel);
+        if (isSubjectShowing) {
+            changeCallStateLabel(null);
+            callStateIcon = null;
+        } else {
+            // Update the call state label and icon.
+            setCallStateLabel(callStateLabel);
+        }
+
         if (!TextUtils.isEmpty(callStateLabel.getCallStateLabel())) {
             if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) {
                 mCallStateLabel.clearAnimation();
@@ -677,6 +693,23 @@
         mInCallMessageLabel.setVisibility(View.VISIBLE);
     }
 
+    /**
+     * Sets and shows the call subject if it is not empty.  Hides the call subject otherwise.
+     *
+     * @param callSubject The call subject.
+     */
+    @Override
+    public void setCallSubject(String callSubject) {
+        boolean showSubject = !TextUtils.isEmpty(callSubject);
+
+        mCallSubject.setVisibility(showSubject ? View.VISIBLE : View.GONE);
+        if (showSubject) {
+            mCallSubject.setText(callSubject);
+        } else {
+            mCallSubject.setText(null);
+        }
+    }
+
     public boolean isAnimating() {
         return mIsAnimating;
     }
@@ -922,6 +955,17 @@
     }
 
     /**
+     * Changes the visibility of the forward icon.
+     *
+     * @param visible {@code true} if the UI should show the forward icon.
+     */
+    @Override
+    public void showForwardIndicator(boolean visible) {
+        mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
+    }
+
+
+    /**
      * Changes the visibility of the "manage conference call" button.
      *
      * @param visible Whether to set the button to be visible or not.
@@ -942,6 +986,16 @@
     }
 
     /**
+     * Determines the current visibility of the call subject.
+     *
+     * @return {@code true} if the subject is visible.
+     */
+    @Override
+    public boolean isCallSubjectVisible() {
+        return mCallSubject.getVisibility() == View.VISIBLE;
+    }
+
+    /**
      * Get the overall InCallUI background colors and apply to call card.
      */
     public void updateColors() {
@@ -959,6 +1013,7 @@
             mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor);
         }
         mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor);
+        mCallSubject.setTextColor(themeColors.mPrimaryColor);
 
         mCurrentThemeColors = themeColors;
     }
@@ -1034,6 +1089,11 @@
         });
     }
 
+    @Override
+    public void showNoteSentToast() {
+        Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show();
+    }
+
     public void onDialpadVisibilityChange(boolean isShown) {
         mIsDialpadShowing = isShown;
         updateFabPosition();
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index 3f9c567..6961e8a 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -66,7 +66,8 @@
     private static final String TAG = CallCardPresenter.class.getSimpleName();
     private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000;
 
-    private final EmergencyCallListener mEmergencyCallListener = ObjectFactory.newEmergencyCallListener();
+    private final EmergencyCallListener mEmergencyCallListener =
+            ObjectFactory.newEmergencyCallListener();
 
     private Call mPrimary;
     private Call mSecondary;
@@ -75,6 +76,7 @@
     private CallTimer mCallTimer;
     private Context mContext;
     private boolean mSpinnerShowing = false;
+    private boolean mHasShownToast = false;
 
     public static class ContactLookupCallback implements ContactInfoCacheCallback {
         private final WeakReference<CallCardPresenter> mCallCardPresenter;
@@ -119,6 +121,12 @@
         // Call may be null if disconnect happened already.
         if (call != null) {
             mPrimary = call;
+            if (shouldShowNoteSentToast(mPrimary)) {
+                final CallCardUi ui = getUi();
+                if (ui != null) {
+                    ui.showNoteSentToast();
+                }
+            }
             CallList.getInstance().addCallUpdateListener(call.getId(), this);
 
             // start processing lookups right away.
@@ -206,16 +214,22 @@
                 Call.areSameNumber(mPrimary, primary));
         final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) &&
                 Call.areSameNumber(mSecondary, secondary));
+        final boolean shouldShowCallSubject = shouldShowCallSubject(mPrimary);
 
         mSecondary = secondary;
         Call previousPrimary = mPrimary;
         mPrimary = primary;
 
+        if (primaryChanged && shouldShowNoteSentToast(primary)) {
+            ui.showNoteSentToast();
+        }
+
         // Refresh primary call information if either:
         // 1. Primary call changed.
         // 2. The call's ability to manage conference has changed.
         if (mPrimary != null && (primaryChanged ||
-                ui.isManageConferenceVisible() != shouldShowManageConference())) {
+                ui.isManageConferenceVisible() != shouldShowManageConference()) ||
+                ui.isCallSubjectVisible() != shouldShowCallSubject) {
             // primary call has changed
             if (previousPrimary != null) {
                 CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
@@ -320,6 +334,19 @@
         updatePrimaryCallState();
     }
 
+    /**
+     * Handles a change to the last forwarding number by refreshing the primary call info.
+     */
+    @Override
+    public void onLastForwardedNumberChange() {
+        Log.v(this, "onLastForwardedNumberChange");
+
+        if (mPrimary == null) {
+            return;
+        }
+        updatePrimaryDisplayInfo();
+    }
+
     private String getSubscriptionNumber() {
         // If it's an emergency call, and they're not populating the callback number,
         // then try to fall back to the phone sub info (to hopefully get the SIM's
@@ -348,15 +375,23 @@
                     mPrimary.hasProperty(Details.PROPERTY_WIFI),
                     mPrimary.isConferenceCall());
 
-            boolean showHdAudioIndicator =
-                    isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
-            getUi().showHdAudioIndicator(showHdAudioIndicator);
-
+            maybeShowHdAudioIcon();
             setCallbackNumber();
         }
     }
 
     /**
+     * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO},
+     * except if the call has a last forwarded number (we will show that icon instead).
+     */
+    private void maybeShowHdAudioIcon() {
+        boolean showHdAudioIndicator =
+                isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) &&
+                TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
+        getUi().showHdAudioIndicator(showHdAudioIndicator);
+    }
+
+    /**
      * Only show the conference call button if we can manage the conference.
      */
     private void maybeShowManageConferenceCallButton() {
@@ -402,11 +437,9 @@
         // 1. This is an emergency call.
         // 2. The phone is in Emergency Callback Mode, which means we should show the callback
         //    number.
-        boolean isEmergencyCall = PhoneNumberUtils.isEmergencyNumber(
-                getNumberFromHandle(mPrimary.getHandle()));
         boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
 
-        if (isEmergencyCall || showCallbackNumber) {
+        if (mPrimary.isEmergencyCall() || showCallbackNumber) {
             callbackNumber = getSubscriptionNumber();
         } else {
             StatusHints statusHints = mPrimary.getTelecommCall().getDetails().getStatusHints();
@@ -426,7 +459,7 @@
             callbackNumber = null;
         }
 
-        getUi().setCallbackNumber(callbackNumber, isEmergencyCall || showCallbackNumber);
+        getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber);
     }
 
     public void updateCallTime() {
@@ -582,13 +615,38 @@
             Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo);
 
             String name = getNameForCall(mPrimaryContactInfo);
-            String number = getNumberForCall(mPrimaryContactInfo);
+            String number;
+
+            boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
+            boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
+            boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
+
+            if (isCallSubjectShown) {
+                ui.setCallSubject(mPrimary.getCallSubject());
+            } else {
+                ui.setCallSubject(null);
+            }
+
+            if (isCallSubjectShown) {
+                number = null;
+            } else if (isChildNumberShown) {
+                number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
+            } else if (isForwardedNumberShown) {
+                // Use last forwarded number instead of second line, if present.
+                number = mPrimary.getLastForwardedNumber();
+            } else {
+                number = getNumberForCall(mPrimaryContactInfo);
+            }
+
+            ui.showForwardIndicator(isForwardedNumberShown);
+            maybeShowHdAudioIcon();
+
             boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
             ui.setPrimary(
                     number,
                     name,
                     nameIsNumber,
-                    mPrimaryContactInfo.label,
+                    isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
                     mPrimaryContactInfo.photo,
                     mPrimaryContactInfo.isSipCall);
         } else {
@@ -597,8 +655,7 @@
         }
 
         if (mEmergencyCallListener != null) {
-            boolean isEmergencyCall = PhoneNumberUtils.isEmergencyNumber(
-                    getNumberFromHandle(mPrimary.getHandle()));
+            boolean isEmergencyCall = mPrimary.isEmergencyCall();
             mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall);
         }
     }
@@ -848,6 +905,38 @@
         }
     }
 
+    /**
+     * Determines whether the call subject should be visible on the UI.  For the call subject to be
+     * visible, the call has to be in an incoming or waiting state, and the subject must not be
+     * empty.
+     *
+     * @param call The call.
+     * @return {@code true} if the subject should be shown, {@code false} otherwise.
+     */
+    private boolean shouldShowCallSubject(Call call) {
+        if (call == null) {
+            return false;
+        }
+
+        boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING ||
+                mPrimary.getState() == Call.State.CALL_WAITING;
+        return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject());
+    }
+
+    /**
+     * Determines whether the "note sent" toast should be shown.  It should be shown for a new
+     * outgoing call with a subject.
+     *
+     * @param call The call
+     * @return {@code true} if the toast should be shown, {@code false} otherwise.
+     */
+    private boolean shouldShowNoteSentToast(Call call) {
+        return call != null && !TextUtils
+                .isEmpty(call.getTelecommCall().getDetails().getIntentExtras().getString(
+                        TelecomManager.EXTRA_CALL_SUBJECT)) &&
+                (call.getState() == Call.State.DIALING || call.getState() == Call.State.CONNECTING);
+    }
+
     public interface CallCardUi extends Ui {
         void setVisible(boolean on);
         void setCallCardVisible(boolean visible);
@@ -866,11 +955,15 @@
         void setPrimaryLabel(String label);
         void setEndCallButtonEnabled(boolean enabled, boolean animate);
         void setCallbackNumber(String number, boolean isEmergencyCalls);
+        void setCallSubject(String callSubject);
         void setProgressSpinnerVisible(boolean visible);
         void showHdAudioIndicator(boolean visible);
+        void showForwardIndicator(boolean visible);
         void showManageConferenceCallButton(boolean visible);
         boolean isManageConferenceVisible();
+        boolean isCallSubjectVisible();
         void animateForNewOutgoingCall();
         void sendAccessibilityAnnouncement();
+        void showNoteSentToast();
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java
index c0014bd..666ba95 100644
--- a/InCallUI/src/com/android/incallui/CallList.java
+++ b/InCallUI/src/com/android/incallui/CallList.java
@@ -160,6 +160,22 @@
         }
     }
 
+    /**
+     * Called when the last forwarded number changes for a call.  With IMS, the last forwarded
+     * number changes due to a supplemental service notification, so it is not pressent at the
+     * start of the call.
+     *
+     * @param call The call.
+     */
+    public void onLastForwardedNumberChange(Call call) {
+        final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
+        if (listeners != null) {
+            for (CallUpdateListener listener : listeners) {
+                listener.onLastForwardedNumberChange();
+            }
+        }
+    }
+
     public void notifyCallUpdateListeners(Call call) {
         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
         if (listeners != null) {
@@ -611,5 +627,10 @@
          * @param sessionModificationState The new session modification state.
          */
         public void onSessionModificationStateChange(int sessionModificationState);
+
+        /**
+         * Notifies of a change to the last forwarded number for a call.
+         */
+        public void onLastForwardedNumberChange();
     }
 }
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index acb11b5..c6892b2 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -871,7 +871,11 @@
         for (int i=0; i<tasks.size(); i++) {
             ActivityManager.AppTask task = tasks.get(i);
             if (task.getTaskInfo().id == taskId) {
-                task.setExcludeFromRecents(exclude);
+                try {
+                    task.setExcludeFromRecents(exclude);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "RuntimeException when excluding task from recents.", e);
+                }
             }
         }
     }
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index ff1a50f..2d7d40a 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -120,28 +120,34 @@
     private final android.telecom.Call.Callback mCallCallback =
             new android.telecom.Call.Callback() {
         @Override
-        public void onPostDialWait(android.telecom.Call call, String remainingPostDialSequence) {
-            onPostDialCharWait(mCallList.getCallByTelecommCall(call).getId(),
-                    remainingPostDialSequence);
+        public void onPostDialWait(android.telecom.Call telecomCall,
+                String remainingPostDialSequence) {
+            final Call call = mCallList.getCallByTelecommCall(telecomCall);
+            if (call == null) {
+                Log.w(this, "Call not found in call list: " + telecomCall);
+                return;
+            }
+            onPostDialCharWait(call.getId(), remainingPostDialSequence);
         }
 
         @Override
-        public void onDetailsChanged(android.telecom.Call call,
+        public void onDetailsChanged(android.telecom.Call telecomCall,
                 android.telecom.Call.Details details) {
+            final Call call = mCallList.getCallByTelecommCall(telecomCall);
+            if (call == null) {
+                Log.w(this, "Call not found in call list: " + telecomCall);
+                return;
+            }
             for (InCallDetailsListener listener : mDetailsListeners) {
-                listener.onDetailsChanged(mCallList.getCallByTelecommCall(call),
-                        details);
+                listener.onDetailsChanged(call, details);
             }
         }
 
         @Override
-        public void onConferenceableCallsChanged(
-                android.telecom.Call call, List<android.telecom.Call> conferenceableCalls) {
-            Log.i(this, "onConferenceableCallsChanged: " + call);
-            for (InCallDetailsListener listener : mDetailsListeners) {
-                listener.onDetailsChanged(mCallList.getCallByTelecommCall(call),
-                        call.getDetails());
-            }
+        public void onConferenceableCallsChanged(android.telecom.Call telecomCall,
+                List<android.telecom.Call> conferenceableCalls) {
+            Log.i(this, "onConferenceableCallsChanged: " + telecomCall);
+            onDetailsChanged(telecomCall, telecomCall.getDetails());
         }
     };
 
@@ -1132,7 +1138,7 @@
      * call.
      */
     public static boolean isCallWithNoValidAccounts(Call call) {
-        if (call != null && !isEmergencyCall(call)) {
+        if (call != null && !call.isEmergencyCall()) {
             Bundle extras = call.getIntentExtras();
 
             if (extras == null) {
@@ -1151,14 +1157,6 @@
         return false;
     }
 
-    private static boolean isEmergencyCall(Call call) {
-        final Uri handle = call.getHandle();
-        if (handle == null) {
-            return false;
-        }
-        return PhoneNumberUtils.isEmergencyNumber(handle.getSchemeSpecificPart());
-    }
-
     /**
      * Sets the DisconnectCause for a call that was disconnected because it was missing a
      * PhoneAccount or PhoneAccounts to select from.
diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
index 8df4520..e583434 100644
--- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java
+++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java
@@ -40,6 +40,8 @@
 
 import com.google.common.base.Preconditions;
 
+import java.util.Objects;
+
 /**
  * This class adds Notifications to the status bar for the in-call experience.
  */
@@ -60,7 +62,7 @@
     private int mCurrentNotification = NOTIFICATION_NONE;
     private int mCallState = Call.State.INVALID;
     private int mSavedIcon = 0;
-    private int mSavedContent = 0;
+    private String mSavedContent = null;
     private Bitmap mSavedLargeIcon;
     private String mSavedContentTitle;
     private String mCallId = null;
@@ -206,7 +208,7 @@
         // Check if data has changed; if nothing is different, don't issue another notification.
         final int iconResId = getIconToDisplay(call);
         Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
-        final int contentResId = getContentString(call);
+        final String content = getContentString(call);
         final String contentTitle = getContentTitle(contactInfo, call);
 
         final int notificationType;
@@ -218,7 +220,7 @@
             notificationType = NOTIFICATION_IN_CALL;
         }
 
-        if (!checkForChangeAndSaveData(iconResId, contentResId, largeIcon, contentTitle, state,
+        if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, state,
                 notificationType)) {
             return;
         }
@@ -244,7 +246,7 @@
         }
 
         // Set the content
-        builder.setContentText(mContext.getString(contentResId));
+        builder.setContentText(content);
         builder.setSmallIcon(iconResId);
         builder.setContentTitle(contentTitle);
         builder.setLargeIcon(largeIcon);
@@ -306,7 +308,7 @@
      * are already displaying. If the data is exactly the same, we return false so that
      * we do not issue a new notification for the exact same data.
      */
-    private boolean checkForChangeAndSaveData(int icon, int content, Bitmap largeIcon,
+    private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon,
             String contentTitle, int state, int notificationType) {
 
         // The two are different:
@@ -317,7 +319,7 @@
                 (contentTitle == null && mSavedContentTitle != null);
 
         // any change means we are definitely updating
-        boolean retval = (mSavedIcon != icon) || (mSavedContent != content) ||
+        boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content) ||
                 (mCallState != state) || (mSavedLargeIcon != largeIcon) ||
                 contentTitleChanged;
 
@@ -419,13 +421,20 @@
     /**
      * Returns the message to use with the notification.
      */
-    private int getContentString(Call call) {
+    private String getContentString(Call call) {
+        boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING ||
+                call.getState() == Call.State.CALL_WAITING;
+
+        if (isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject())) {
+            return call.getCallSubject();
+        }
+
         int resId = R.string.notification_ongoing_call;
         if (call.hasProperty(Details.PROPERTY_WIFI)) {
             resId = R.string.notification_ongoing_call_wifi;
         }
 
-        if (call.getState() == Call.State.INCOMING || call.getState() == Call.State.CALL_WAITING) {
+        if (isIncomingOrWaiting) {
             if (call.hasProperty(Details.PROPERTY_WIFI)) {
                 resId = R.string.notification_incoming_call_wifi;
             } else {
@@ -440,7 +449,7 @@
             resId = R.string.notification_requesting_video_call;
         }
 
-        return resId;
+        return mContext.getString(resId);
     }
 
     /**
@@ -637,4 +646,9 @@
             updateNotification(mInCallState, CallList.getInstance());
         }
     }
+
+    @Override
+    public void onLastForwardedNumberChange() {
+        // no-op
+    }
 }