Merge "Remove the work badge icon for Google caller id in call log" into ub-contactsdialer-b-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 67161ad..1be1910 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -271,6 +271,12 @@
             android:exported="false"
         />
 
+        <receiver android:name=".calllog.MissedCallNotificationReceiver">
+            <intent-filter>
+                <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" />
+            </intent-filter>
+        </receiver>
+
         <!-- Service to update a contact -->
         <service
             android:name=".contact.ContactUpdateService"
diff --git a/res/drawable-hdpi/ic_phone_24dp.png b/res/drawable-hdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..b27dfba
--- /dev/null
+++ b/res/drawable-hdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_phone_24dp.png b/res/drawable-mdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..c1766b8
--- /dev/null
+++ b/res/drawable-mdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_phone_24dp.png b/res/drawable-xhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..83167f4
--- /dev/null
+++ b/res/drawable-xhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_phone_24dp.png b/res/drawable-xxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..8fff728
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_phone_24dp.png b/res/drawable-xxxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..30d141d
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2838e68..7e87b2f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -99,6 +99,28 @@
     <!-- Title of the "Clearing call log" progress-dialog [CHAR LIMIT=35] -->
     <string name="clearCallLogProgress_title">Clearing call history\u2026</string>
 
+    <!-- Title used for the activity for placing a call. This name appears
+         in activity disambig dialogs -->
+    <string name="userCallActivityLabel" product="default">Phone</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>
+    <!-- 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=18] -->
+    <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=18] -->
+    <string name="notification_missedCall_message">Message</string>
+        <!-- DO NOT TRANSLATE. Hardcoded number used for restricted incoming phone numbers. -->
+    <string name="handle_restricted">RESTRICTED</string>
+
     <!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
     <plurals name="notification_voicemail_title">
         <item quantity="one">Voicemail</item>
diff --git a/src-N/com/android/dialer/compat/UserManagerSdkCompat.java b/src-N/com/android/dialer/compat/UserManagerSdkCompat.java
new file mode 100644
index 0000000..9a08d4e
--- /dev/null
+++ b/src-N/com/android/dialer/compat/UserManagerSdkCompat.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.compat;
+
+import android.content.Context;
+
+/**
+ * UserManagerCompat respecting Sdk requirements
+ */
+public class UserManagerSdkCompat {
+
+    /**
+     * Return whether the calling user is running in an "unlocked" state. A user
+     * is unlocked only after they've entered their credentials (such as a lock
+     * pattern or PIN), and credential-encrypted private app data storage is
+     * available.
+     *
+     * @param context the current context
+     * @return {@code true} if the user is unlocked or context is null, {@code false} otherwise
+     * @throws NullPointerException if context is null
+     */
+    public static boolean isUserUnlocked(Context context) {
+        return android.support.v4.os.UserManagerCompat.isUserUnlocked(context);
+    }
+
+}
diff --git a/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java b/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java
new file mode 100644
index 0000000..c79ac2f
--- /dev/null
+++ b/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.compat;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * UserManagerCompat respecting Sdk requirements
+ */
+public class UserManagerSdkCompat {
+
+    /**
+     * @return {@code true}
+     */
+    public static boolean isUserUnlocked(Context context) {
+        Log.wtf("UserManagerSdkCompat", "Not implemented");
+        return true;
+    }
+
+}
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index bcc45a0..72753be 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -1038,6 +1038,7 @@
                     public boolean onTouch(View v, MotionEvent event) {
                         // Show the FAB when the user touches the lists fragment and the soft
                         // keyboard is hidden.
+                        hideDialpadFragment(true, false);
                         showFabInSearchUi();
                         return false;
                     }
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
index 367cb78..64ccd5f 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
@@ -16,14 +16,127 @@
 
 package com.android.dialer.calllog;
 
-import android.content.Context;
+import static android.Manifest.permission.READ_CALL_LOG;
+import static android.Manifest.permission.READ_CONTACTS;
 
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.R;
 import com.android.dialer.util.TelecomUtil;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Helper class operating on call log notifications.
  */
 public class CallLogNotificationsHelper {
+    private static final String TAG = "CallLogNotifHelper";
+    private static CallLogNotificationsHelper sInstance;
+
+    /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
+    public static CallLogNotificationsHelper getInstance(Context context) {
+        if (sInstance == null) {
+            ContentResolver contentResolver = context.getContentResolver();
+            String countryIso = GeoUtil.getCurrentCountryIso(context);
+            sInstance = new CallLogNotificationsHelper(context,
+                    createNewCallsQuery(context, contentResolver),
+                    createNameLookupQuery(context, contentResolver),
+                    new ContactInfoHelper(context, countryIso),
+                    countryIso);
+        }
+        return sInstance;
+    }
+
+    private final Context mContext;
+    private final NewCallsQuery mNewCallsQuery;
+    private final NameLookupQuery mNameLookupQuery;
+    private final ContactInfoHelper mContactInfoHelper;
+    private final String mCurrentCountryIso;
+
+    CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery,
+            NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper,
+            String countryIso) {
+        mContext = context;
+        mNewCallsQuery = newCallsQuery;
+        mNameLookupQuery = nameLookupQuery;
+        mContactInfoHelper = contactInfoHelper;
+        mCurrentCountryIso = countryIso;
+    }
+
+    /**
+     * Get all voicemails with the "new" flag set to 1.
+     *
+     * @return A list of NewCall objects where each object represents a new voicemail.
+     */
+    @Nullable
+    public List<NewCall> getNewVoicemails() {
+        return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
+    }
+
+    /**
+     * Get all missed calls with the "new" flag set to 1.
+     *
+     * @return A list of NewCall objects where each object represents a new missed call.
+     */
+    @Nullable
+    public List<NewCall> getNewMissedCalls() {
+        return mNewCallsQuery.query(Calls.MISSED_TYPE);
+    }
+
+    /**
+     * Given a number and number information (presentation and country ISO), get the best name
+     * for display. If the name is empty but we have a special presentation, display that.
+     * Otherwise attempt to look it up in the database or the cache.
+     * If that fails, fall back to displaying the number.
+     */
+    public String getName(@Nullable String number, int numberPresentation,
+                          @Nullable String countryIso) {
+        String name = PhoneNumberDisplayUtil.getDisplayName(
+                mContext,
+                number,
+                numberPresentation,
+                false).toString();
+        if (!TextUtils.isEmpty(name)) {
+            return name;
+        }
+
+        // Look it up in the database.
+        name = mNameLookupQuery.query(number);
+        if (!TextUtils.isEmpty(name)) {
+            return name;
+        }
+
+        if (countryIso == null) {
+            countryIso = mCurrentCountryIso;
+        }
+
+        // Look it up in the cache
+        ContactInfo contactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
+
+        if (contactInfo != null && !TextUtils.isEmpty(contactInfo.name)) {
+            return contactInfo.name;
+        }
+
+        if (!TextUtils.isEmpty(number)) {
+            // If we cannot lookup the contact, use the number instead.
+            return PhoneNumberUtils.formatNumber(number, countryIso);
+        }
+        return mContext.getResources().getString(R.string.unknown);
+    }
+
     /** Removes the missed call notifications. */
     public static void removeMissedCallNotifications(Context context) {
         TelecomUtil.cancelMissedCallsNotification(context);
@@ -33,4 +146,188 @@
     public static void updateVoicemailNotifications(Context context) {
         CallLogNotificationsService.updateVoicemailNotifications(context, null);
     }
+
+    /** Information about a new voicemail. */
+    public static final class NewCall {
+        public final Uri callsUri;
+        public final Uri voicemailUri;
+        public final String number;
+        public final int numberPresentation;
+        public final String accountComponentName;
+        public final String accountId;
+        public final String transcription;
+        public final String countryIso;
+        public final long dateMs;
+
+        public NewCall(
+                Uri callsUri,
+                Uri voicemailUri,
+                String number,
+                int numberPresentation,
+                String accountComponentName,
+                String accountId,
+                String transcription,
+                String countryIso,
+                long dateMs) {
+            this.callsUri = callsUri;
+            this.voicemailUri = voicemailUri;
+            this.number = number;
+            this.numberPresentation = numberPresentation;
+            this.accountComponentName = accountComponentName;
+            this.accountId = accountId;
+            this.transcription = transcription;
+            this.countryIso = countryIso;
+            this.dateMs = dateMs;
+        }
+    }
+
+    /** Allows determining the new calls for which a notification should be generated. */
+    public interface NewCallsQuery {
+        /**
+         * Returns the new calls of a certain type for which a notification should be generated.
+         */
+        @Nullable
+        public List<NewCall> query(int type);
+    }
+
+    /** Create a new instance of {@link NewCallsQuery}. */
+    public static NewCallsQuery createNewCallsQuery(Context context,
+            ContentResolver contentResolver) {
+
+        return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
+    }
+
+    /**
+     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
+     * notify about in the call log.
+     */
+    private static final class DefaultNewCallsQuery implements NewCallsQuery {
+        private static final String[] PROJECTION = {
+            Calls._ID,
+            Calls.NUMBER,
+            Calls.VOICEMAIL_URI,
+            Calls.NUMBER_PRESENTATION,
+            Calls.PHONE_ACCOUNT_COMPONENT_NAME,
+            Calls.PHONE_ACCOUNT_ID,
+            Calls.TRANSCRIPTION,
+            Calls.COUNTRY_ISO,
+            Calls.DATE
+        };
+        private static final int ID_COLUMN_INDEX = 0;
+        private static final int NUMBER_COLUMN_INDEX = 1;
+        private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
+        private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
+        private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
+        private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
+        private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
+        private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
+        private static final int DATE_COLUMN_INDEX = 8;
+
+        private final ContentResolver mContentResolver;
+        private final Context mContext;
+
+        private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
+            mContext = context;
+            mContentResolver = contentResolver;
+        }
+
+        @Override
+        @Nullable
+        public List<NewCall> query(int type) {
+            if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
+                Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+                return null;
+            }
+            final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+            final String[] selectionArgs = new String[]{ Integer.toString(type) };
+            try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL,
+                    PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) {
+                if (cursor == null) {
+                    return null;
+                }
+                List<NewCall> newCalls = new ArrayList<>();
+                while (cursor.moveToNext()) {
+                    newCalls.add(createNewCallsFromCursor(cursor));
+                }
+                return newCalls;
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+                return null;
+            }
+        }
+
+        /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
+        private NewCall createNewCallsFromCursor(Cursor cursor) {
+            String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
+            Uri callsUri = ContentUris.withAppendedId(
+                    Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
+            Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
+            return new NewCall(
+                    callsUri,
+                    voicemailUri,
+                    cursor.getString(NUMBER_COLUMN_INDEX),
+                    cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
+                    cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
+                    cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
+                    cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
+                    cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
+                    cursor.getLong(DATE_COLUMN_INDEX));
+        }
+    }
+
+    /** Allows determining the name associated with a given phone number. */
+    public interface NameLookupQuery {
+        /**
+         * Returns the name associated with the given number in the contacts database, or null if
+         * the number does not correspond to any of the contacts.
+         * <p>
+         * If there are multiple contacts with the same phone number, it will return the name of one
+         * of the matching contacts.
+         */
+        @Nullable
+        public String query(@Nullable String number);
+    }
+
+    /** Create a new instance of {@link NameLookupQuery}. */
+    public static NameLookupQuery createNameLookupQuery(Context context,
+            ContentResolver contentResolver) {
+        return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
+    }
+
+    /**
+     * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
+     * contacts database.
+     */
+    private static final class DefaultNameLookupQuery implements NameLookupQuery {
+        private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
+        private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
+
+        private final ContentResolver mContentResolver;
+        private final Context mContext;
+
+        private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
+            mContext = context;
+            mContentResolver = contentResolver;
+        }
+
+        @Override
+        @Nullable
+        public String query(@Nullable String number) {
+            if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
+                Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
+                return null;
+            }
+            try (Cursor cursor =  mContentResolver.query(
+                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                    PROJECTION, null, null, null)) {
+                if (cursor == null || !cursor.moveToFirst()) {
+                    return null;
+                }
+                return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
+                return null;
+            }
+        }
+    }
 }
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsService.java b/src/com/android/dialer/calllog/CallLogNotificationsService.java
index d2a494d..4ff9576 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsService.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsService.java
@@ -26,15 +26,16 @@
 import com.android.dialer.util.TelecomUtil;
 
 /**
- * Provides operations for managing notifications.
+ * Provides operations for managing call-related notifications.
  * <p>
  * It handles the following actions:
  * <ul>
- * <li>{@link #ACTION_MARK_NEW_VOICEMAILS_AS_OLD}: marks all the new voicemails in the call log as
- * old; this is called when a notification is dismissed.</li>
- * <li>{@link #ACTION_UPDATE_NOTIFICATIONS}: updates the content of the new items notification; it
- * may include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}, containing the URI of the new
- * voicemail that has triggered this update (if any).</li>
+ * <li>Updating voicemail notifications</li>
+ * <li>Marking new voicemails as old</li>
+ * <li>Updating missed call notifications</li>
+ * <li>Marking new missed calls as old</li>
+ * <li>Calling back from a missed call</li>
+ * <li>Sending an SMS from a missed call</li>
  * </ul>
  */
 public class CallLogNotificationsService extends IntentService {
@@ -45,21 +46,62 @@
             "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
 
     /**
-     * Action to update the notifications.
+     * Action to update voicemail notifications.
      * <p>
      * May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
      */
-    public static final String ACTION_UPDATE_NOTIFICATIONS =
-            "com.android.dialer.calllog.UPDATE_NOTIFICATIONS";
+    public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
+            "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
 
     /**
-     * Extra to included with {@link #ACTION_UPDATE_NOTIFICATIONS} to identify the new voicemail
-     * that triggered an update.
+     * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new
+     * voicemail that triggered an update.
      * <p>
      * It must be a {@link Uri}.
      */
     public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
 
+    /**
+     * Action to update the missed call notifications.
+     * <p>
+     * Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and
+     * {@link #EXTRA_MISSED_CALL_COUNT}.
+     */
+    public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
+            "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+
+    /** Action to mark all the new missed calls as old. */
+    public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
+            "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+
+    /** Action to call back a missed call. */
+    public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
+            "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
+
+    public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION =
+            "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION";
+
+    /**
+     * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS},
+     * {@link #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and
+     * {@link #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display,
+     * call or text back.
+     * <p>
+     * It must be a {@link String}.
+     */
+    public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER";
+
+    /**
+     * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the
+     * number of missed calls.
+     * <p>
+     * It must be a {@link Integer}
+     */
+    public static final String EXTRA_MISSED_CALL_COUNT =
+            "MISSED_CALL_COUNT";
+
+    public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
+
     private VoicemailQueryHandler mVoicemailQueryHandler;
 
     public CallLogNotificationsService() {
@@ -77,16 +119,38 @@
             return;
         }
 
-        if (ACTION_MARK_NEW_VOICEMAILS_AS_OLD.equals(intent.getAction())) {
-            if (mVoicemailQueryHandler == null) {
-                mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
-            }
-            mVoicemailQueryHandler.markNewVoicemailsAsOld();
-        } else if (ACTION_UPDATE_NOTIFICATIONS.equals(intent.getAction())) {
-            Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
-            DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
-        } else {
-            Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+        String action = intent.getAction();
+        switch (action) {
+            case ACTION_MARK_NEW_VOICEMAILS_AS_OLD:
+                if (mVoicemailQueryHandler == null) {
+                    mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
+                }
+                mVoicemailQueryHandler.markNewVoicemailsAsOld();
+                break;
+            case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
+                Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+                DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+                break;
+            case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
+                int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT,
+                        UNKNOWN_MISSED_CALL_COUNT);
+                String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER);
+                MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
+                break;
+            case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
+                CallLogNotificationsHelper.removeMissedCallNotifications(this);
+                break;
+            case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
+                MissedCallNotifier.getInstance(this).callBackFromMissedCall(
+                        intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+                break;
+            case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
+                MissedCallNotifier.getInstance(this).sendSmsFromMissedCall(
+                        intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+                break;
+            default:
+                Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+                break;
         }
     }
 
@@ -100,7 +164,8 @@
     public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
         if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
             Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
-            serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+            serviceIntent.setAction(
+                    CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
             // If voicemailUri is null, then notifications for all voicemails will be updated.
             if (voicemailUri != null) {
                 serviceIntent.putExtra(
@@ -109,4 +174,21 @@
             context.startService(serviceIntent);
         }
     }
+
+    /**
+     * Updates notifications for any new missed calls.
+     *
+     * @param context A valid context.
+     * @param count The number of new missed calls.
+     * @param number The phone number of the newest missed call.
+     */
+    public static void updateMissedCallNotifications(Context context, int count,
+            String number) {
+        Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+        serviceIntent.setAction(
+                CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS);
+        serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count);
+        serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number);
+        context.startService(serviceIntent);
+    }
 }
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index e075b9b..7f08fdc 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -19,7 +19,6 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteFullException;
 import android.net.Uri;
-import android.os.Build;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -32,7 +31,6 @@
 import android.util.Log;
 
 import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.compat.ContactsCompat;
 import com.android.contacts.common.util.Constants;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.util.PhoneNumberHelper;
@@ -42,7 +40,6 @@
 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
 import com.android.dialer.util.TelecomUtil;
 import com.android.dialerbind.ObjectFactory;
-import com.android.incallui.CallerInfo;
 
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index 2b894a1..db82295 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -19,42 +19,41 @@
 import static android.Manifest.permission.READ_CALL_LOG;
 import static android.Manifest.permission.READ_CONTACTS;
 
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
 import com.google.common.collect.Maps;
 
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.net.Uri;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.PhoneLookup;
-import android.telephony.PhoneNumberUtils;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.common.io.MoreCloseables;
 import com.android.contacts.common.util.ContactDisplayUtils;
-import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.DialtactsActivity;
 import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
 import com.android.dialer.filterednumber.FilteredNumbersUtil;
 import com.android.dialer.list.ListsFragment;
 
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
 /**
- * VoicemailNotifier that shows a notification in the status bar.
+ * Shows a voicemail notification in the status bar.
  */
 public class DefaultVoicemailNotifier {
-    public static final String TAG = "DefaultVoicemailNotifier";
+    public static final String TAG = "VoicemailNotifier";
 
     /** The tag used to identify notifications from this class. */
     private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
@@ -65,30 +64,18 @@
     private static DefaultVoicemailNotifier sInstance;
 
     private final Context mContext;
-    private final NotificationManager mNotificationManager;
-    private final NewCallsQuery mNewCallsQuery;
-    private final NameLookupQuery mNameLookupQuery;
 
     /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
-    public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
+    public static DefaultVoicemailNotifier getInstance(Context context) {
         if (sInstance == null) {
-            NotificationManager notificationManager =
-                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
             ContentResolver contentResolver = context.getContentResolver();
-            sInstance = new DefaultVoicemailNotifier(context, notificationManager,
-                    createNewCallsQuery(context, contentResolver),
-                    createNameLookupQuery(context, contentResolver));
+            sInstance = new DefaultVoicemailNotifier(context);
         }
         return sInstance;
     }
 
-    private DefaultVoicemailNotifier(Context context,
-            NotificationManager notificationManager, NewCallsQuery newCallsQuery,
-            NameLookupQuery nameLookupQuery) {
+    private DefaultVoicemailNotifier(Context context) {
         mContext = context;
-        mNotificationManager = notificationManager;
-        mNewCallsQuery = newCallsQuery;
-        mNameLookupQuery = nameLookupQuery;
     }
 
     /**
@@ -102,16 +89,17 @@
     public void updateNotification(Uri newCallUri) {
         // Lookup the list of new voicemails to include in the notification.
         // TODO: Move this into a service, to avoid holding the receiver up.
-        final List<NewCall> newCalls = mNewCallsQuery.query();
+        final List<NewCall> newCalls =
+                CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
 
         if (newCalls == null) {
             // Query failed, just return.
             return;
         }
 
-        if (newCalls.size() == 0) {
+        if (newCalls.isEmpty()) {
             // No voicemails to notify about: clear the notification.
-            clearNotification();
+            getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
             return;
         }
 
@@ -145,19 +133,8 @@
             // Check if we already know the name associated with this number.
             String name = names.get(newCall.number);
             if (name == null) {
-                name = PhoneNumberDisplayUtil.getDisplayName(
-                        mContext,
-                        newCall.number,
-                        newCall.numberPresentation,
-                        /* isVoicemail */ false).toString();
-                // If we cannot lookup the contact, use the number instead.
-                if (TextUtils.isEmpty(name)) {
-                    // Look it up in the database.
-                    name = mNameLookupQuery.query(newCall.number);
-                    if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(newCall.number)) {
-                        name = PhoneNumberUtils.formatNumber(newCall.number, newCall.countryIso);
-                    }
-                }
+                name = CallLogNotificationsHelper.getInstance(mContext).getName(newCall.number,
+                        newCall.numberPresentation, newCall.countryIso);
                 names.put(newCall.number, name);
                 // This is a new caller. Add it to the back of the list of callers.
                 if (TextUtils.isEmpty(callers)) {
@@ -174,6 +151,7 @@
             }
         }
 
+        // All the potential new voicemails have been removed, e.g. if they were spam.
         if (newCalls.isEmpty()) {
             return;
         }
@@ -194,13 +172,31 @@
         // TODO: Use the photo of contact if all calls are from the same person.
         final int icon = android.R.drawable.stat_notify_voicemail;
 
+        Uri ringtoneUri = null;
+        int notificationDefaults = 0;
+        if (callToNotify != null) {
+            PhoneAccountHandle accountHandle = new PhoneAccountHandle(
+                    ComponentName.unflattenFromString(callToNotify.accountComponentName),
+                    callToNotify.accountId);
+            ringtoneUri = TelephonyManagerCompat
+                    .getVoicemailRingtoneUri(getTelephonyManager(), accountHandle);
+            if (ContactsUtils.FLAG_N_FEATURE) {
+                notificationDefaults = TelephonyManagerCompat.isVoicemailVibrationEnabled(
+                        getTelephonyManager(), accountHandle)
+                        ? Notification.DEFAULT_VIBRATE : 0;
+            } else {
+                notificationDefaults = Notification.DEFAULT_ALL;
+            }
+        }
+
         Notification.Builder notificationBuilder = new Notification.Builder(mContext)
                 .setSmallIcon(icon)
                 .setContentTitle(title)
                 .setContentText(callers)
                 .setStyle(new Notification.BigTextStyle().bigText(transcription))
                 .setColor(resources.getColor(R.color.dialer_theme_color))
-                .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
+                .setSound(ringtoneUri)
+                .setDefaults(notificationDefaults)
                 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
                 .setAutoCancel(true);
 
@@ -221,7 +217,8 @@
             notificationBuilder.setTicker(msg);
         }
 
-        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
+        getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID,
+                notificationBuilder.build());
     }
 
     /** Creates a pending intent that marks all new voicemails as old. */
@@ -231,194 +228,11 @@
         return PendingIntent.getService(mContext, 0, intent, 0);
     }
 
-    public void clearNotification() {
-        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+    private NotificationManager getNotificationManager() {
+        return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
     }
 
-    /** Information about a new voicemail. */
-    private static final class NewCall {
-        public final Uri callsUri;
-        public final Uri voicemailUri;
-        public final String number;
-        public final int numberPresentation;
-        public final String accountComponentName;
-        public final String accountId;
-        public final String transcription;
-        public final String countryIso;
-        public final long dateMs;
-
-        public NewCall(
-                Uri callsUri,
-                Uri voicemailUri,
-                String number,
-                int numberPresentation,
-                String accountComponentName,
-                String accountId,
-                String transcription,
-                String countryIso,
-                long dateMs) {
-            this.callsUri = callsUri;
-            this.voicemailUri = voicemailUri;
-            this.number = number;
-            this.numberPresentation = numberPresentation;
-            this.accountComponentName = accountComponentName;
-            this.accountId = accountId;
-            this.transcription = transcription;
-            this.countryIso = countryIso;
-            this.dateMs = dateMs;
-        }
-    }
-
-    /** Allows determining the new calls for which a notification should be generated. */
-    public interface NewCallsQuery {
-        /**
-         * Returns the new calls for which a notification should be generated.
-         */
-        public List<NewCall> query();
-    }
-
-    /** Create a new instance of {@link NewCallsQuery}. */
-    public static NewCallsQuery createNewCallsQuery(Context context,
-            ContentResolver contentResolver) {
-        return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
-    }
-
-    /**
-     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
-     * notify about in the call log.
-     */
-    private static final class DefaultNewCallsQuery implements NewCallsQuery {
-        private static final String[] PROJECTION = {
-            Calls._ID,
-            Calls.NUMBER,
-            Calls.VOICEMAIL_URI,
-            Calls.NUMBER_PRESENTATION,
-            Calls.PHONE_ACCOUNT_COMPONENT_NAME,
-            Calls.PHONE_ACCOUNT_ID,
-            Calls.TRANSCRIPTION,
-            Calls.COUNTRY_ISO,
-            Calls.DATE
-        };
-        private static final int ID_COLUMN_INDEX = 0;
-        private static final int NUMBER_COLUMN_INDEX = 1;
-        private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
-        private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
-        private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
-        private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
-        private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
-        private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
-        private static final int DATE_COLUMN_INDEX = 8;
-
-        private final ContentResolver mContentResolver;
-        private final Context mContext;
-
-        private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
-            mContext = context;
-            mContentResolver = contentResolver;
-        }
-
-        @Override
-        public List<NewCall> query() {
-            if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
-                Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
-                return null;
-            }
-            final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
-            final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
-            Cursor cursor = null;
-            try {
-                cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
-                        selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
-                if (cursor == null) {
-                    return null;
-                }
-                List<NewCall> newCalls = new LinkedList<NewCall>();
-                while (cursor.moveToNext()) {
-                    newCalls.add(createNewCallsFromCursor(cursor));
-                }
-                return newCalls;
-            } catch (RuntimeException e) {
-                Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
-                return null;
-            } finally {
-                MoreCloseables.closeQuietly(cursor);
-            }
-        }
-
-        /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
-        private NewCall createNewCallsFromCursor(Cursor cursor) {
-            String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
-            Uri callsUri = ContentUris.withAppendedId(
-                    Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
-            Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
-            return new NewCall(
-                    callsUri,
-                    voicemailUri,
-                    cursor.getString(NUMBER_COLUMN_INDEX),
-                    cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
-                    cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
-                    cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
-                    cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
-                    cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
-                    cursor.getLong(DATE_COLUMN_INDEX));
-        }
-    }
-
-    /** Allows determining the name associated with a given phone number. */
-    public interface NameLookupQuery {
-        /**
-         * Returns the name associated with the given number in the contacts database, or null if
-         * the number does not correspond to any of the contacts.
-         * <p>
-         * If there are multiple contacts with the same phone number, it will return the name of one
-         * of the matching contacts.
-         */
-        public String query(String number);
-    }
-
-    /** Create a new instance of {@link NameLookupQuery}. */
-    public static NameLookupQuery createNameLookupQuery(Context context,
-            ContentResolver contentResolver) {
-        return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
-    }
-
-    /**
-     * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
-     * contacts database.
-     */
-    private static final class DefaultNameLookupQuery implements NameLookupQuery {
-        private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
-        private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
-
-        private final ContentResolver mContentResolver;
-        private final Context mContext;
-
-        private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
-            mContext = context;
-            mContentResolver = contentResolver;
-        }
-
-        @Override
-        public String query(String number) {
-            if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
-                Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
-                return null;
-            }
-            Cursor cursor = null;
-            try {
-                cursor = mContentResolver.query(
-                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
-                        PROJECTION, null, null, null);
-                if (cursor == null || !cursor.moveToFirst()) return null;
-                return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
-            } catch (RuntimeException e) {
-                Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
-                return null;
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
-        }
+    private TelephonyManager getTelephonyManager() {
+        return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
     }
 }
diff --git a/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
new file mode 100644
index 0000000..86d6cb9
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.dialer.calllog.CallLogNotificationsService;
+
+/**
+ * Receives broadcasts that should trigger a refresh of the missed call notification. This includes
+ * both an explicit broadcast from Telecom and a reboot.
+ */
+public class MissedCallNotificationReceiver extends BroadcastReceiver {
+    //TODO: Use compat class for these methods.
+    public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION =
+            "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION";
+
+    public static final String EXTRA_NOTIFICATION_COUNT =
+            "android.telecom.extra.NOTIFICATION_COUNT";
+
+    public static final String EXTRA_NOTIFICATION_PHONE_NUMBER =
+            "android.telecom.extra.NOTIFICATION_PHONE_NUMBER";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) {
+            return;
+        }
+
+        int count = intent.getIntExtra(EXTRA_NOTIFICATION_COUNT,
+                CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT);
+        String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER);
+        CallLogNotificationsService.updateMissedCallNotifications(context, count, number);
+    }
+}
diff --git a/src/com/android/dialer/calllog/MissedCallNotifier.java b/src/com/android/dialer/calllog/MissedCallNotifier.java
new file mode 100644
index 0000000..8811baf
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotifier.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.list.ListsFragment;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
+import com.android.dialer.R;
+
+import java.util.List;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ *
+ */
+public class MissedCallNotifier {
+    public static final String TAG = "MissedCallNotifier";
+
+    /** The tag used to identify notifications from this class. */
+    private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+    /** The identifier of the notification of new missed calls. */
+    private static final int NOTIFICATION_ID = 1;
+    /** Preference file key for number of missed calls. */
+    private static final String MISSED_CALL_COUNT = "missed_call_count";
+
+    private static MissedCallNotifier sInstance;
+    private Context mContext;
+
+    /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+    public static MissedCallNotifier getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new MissedCallNotifier(context);
+        }
+        return sInstance;
+    }
+
+    private MissedCallNotifier(Context context) {
+        mContext = context;
+    }
+
+    public void updateMissedCallNotification(int count, String number) {
+        final int titleResId;
+        final String expandedText;  // The text in the notification's line 1 and 2.
+
+        final List<NewCall> newCalls =
+                CallLogNotificationsHelper.getInstance(mContext).getNewMissedCalls();
+
+        if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+            if (newCalls == null) {
+                // If the intent did not contain a count, and we are unable to get a count from the
+                // call log, then no notification can be shown.
+                return;
+            }
+            count = newCalls.size();
+        }
+
+        if (count == 0) {
+            // No voicemails to notify about: clear the notification.
+            clearMissedCalls();
+            return;
+        }
+
+        // The call log has been updated, use that information preferentially.
+        boolean useCallLog = newCalls != null && newCalls.size() == count;
+        NewCall newestCall = useCallLog ? newCalls.get(0) : null;
+        long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
+
+        // Display the first line of the notification:
+        // 1 missed call: <caller name || handle>
+        // More than 1 missed call: <number of calls> + "missed calls"
+        if (count == 1) {
+            titleResId = R.string.notification_missedCallTitle;
+
+            //TODO: look up caller ID that is not in contacts.
+            expandedText = CallLogNotificationsHelper.getInstance(mContext)
+                    .getName(useCallLog ? newestCall.number : number,
+                            useCallLog ? newestCall.numberPresentation
+                                    : Calls.PRESENTATION_ALLOWED,
+                            useCallLog ? newestCall.countryIso : null);
+        } else {
+            titleResId = R.string.notification_missedCallsTitle;
+            expandedText =
+                    mContext.getString(R.string.notification_missedCallsMsg, count);
+        }
+
+        // Create a public viewable version of the notification, suitable for display when sensitive
+        // notification content is hidden.
+        Notification.Builder publicBuilder = new Notification.Builder(mContext);
+        publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+                .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+                // Show "Phone" for notification title.
+                .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
+                // Notification details shows that there are missed call(s), but does not reveal
+                // the missed caller information.
+                .setContentText(mContext.getText(titleResId))
+                .setContentIntent(createCallLogPendingIntent())
+                .setAutoCancel(true)
+                .setWhen(timeMs)
+                .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+        // Create the notification suitable for display when sensitive information is showing.
+        Notification.Builder builder = new Notification.Builder(mContext);
+        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+                .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+                .setContentTitle(mContext.getText(titleResId))
+                .setContentText(expandedText)
+                .setContentIntent(createCallLogPendingIntent())
+                .setAutoCancel(true)
+                .setWhen(timeMs)
+                .setDeleteIntent(createClearMissedCallsPendingIntent())
+                // Include a public version of the notification to be shown when the missed call
+                // notification is shown on the user's lock screen and they have chosen to hide
+                // sensitive notification information.
+                .setPublicVersion(publicBuilder.build());
+
+        // Add additional actions when there is only 1 missed call, like call-back and SMS.
+        if (count == 1) {
+            if (!TextUtils.isEmpty(number)
+                    && !TextUtils.equals(
+                    number, mContext.getString(R.string.handle_restricted))) {
+                builder.addAction(R.drawable.ic_phone_24dp,
+                        mContext.getString(R.string.notification_missedCall_call_back),
+                        createCallBackPendingIntent(number));
+
+                if (!PhoneNumberHelper.isUriNumber(number)) {
+                    builder.addAction(R.drawable.ic_message_24dp,
+                            mContext.getString(R.string.notification_missedCall_message),
+                            createSendSmsFromNotificationPendingIntent(number));
+                }
+            }
+            //TODO: add photo
+        }
+
+        Notification notification = builder.build();
+        configureLedOnNotification(notification);
+
+        Log.i(TAG, "Adding missed call notification.");
+        getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+    }
+
+    private void clearMissedCalls() {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                // 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(" = ?");
+                try {
+                    mContext.getContentResolver().update(Calls.CONTENT_URI, values,
+                            where.toString(), new String[]{ Integer.toString(Calls.
+                            MISSED_TYPE) });
+                } catch (IllegalArgumentException e) {
+                    Log.w(TAG, "ContactsProvider update command failed", e);
+                }
+                getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+            }
+        });
+    }
+
+    /**
+     * Trigger an intent to make a call from a missed call number.
+     */
+    public void callBackFromMissedCall(String number) {
+        closeSystemDialogs(mContext);
+        CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+        DialerUtils.startActivityWithErrorToast(
+                mContext,
+                new CallIntentBuilder(number)
+                        .build()
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+    }
+
+    /**
+     * Trigger an intent to send an sms from a missed call number.
+     */
+    public void sendSmsFromMissedCall(String number) {
+        closeSystemDialogs(mContext);
+        CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+        DialerUtils.startActivityWithErrorToast(
+                mContext,
+                IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+    }
+
+    /**
+     * Creates a new pending intent that sends the user to the call log.
+     *
+     * @return The pending intent.
+     */
+    private PendingIntent createCallLogPendingIntent() {
+        Intent contentIntent = new Intent(mContext, DialtactsActivity.class);
+        contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_HISTORY);
+        return PendingIntent.getActivity(
+                mContext, 0, contentIntent,PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    /** Creates a pending intent that marks all new missed calls as old. */
+    private PendingIntent createClearMissedCallsPendingIntent() {
+        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+        intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+        return PendingIntent.getService(mContext, 0, intent, 0);
+    }
+
+    private PendingIntent createCallBackPendingIntent(String number) {
+        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+        intent.setAction(
+                CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
+        intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+        return PendingIntent.getService(mContext, 0, intent, 0);
+    }
+
+    private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
+        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+        intent.setAction(
+                CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
+        intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+        return PendingIntent.getService(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;
+    }
+
+    /**
+     * Closes open system dialogs and the notification shade.
+     */
+    private void closeSystemDialogs(Context context) {
+        context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+    }
+
+    private NotificationManager getNotificationMgr() {
+        return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+}
diff --git a/src/com/android/dialer/calllog/VoicemailQueryHandler.java b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
index 26f9bd1..c6e644c 100644
--- a/src/com/android/dialer/calllog/VoicemailQueryHandler.java
+++ b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
@@ -59,7 +59,8 @@
         if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) {
             if (mContext != null) {
                 Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class);
-                serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+                serviceIntent.setAction(
+                        CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
                 mContext.startService(serviceIntent);
             } else {
                 Log.w(TAG, "Unknown update completed: ignoring: " + token);
diff --git a/src/com/android/dialer/compat/UserManagerCompat.java b/src/com/android/dialer/compat/UserManagerCompat.java
index a1fdcc6..5767033 100644
--- a/src/com/android/dialer/compat/UserManagerCompat.java
+++ b/src/com/android/dialer/compat/UserManagerCompat.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.dialer.compat;
 
 import android.content.Context;
@@ -34,4 +49,23 @@
         // Adapted from {@link UserManager} and {@link UserHandle}.
         return (Process.myUid() / PER_USER_RANGE) == USER_SYSTEM;
     }
+
+    /**
+     * Return whether the calling user is running in an "unlocked" state. A user
+     * is unlocked only after they've entered their credentials (such as a lock
+     * pattern or PIN), and credential-encrypted private app data storage is
+     * available.
+     *
+     * TODO b/26688153
+     *
+     * @param context the current context
+     * @return {@code true} if the user is unlocked, {@code false} otherwise
+     * @throws NullPointerException if context is null
+     */
+    public static boolean isUserUnlocked(Context context) {
+        if (CompatUtils.isNCompatible()) {
+            return UserManagerSdkCompat.isUserUnlocked(context);
+        }
+        return true;
+    }
 }
diff --git a/src/com/android/dialer/settings/AppCompatPreferenceActivity.java b/src/com/android/dialer/settings/AppCompatPreferenceActivity.java
deleted file mode 100644
index 4e5d9c9..0000000
--- a/src/com/android/dialer/settings/AppCompatPreferenceActivity.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.dialer.settings;
-
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
- * to be used with AppCompat.
- */
-public class AppCompatPreferenceActivity extends PreferenceActivity {
-    private AppCompatDelegate mDelegate;
-
-    private boolean mIsSafeToCommitTransactions;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        getDelegate().installViewFactory();
-        getDelegate().onCreate(savedInstanceState);
-        super.onCreate(savedInstanceState);
-        mIsSafeToCommitTransactions = true;
-    }
-
-    @Override
-    protected void onPostCreate(Bundle savedInstanceState) {
-        super.onPostCreate(savedInstanceState);
-        getDelegate().onPostCreate(savedInstanceState);
-    }
-
-    public ActionBar getSupportActionBar() {
-        return getDelegate().getSupportActionBar();
-    }
-
-    public void setSupportActionBar(Toolbar toolbar) {
-        getDelegate().setSupportActionBar(toolbar);
-    }
-
-    @Override
-    public MenuInflater getMenuInflater() {
-        return getDelegate().getMenuInflater();
-    }
-
-    @Override
-    public void setContentView(int layoutResID) {
-        getDelegate().setContentView(layoutResID);
-    }
-
-    @Override
-    public void setContentView(View view) {
-        getDelegate().setContentView(view);
-    }
-
-    @Override
-    public void setContentView(View view, ViewGroup.LayoutParams params) {
-        getDelegate().setContentView(view, params);
-    }
-
-    @Override
-    public void addContentView(View view, ViewGroup.LayoutParams params) {
-        getDelegate().addContentView(view, params);
-    }
-
-    @Override
-    protected void onPostResume() {
-        super.onPostResume();
-        getDelegate().onPostResume();
-    }
-
-    @Override
-    protected void onTitleChanged(CharSequence title, int color) {
-        super.onTitleChanged(title, color);
-        getDelegate().setTitle(title);
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        getDelegate().onConfigurationChanged(newConfig);
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        getDelegate().onStop();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        getDelegate().onDestroy();
-    }
-
-    @Override
-    public void invalidateOptionsMenu() {
-        getDelegate().invalidateOptionsMenu();
-    }
-
-    private AppCompatDelegate getDelegate() {
-        if (mDelegate == null) {
-            mDelegate = AppCompatDelegate.create(this, null);
-        }
-        return mDelegate;
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        mIsSafeToCommitTransactions = true;
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        mIsSafeToCommitTransactions = true;
-    }
-
-    @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        mIsSafeToCommitTransactions = false;
-    }
-
-    /**
-     * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
-     * whether {@link Activity#onSaveInstanceState} has been called or not.
-     *
-     * Make sure that the current activity calls into
-     * {@link super.onSaveInstanceState(Bundle outState)} (if that method is overridden),
-     * so the flag is properly set.
-     */
-    public boolean isSafeToCommitTransactions() {
-        return mIsSafeToCommitTransactions;
-    }
-}
diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java
index a187157..b25c5a6 100644
--- a/src/com/android/dialer/settings/DialerSettingsActivity.java
+++ b/src/com/android/dialer/settings/DialerSettingsActivity.java
@@ -28,6 +28,7 @@
 import android.view.MenuItem;
 import android.widget.Toast;
 
+import com.android.contacts.common.activity.AppCompatPreferenceActivity;
 import com.android.contacts.common.compat.CompatUtils;
 import com.android.contacts.common.compat.SdkVersionOverride;
 import com.android.contacts.common.compat.TelephonyManagerCompat;
diff --git a/tests/src/com/android/dialer/compat/UserManagerCompatTest.java b/tests/src/com/android/dialer/compat/UserManagerCompatTest.java
new file mode 100644
index 0000000..ff734a1
--- /dev/null
+++ b/tests/src/com/android/dialer/compat/UserManagerCompatTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dialer.compat;
+
+import android.test.AndroidTestCase;
+
+import com.android.contacts.common.compat.CompatUtils;
+
+public class UserManagerCompatTest extends AndroidTestCase {
+
+    public void testIsUserUnlocked_N_NullContext() {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        try {
+            UserManagerCompat.isUserUnlocked(null);
+            fail("Expected NullPointerException but none was thrown");
+        } catch (NullPointerException e) {}
+    }
+
+    public void testIsUserUnlocked_M_NullContext() {
+        if (CompatUtils.isNCompatible()) {
+            return;
+        }
+        assertTrue(UserManagerCompat.isUserUnlocked(null));
+    }
+
+    public void testIsUserUnlocked() {
+        assertTrue(UserManagerCompat.isUserUnlocked(getContext()));
+    }
+}