Merge "Fix logging in NewOutgoingCallIntentBroadcaster" into master-nova
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d58c898..86641ee 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -25,8 +25,10 @@
     <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
     <uses-permission android:name="android.permission.READ_CALL_LOG" />
     <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
 
-    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"></uses-permission>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <!-- Declare which SDK level this application was built against. This is needed so that IDEs
          can check for incompatible APIs. -->
@@ -164,5 +166,13 @@
         <!-- Selects call services to place emergency calls. -->
         <service android:name="EmergencyCallServiceSelector" android:exported="false" />
 
+        <receiver android:name="TelecommBroadcastReceiver" android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.telecomm.ACTION_CALL_BACK_FROM_NOTIFICATION" />
+                <action android:name="com.android.telecomm.ACTION_CALL_BACK_FROM_NOTIFICATION" />
+                <action android:name="com.android.telecomm.ACTION_SEND_SMS_FROM_NOTIFICATION" />
+            </intent-filter>
+        </receiver>
+
     </application>
 </manifest>
diff --git a/res/drawable-hdpi/ic_text_holo_dark.png b/res/drawable-hdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..6d21e42
--- /dev/null
+++ b/res/drawable-hdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/stat_sys_phone_call.png b/res/drawable-hdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..7eda84c
--- /dev/null
+++ b/res/drawable-hdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/drawable-ldrtl-hdpi/ic_text_holo_dark.png b/res/drawable-ldrtl-hdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..b99073e
--- /dev/null
+++ b/res/drawable-ldrtl-hdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png b/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..e0f33f8
--- /dev/null
+++ b/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/drawable-ldrtl-mdpi/ic_text_holo_dark.png b/res/drawable-ldrtl-mdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..4106c32
--- /dev/null
+++ b/res/drawable-ldrtl-mdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png b/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..d771d87
--- /dev/null
+++ b/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/drawable-ldrtl-xhdpi/ic_text_holo_dark.png b/res/drawable-ldrtl-xhdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..99e7e38
--- /dev/null
+++ b/res/drawable-ldrtl-xhdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png b/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..86af9c2
--- /dev/null
+++ b/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_text_holo_dark.png b/res/drawable-mdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..80b95ee
--- /dev/null
+++ b/res/drawable-mdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/stat_sys_phone_call.png b/res/drawable-mdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..70a4bbe
--- /dev/null
+++ b/res/drawable-mdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_text_holo_dark.png b/res/drawable-xhdpi/ic_text_holo_dark.png
new file mode 100644
index 0000000..e80a042
--- /dev/null
+++ b/res/drawable-xhdpi/ic_text_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/stat_sys_phone_call.png b/res/drawable-xhdpi/stat_sys_phone_call.png
new file mode 100644
index 0000000..1bb4340
--- /dev/null
+++ b/res/drawable-xhdpi/stat_sys_phone_call.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index acae2b6..42cda0e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -18,4 +18,26 @@
     <!-- Official label of the Telecomm app, as seen in "Manage Applications"
          and other settings UIs. -->
     <string name="telecommAppLabel" product="default">Telecomm</string>
+
+    <!-- Name for an "unknown" caller. -->
+    <string name="unknown">Unknown</string>
+
+    <!-- Notification strings -->
+    <!-- Missed call notification label, used when there's exactly one missed call -->
+    <string name="notification_missedCallTitle">Missed call</string>
+    <!-- Missed call notification label, used when there are two or more missed calls -->
+    <string name="notification_missedCallsTitle">Missed calls</string>
+    <!-- Missed call notification message used when there are multiple missed calls -->
+    <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+    <!-- Missed call notification message used for a single missed call, including
+         the caller-id info from the missed call -->
+    <string name="notification_missedCallTicker">Missed call from <xliff:g id="missed_call_from">%s</xliff:g></string>
+    <!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
+         The user will be able to call back to the person or the phone number.
+         [CHAR LIMIT=60] -->
+    <string name="notification_missedCall_call_back">Call back</string>
+    <!-- Message for "reply via sms" action, which is displayed in the missed call notification.
+         The user will be able to send text messages using the phone number.
+         [CHAR LIMIT=60] -->
+    <string name="notification_missedCall_message">Message</string>
 </resources>
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index f79a362..0eef378 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -137,7 +137,7 @@
     @Override public String toString() {
         return String.format(Locale.US, "[%s, %s, %s]", mState,
                 mCallService == null ? "<null>" : mCallService.getComponentName(),
-                Log.pii(mHandle));
+                Log.piiHandle(mHandle));
     }
 
     CallState getState() {
@@ -220,7 +220,7 @@
      * @return The "age" of this call object in milliseconds, which typically also represents the
      *     period since this call was added to the set pending outgoing calls, see mCreationTime.
      */
-    long getAgeInMilliseconds() {
+    long getAgeMs() {
         return new Date().getTime() - mCreationTime.getTime();
     }
 
@@ -228,7 +228,7 @@
      * @return The time when this call object was created and added to the set of pending outgoing
      *     calls.
      */
-    long getCreationTimeInMilliseconds() {
+    long getCreationTimeMs() {
         return mCreationTime.getTime();
     }
 
diff --git a/src/com/android/telecomm/CallActivity.java b/src/com/android/telecomm/CallActivity.java
index 77e64bc..2868216 100644
--- a/src/com/android/telecomm/CallActivity.java
+++ b/src/com/android/telecomm/CallActivity.java
@@ -73,6 +73,8 @@
     private void processIntent(Intent intent) {
         String action = intent.getAction();
 
+        // TODO: Check for non-voice capable devices before reading any intents.
+
         if (Intent.ACTION_CALL.equals(action) ||
                 Intent.ACTION_CALL_PRIVILEGED.equals(action) ||
                 Intent.ACTION_CALL_EMERGENCY.equals(action)) {
diff --git a/src/com/android/telecomm/CallLogManager.java b/src/com/android/telecomm/CallLogManager.java
index 99eb845..8bdc150 100644
--- a/src/com/android/telecomm/CallLogManager.java
+++ b/src/com/android/telecomm/CallLogManager.java
@@ -101,8 +101,8 @@
      *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
      */
     private void logCall(Call call, int callLogType) {
-        final long creationTime = call.getCreationTimeInMilliseconds();
-        final long age = call.getAgeInMilliseconds();
+        final long creationTime = call.getCreationTimeMs();
+        final long age = call.getAgeMs();
 
         final ContactInfo contactInfo = call.getContactInfo();  // May be null.
         final String logNumber = getLogNumber(call);
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index a302c8d..b8724e3 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -92,16 +92,20 @@
      * Initializes the required Telecomm components.
      */
     private CallsManager() {
-        mSwitchboard = new Switchboard(this);
+        TelecommApp app = TelecommApp.getInstance();
 
+        mSwitchboard = new Switchboard(this);
         mCallAudioManager = new CallAudioManager();
 
-        mListeners.add(new CallLogManager(TelecommApp.getInstance()));
+        InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
+        mListeners.add(new CallLogManager(app));
         mListeners.add(new PhoneStateBroadcaster());
         mListeners.add(new InCallController());
         mListeners.add(new Ringer(mCallAudioManager));
-        mListeners.add(new RingbackPlayer(this, new InCallTonePlayer.Factory(mCallAudioManager)));
+        mListeners.add(new RingbackPlayer(this, playerFactory));
+        mListeners.add(new InCallToneMonitor(playerFactory, this));
         mListeners.add(mCallAudioManager);
+        mListeners.add(app.getMissedCallNotifier());
     }
 
     static CallsManager getInstance() {
diff --git a/src/com/android/telecomm/InCallToneMonitor.java b/src/com/android/telecomm/InCallToneMonitor.java
new file mode 100644
index 0000000..bafbfcc
--- /dev/null
+++ b/src/com/android/telecomm/InCallToneMonitor.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.telecomm.CallAudioState;
+import android.telecomm.CallState;
+import android.telephony.DisconnectCause;
+
+import java.util.Collection;
+
+/**
+ * Monitors events from CallsManager and plays in-call tones for events which require them, such as
+ * different type of call disconnections (busy tone, congestion tone, etc).
+ */
+public final class InCallToneMonitor extends CallsManagerListenerBase {
+    private final InCallTonePlayer.Factory mPlayerFactory;
+
+    private final CallsManager mCallsManager;
+
+    InCallToneMonitor(InCallTonePlayer.Factory playerFactory, CallsManager callsManager) {
+        mPlayerFactory = playerFactory;
+        mCallsManager = callsManager;
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        if (mCallsManager.getForegroundCall() != call) {
+            // We only play tones for foreground calls.
+            return;
+        }
+
+        if (newState == CallState.DISCONNECTED) {
+            int toneToPlay = InCallTonePlayer.TONE_INVALID;
+
+            Log.v(this, "Disconnect cause: %d.", call.getDisconnectCause());
+
+            switch(call.getDisconnectCause()) {
+                case DisconnectCause.BUSY:
+                    toneToPlay = InCallTonePlayer.TONE_BUSY;
+                    break;
+                case DisconnectCause.CONGESTION:
+                    toneToPlay = InCallTonePlayer.TONE_CONGESTION;
+                    break;
+                case DisconnectCause.CDMA_REORDER:
+                    toneToPlay = InCallTonePlayer.TONE_REORDER;
+                    break;
+                case DisconnectCause.CDMA_INTERCEPT:
+                    toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
+                    break;
+                case DisconnectCause.CDMA_DROP:
+                    toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
+                    break;
+                case DisconnectCause.OUT_OF_SERVICE:
+                    toneToPlay = InCallTonePlayer.TONE_OUT_OF_SERVICE;
+                    break;
+                case DisconnectCause.UNOBTAINABLE_NUMBER:
+                    toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
+                    break;
+                case DisconnectCause.ERROR_UNSPECIFIED:
+                    toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+                    break;
+                case DisconnectCause.NORMAL:
+                case DisconnectCause.LOCAL:
+                    // Only play the disconnect sound on normal disconnects if there are no other
+                    // calls present beyond the one that is currently disconnected.
+                    Collection<Call> allCalls = mCallsManager.getCalls();
+                    if (allCalls.size() == 1) {
+                        if (!allCalls.contains(call)) {
+                            Log.wtf(this, "Disconnecting call not found %s.", call);
+                        }
+                        toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+                    }
+                    break;
+            }
+
+            Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
+
+            if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
+                mPlayerFactory.createPlayer(toneToPlay).startTone();
+            }
+        }
+    }
+}
diff --git a/src/com/android/telecomm/InCallTonePlayer.java b/src/com/android/telecomm/InCallTonePlayer.java
index c00e1cf..58e0423 100644
--- a/src/com/android/telecomm/InCallTonePlayer.java
+++ b/src/com/android/telecomm/InCallTonePlayer.java
@@ -44,10 +44,21 @@
     }
 
     // The possible tones that we can play.
-    public static final int TONE_NONE = 0;
-    public static final int TONE_RING_BACK = 1;
+    public static final int TONE_INVALID = 0;
+    public static final int TONE_BUSY = 1;
+    public static final int TONE_CALL_ENDED = 2;
+    public static final int TONE_OTA_CALL_ENDED = 3;
+    public static final int TONE_CALL_WAITING = 4;
+    public static final int TONE_CDMA_DROP = 5;
+    public static final int TONE_CONGESTION = 6;
+    public static final int TONE_INTERCEPT = 7;
+    public static final int TONE_OUT_OF_SERVICE = 8;
+    public static final int TONE_REDIAL = 9;
+    public static final int TONE_REORDER = 10;
+    public static final int TONE_RING_BACK = 11;
+    public static final int TONE_UNOBTAINABLE_NUMBER = 12;
+    public static final int TONE_VOICE_PRIVACY = 13;
 
-    // The tone volume relative to other sounds in the stream.
     private static final int RELATIVE_VOLUME_EMERGENCY = 100;
     private static final int RELATIVE_VOLUME_HIPRI = 80;
     private static final int RELATIVE_VOLUME_LOPRI = 50;
@@ -101,11 +112,66 @@
             final int toneLengthMs;
 
             switch (mToneId) {
+                case TONE_BUSY:
+                    // TODO: CDMA-specific tones
+                    toneType = ToneGenerator.TONE_SUP_BUSY;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = 4000;
+                    break;
+                case TONE_CALL_ENDED:
+                    toneType = ToneGenerator.TONE_PROP_PROMPT;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = 4000;
+                    break;
+                case TONE_OTA_CALL_ENDED:
+                    // TODO: fill in
+                    throw new IllegalStateException("OTA Call ended NYI.");
+                case TONE_CALL_WAITING:
+                    // TODO: fill in.
+                    throw new IllegalStateException("Call waiting NYI.");
+                case TONE_CDMA_DROP:
+                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMs = 375;
+                    break;
+                case TONE_CONGESTION:
+                    toneType = ToneGenerator.TONE_SUP_CONGESTION;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = 4000;
+                    break;
+                case TONE_INTERCEPT:
+                    toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMs = 500;
+                    break;
+                case TONE_OUT_OF_SERVICE:
+                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMs = 375;
+                    break;
+                case TONE_REDIAL:
+                    toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMs = 5000;
+                    break;
+                case TONE_REORDER:
+                    toneType = ToneGenerator.TONE_CDMA_REORDER;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = 5000;
+                    break;
                 case TONE_RING_BACK:
                     toneType = ToneGenerator.TONE_SUP_RINGTONE;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
                     toneLengthMs = Integer.MAX_VALUE - TIMEOUT_BUFFER_MS;
                     break;
+                case TONE_UNOBTAINABLE_NUMBER:
+                    toneType = ToneGenerator.TONE_SUP_ERROR;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = 4000;
+                    break;
+                case TONE_VOICE_PRIVACY:
+                    // TODO: fill in.
+                    throw new IllegalStateException("Voice privacy tone NYI.");
                 default:
                     throw new IllegalStateException("Bad toneId: " + mToneId);
             }
diff --git a/src/com/android/telecomm/Log.java b/src/com/android/telecomm/Log.java
index d63f23c..b25aec1 100644
--- a/src/com/android/telecomm/Log.java
+++ b/src/com/android/telecomm/Log.java
@@ -16,6 +16,9 @@
 
 package com.android.telecomm;
 
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.IllegalFormatException;
@@ -122,6 +125,34 @@
         android.util.Log.wtf(TAG, msg, new IllegalStateException(msg));
     }
 
+    public static String piiHandle(Object pii) {
+        if (pii == null || VERBOSE) {
+            return String.valueOf(pii);
+        }
+
+        if (pii instanceof Uri) {
+            Uri uri = (Uri) pii;
+
+            // All Uri's which are not "tel" go through normal pii() method.
+            if (!"tel".equals(uri.getScheme())) {
+                return pii(pii);
+            } else {
+                pii = uri.getSchemeSpecificPart();
+            }
+        }
+
+        String originalString = String.valueOf(pii);
+        StringBuilder stringBuilder = new StringBuilder(originalString.length());
+        for (char c : originalString.toCharArray()) {
+            if (PhoneNumberUtils.isDialable(c)) {
+                stringBuilder.append('*');
+            } else {
+                stringBuilder.append(c);
+            }
+        }
+        return stringBuilder.toString();
+    }
+
     /**
      * Redact personally identifiable information for production users.
      * If we are running in verbose mode, return the original string, otherwise
diff --git a/src/com/android/telecomm/MissedCallNotifier.java b/src/com/android/telecomm/MissedCallNotifier.java
new file mode 100644
index 0000000..8100472
--- /dev/null
+++ b/src/com/android/telecomm/MissedCallNotifier.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.telecomm.CallState;
+import android.telephony.DisconnectCause;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ * TODO(santoscordon): Make TelephonyManager.clearMissedCalls call into this class.
+ * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
+ */
+class MissedCallNotifier extends CallsManagerListenerBase {
+
+    private static final int MISSED_CALL_NOTIFICATION_ID = 1;
+    private static final String SCHEME_SMSTO = "smsto";
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+
+    // Used to track the number of missed calls.
+    private int mMissedCallCount = 0;
+
+    MissedCallNotifier(Context context) {
+        mContext = context;
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
+                call.getDisconnectCause() == DisconnectCause.INCOMING_MISSED) {
+            showMissedCallNotification(call);
+        }
+    }
+
+    /** Clears missed call notification and marks the call log's missed calls as read. */
+    void clearMissedCalls() {
+        // Clear the list of new missed calls from the call log.
+        ContentValues values = new ContentValues();
+        values.put(Calls.NEW, 0);
+        values.put(Calls.IS_READ, 1);
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.NEW);
+        where.append(" = 1 AND ");
+        where.append(Calls.TYPE);
+        where.append(" = ?");
+        mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
+                new String[]{ Integer.toString(Calls.MISSED_TYPE) });
+
+        cancelMissedCallNotification();
+    }
+
+    /**
+     * Create a system notification for the missed call.
+     *
+     * @param call The missed call.
+     */
+    private void showMissedCallNotification(Call call) {
+        mMissedCallCount++;
+
+        final int titleResId;
+        final String expandedText;  // The text in the notification's line 1 and 2.
+
+        // Display the first line of the notification:
+        // 1 missed call: <caller name || handle>
+        // More than 1 missed call: <number of calls> + "missed calls"
+        if (mMissedCallCount == 1) {
+            titleResId = R.string.notification_missedCallTitle;
+            expandedText = getNameForCall(call);
+        } else {
+            titleResId = R.string.notification_missedCallsTitle;
+            expandedText =
+                    mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
+        }
+
+        // Create the notification.
+        Notification.Builder builder = new Notification.Builder(mContext);
+        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+                .setWhen(call.getCreationTimeMs())
+                .setContentTitle(mContext.getText(titleResId))
+                .setContentText(expandedText)
+                .setContentIntent(createCallLogPendingIntent())
+                .setAutoCancel(true)
+                .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+        Uri handleUri = call.getHandle();
+        String handle = handleUri.getSchemeSpecificPart();
+
+        // Add additional actions when there is only 1 missed call, like call-back and SMS.
+        if (mMissedCallCount == 1) {
+            Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));
+
+            builder.addAction(R.drawable.stat_sys_phone_call,
+                    mContext.getString(R.string.notification_missedCall_call_back),
+                    createCallBackPendingIntent(handleUri));
+
+            builder.addAction(R.drawable.ic_text_holo_dark,
+                    mContext.getString(R.string.notification_missedCall_message),
+                    createSendSmsFromNotificationPendingIntent(handleUri));
+
+            // TODO(santoscordon): Add photo for contact.
+        } else {
+            Log.d(this, "Suppress actions. handle: %s, missedCalls: %s." + Log.piiHandle(handle),
+                    mMissedCallCount);
+        }
+
+        Notification notification = builder.build();
+        configureLedOnNotification(notification);
+        mNotificationManager.notify(MISSED_CALL_NOTIFICATION_ID, notification);
+    }
+
+    /** Cancels the "missed call" notification. */
+    private void cancelMissedCallNotification() {
+        // Reset the number of missed calls to 0.
+        mMissedCallCount = 0;
+        mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID);
+    }
+
+    /**
+     * Returns the name to use in the missed call notification.
+     */
+    private String getNameForCall(Call call) {
+        // TODO(santoscordon): Get detailed caller information.
+
+        String handle = call.getHandle().getSchemeSpecificPart();
+        if (!TextUtils.isEmpty(handle)) {
+            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
+            // content of the rest of the notification.
+            // TODO(santoscordon): Does this apply to SIP addresses?
+            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+            return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR);
+        } else {
+            // Use "unknown" if the call is unidentifiable.
+            return mContext.getString(R.string.unknown);
+        }
+    }
+
+    /**
+     * Creates a new pending intent that sends the user to the call log.
+     *
+     * @return The pending intent.
+     */
+    private PendingIntent createCallLogPendingIntent() {
+        Intent intent = new Intent(Intent.ACTION_VIEW, null);
+        intent.setType(CallLog.Calls.CONTENT_TYPE);
+
+        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
+        taskStackBuilder.addNextIntent(intent);
+
+        return taskStackBuilder.getPendingIntent(0, 0);
+    }
+
+    /**
+     * Creates an intent to be invoked when the missed call notification is cleared.
+     */
+    private PendingIntent createClearMissedCallsPendingIntent() {
+        return createTelecommPendingIntent(
+                TelecommBroadcastReceiver.ACTION_CLEAR_MISSED_CALLS, null);
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "call back" from the missed call
+     * notification.
+     *
+     * @param handle The handle to call back.
+     */
+    private PendingIntent createCallBackPendingIntent(Uri handle) {
+        return createTelecommPendingIntent(
+                TelecommBroadcastReceiver.ACTION_CALL_BACK_FROM_NOTIFICATION, handle);
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
+     * notification.
+     */
+    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) {
+        return createTelecommPendingIntent(
+                TelecommBroadcastReceiver.ACTION_SEND_SMS_FROM_NOTIFICATION,
+                Uri.fromParts(SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
+    }
+
+    /**
+     * Creates generic pending intent from the specified parameters to be received by
+     * {@link TelecommBroadcastReceiver}.
+     *
+     * @param action The intent action.
+     * @param data The intent data.
+     */
+    private PendingIntent createTelecommPendingIntent(String action, Uri data) {
+        Intent intent = new Intent(action, data, mContext, TelecommBroadcastReceiver.class);
+        return PendingIntent.getBroadcast(mContext, 0, intent, 0);
+    }
+
+    /**
+     * Configures a notification to emit the blinky notification light.
+     */
+    private void configureLedOnNotification(Notification notification) {
+        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+        notification.defaults |= Notification.DEFAULT_LIGHTS;
+    }
+}
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
index bb32cc3..fdc9349 100644
--- a/src/com/android/telecomm/Ringer.java
+++ b/src/com/android/telecomm/Ringer.java
@@ -18,6 +18,9 @@
 
 import android.content.Context;
 import android.media.AudioManager;
+import android.os.SystemVibrator;
+import android.os.Vibrator;
+import android.provider.Settings;
 import android.telecomm.CallState;
 
 import com.google.common.collect.Lists;
@@ -28,6 +31,15 @@
  * Controls the ringtone player.
  */
 final class Ringer extends CallsManagerListenerBase {
+    private static final long[] VIBRATION_PATTERN = new long[] {
+        0, // No delay before starting
+        1000, // How long to vibrate
+        1000, // How long to wait before vibrating again
+    };
+
+    /** Indicate that we want the pattern to repeat at the step which turns on vibration. */
+    private static final int VIBRATION_PATTERN_REPEAT = 1;
+
     private final AsyncRingtonePlayer mRingtonePlayer = new AsyncRingtonePlayer();
 
     /**
@@ -38,8 +50,19 @@
 
     private final CallAudioManager mCallAudioManager;
 
+    private final Vibrator mVibrator;
+
+    /**
+     * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls.
+     */
+    private boolean mIsVibrating = false;
+
     Ringer(CallAudioManager callAudioManager) {
         mCallAudioManager = callAudioManager;
+
+        // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this
+        // vibrator object will be isolated from others.
+        mVibrator = new SystemVibrator(TelecommApp.getInstance());
     }
 
     @Override
@@ -111,6 +134,12 @@
         } else {
             Log.v(this, "startRinging, skipping because volume is 0");
         }
+
+        if (shouldVibrate(TelecommApp.getInstance()) && !mIsVibrating) {
+            mVibrator.vibrate(VIBRATION_PATTERN, VIBRATION_PATTERN_REPEAT,
+                    AudioManager.STREAM_RING);
+            mIsVibrating = true;
+        }
     }
 
     private void stopRinging() {
@@ -119,5 +148,28 @@
         // Even though stop is asynchronous it's ok to update the audio manager. Things like audio
         // focus are voluntary so releasing focus too early is not detrimental.
         mCallAudioManager.setIsRinging(false);
+
+        if (mIsVibrating) {
+            mVibrator.cancel();
+            mIsVibrating = false;
+        }
+    }
+
+    private boolean shouldVibrate(Context context) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        int ringerMode = audioManager.getRingerMode();
+        if (getVibrateWhenRinging(context)) {
+            return ringerMode != AudioManager.RINGER_MODE_SILENT;
+        } else {
+            return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+        }
+    }
+
+    private boolean getVibrateWhenRinging(Context context) {
+        if (!mVibrator.hasVibrator()) {
+            return false;
+        }
+        return Settings.System.getInt(context.getContentResolver(),
+                Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
     }
 }
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index c2df63e..66e1269 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -363,7 +363,7 @@
         Iterator<Call> iterator = calls.iterator();
         while (iterator.hasNext()) {
             Call call = iterator.next();
-            if (call.getAgeInMilliseconds() >= newCallTimeoutMs) {
+            if (call.getAgeMs() >= newCallTimeoutMs) {
                 Log.d(this, "Call %s timed out.", call);
                 mOutgoingCallsManager.abort(call);
                 calls.remove(call);
diff --git a/src/com/android/telecomm/TelecommApp.java b/src/com/android/telecomm/TelecommApp.java
index e0a3bf6..49554da 100644
--- a/src/com/android/telecomm/TelecommApp.java
+++ b/src/com/android/telecomm/TelecommApp.java
@@ -23,13 +23,20 @@
  */
 public final class TelecommApp extends Application {
 
-    // Singleton instance of TelecommApp.
+    /** Singleton instance of TelecommApp. */
     private static TelecommApp sInstance;
 
+    /**
+     * Missed call notifier. Exists here so that the instance can be shared with
+     * {@link TelecommBroadcastReceiver}.
+     */
+    private MissedCallNotifier mMissedCallNotifier;
+
     /** {@inheritDoc} */
     @Override public void onCreate() {
         super.onCreate();
         sInstance = this;
+        mMissedCallNotifier = new MissedCallNotifier(this);
     }
 
     public static TelecommApp getInstance() {
@@ -38,4 +45,8 @@
         }
         return sInstance;
     }
+
+    MissedCallNotifier getMissedCallNotifier() {
+        return mMissedCallNotifier;
+    }
 }
diff --git a/src/com/android/telecomm/TelecommBroadcastReceiver.java b/src/com/android/telecomm/TelecommBroadcastReceiver.java
new file mode 100644
index 0000000..1168b89
--- /dev/null
+++ b/src/com/android/telecomm/TelecommBroadcastReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+/**
+ * Handles miscellaneous Telecomm broadcast intents. This should be visible from outside, but
+ * should not be in the "exported" state.
+ */
+public final class TelecommBroadcastReceiver extends BroadcastReceiver {
+    /** The action used to send SMS response for the missed call notification. */
+    static final String ACTION_SEND_SMS_FROM_NOTIFICATION =
+            "com.android.telecomm.ACTION_SEND_SMS_FROM_NOTIFICATION";
+
+    /** The action used to call a handle back for the missed call notification. */
+    static final String ACTION_CALL_BACK_FROM_NOTIFICATION =
+            "com.android.telecomm.ACTION_CALL_BACK_FROM_NOTIFICATION";
+
+    /** The action used to clear missed calls. */
+    static final String ACTION_CLEAR_MISSED_CALLS =
+            "com.android.telecomm.ACTION_CLEAR_MISSED_CALLS";
+
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        Log.v(this, "Action received: %s.", action);
+
+        MissedCallNotifier missedCallNotifier = TelecommApp.getInstance().getMissedCallNotifier();
+
+        // Send an SMS from the missed call notification.
+        if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
+            // Close the notification shade and the notification itself.
+            closeSystemDialogs(context);
+            missedCallNotifier.clearMissedCalls();
+
+            Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
+            callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(callIntent);
+
+        // Call back recent caller from the missed call notification.
+        } else if (ACTION_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
+            // Close the notification shade and the notification itself.
+            closeSystemDialogs(context);
+            missedCallNotifier.clearMissedCalls();
+
+            Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
+            callIntent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+            context.startActivity(callIntent);
+
+        // Clear the missed call notification and call log entries.
+        } else if (ACTION_CLEAR_MISSED_CALLS.equals(action)) {
+            missedCallNotifier.clearMissedCalls();
+        }
+    }
+
+    /**
+     * Closes open system dialogs and the notification shade.
+     */
+    private void closeSystemDialogs(Context context) {
+        Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        context.sendBroadcastAsUser(intent, UserHandle.ALL);
+    }
+}