am eb46c79f: (-s ours) am 1e4ca7d4: Merge "Fix SecurityException in DefaultVoicemailNotifier" into mnc-dev

* commit 'eb46c79f4fa0c8f3a15ab0ba663c15942bed3428':
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2b0447d..4055b44 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,11 +16,13 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.dialer"
-    coreApp="true">
+    coreApp="true"
+    android:versionCode="20210"
+    android:versionName="2.21">
 
     <uses-sdk
-        android:minSdkVersion="MNC"
-        android:targetSdkVersion="MNC" />
+        android:minSdkVersion="23"
+        android:targetSdkVersion="23" />
 
 
     <uses-permission android:name="android.permission.CALL_PHONE" />
@@ -172,6 +174,14 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="com.android.contacts.common.dialog.CallSubjectDialog"
+                  android:theme="@style/Theme.CallSubjectDialogTheme"
+                  android:windowSoftInputMode="stateVisible|adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+            </intent-filter>
+        </activity>
+
         <!-- Backwards compatibility: "Phone" from Gingerbread and earlier -->
         <activity-alias android:name="DialtactsActivity"
             android:targetActivity=".DialtactsActivity"
diff --git a/res/layout/show_all_contacts_fragment.xml b/res/layout/all_contacts_fragment.xml
similarity index 87%
rename from res/layout/show_all_contacts_fragment.xml
rename to res/layout/all_contacts_fragment.xml
index 00358dc..2ca013a 100644
--- a/res/layout/show_all_contacts_fragment.xml
+++ b/res/layout/all_contacts_fragment.xml
@@ -42,13 +42,13 @@
             android:fastScrollEnabled="true"
             android:fadingEdge="none"
             android:nestedScrollingEnabled="true" />
+
+        <com.android.dialer.widget.EmptyContentView
+            android:id="@+id/empty_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="gone"/>
+
     </FrameLayout>
-
-    <include
-        android:id="@+id/empty_list_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        layout="@layout/empty_list_view"
-        android:visibility="gone"/>
-
 </LinearLayout>
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
index 68e3060..f69c513 100644
--- a/res/layout/call_log_fragment.xml
+++ b/res/layout/call_log_fragment.xml
@@ -27,11 +27,11 @@
         android:paddingStart="@dimen/call_log_horizontal_margin"
         android:paddingEnd="@dimen/call_log_horizontal_margin" />
 
-    <include
+    <com.android.dialer.widget.EmptyContentView
         android:id="@+id/empty_list_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        layout="@layout/empty_list_view"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
         android:visibility="gone" />
 
 </FrameLayout>
diff --git a/res/layout/call_log_list_item_actions.xml b/res/layout/call_log_list_item_actions.xml
index b427206..16a712b 100644
--- a/res/layout/call_log_list_item_actions.xml
+++ b/res/layout/call_log_list_item_actions.xml
@@ -105,6 +105,20 @@
     </LinearLayout>
 
     <LinearLayout
+        android:id="@+id/call_with_note_action"
+        style="@style/CallLogActionStyle">
+
+        <ImageView
+            style="@style/CallLogActionIconStyle"
+            android:src="@drawable/ic_call_note_white_24dp" />
+
+        <TextView
+            style="@style/CallLogActionTextStyle"
+            android:text="@string/call_with_a_note" />
+
+    </LinearLayout>
+
+    <LinearLayout
         android:id="@+id/details_action"
         style="@style/CallLogActionStyle">
 
diff --git a/res/layout/empty_content_view.xml b/res/layout/empty_content_view.xml
new file mode 100644
index 0000000..97ac4c7
--- /dev/null
+++ b/res/layout/empty_content_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ImageView
+        android:id="@+id/emptyListViewImage"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:gravity="center_horizontal" />
+
+    <TextView
+        android:id="@+id/emptyListViewMessage"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal|top"
+        android:textSize="@dimen/empty_list_message_text_size"
+        android:textColor="@color/empty_list_text_color"
+        android:paddingRight="16dp"
+        android:paddingLeft="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp" />
+
+    <TextView
+        android:id="@+id/emptyListViewAction"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_gravity="center_horizontal"
+        android:paddingRight="16dp"
+        android:paddingLeft="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:clickable="true"
+        style="@style/TextActionStyle" />
+
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="40dp" />
+
+</merge>
diff --git a/res/layout/empty_list_view.xml b/res/layout/empty_list_view.xml
deleted file mode 100644
index 7f961a3..0000000
--- a/res/layout/empty_list_view.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2012 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.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:paddingTop="@dimen/empty_list_message_top_padding"
-    android:paddingBottom="@dimen/actionbar_and_tab_height"
-    android:minHeight="?android:attr/listPreferredItemHeight">
-
-    <ImageView
-        android:id="@+id/emptyListViewImage"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:gravity="center_horizontal" />
-
-    <TextView
-        android:id="@+id/emptyListViewMessage"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="84dp"
-        android:gravity="center_horizontal|top"
-        android:textSize="@dimen/empty_list_message_text_size"
-        android:textColor="@color/empty_list_text_color"
-        android:paddingRight="16dp"
-        android:paddingLeft="16dp" />
-
-</LinearLayout>
diff --git a/res/layout/speed_dial_fragment.xml b/res/layout/speed_dial_fragment.xml
index 1882049..5b6ce4f 100644
--- a/res/layout/speed_dial_fragment.xml
+++ b/res/layout/speed_dial_fragment.xml
@@ -41,11 +41,11 @@
             android:nestedScrollingEnabled="true" />
     </FrameLayout>
 
-    <include
+    <com.android.dialer.widget.EmptyContentView
         android:id="@+id/empty_list_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        layout="@layout/empty_list_view"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
         android:visibility="gone"/>
 
 </FrameLayout>
diff --git a/res/menu/dialpad_options.xml b/res/menu/dialpad_options.xml
index f0399a8..63fca07 100644
--- a/res/menu/dialpad_options.xml
+++ b/res/menu/dialpad_options.xml
@@ -23,5 +23,8 @@
         android:id="@+id/menu_add_wait"
         android:title="@string/add_wait"
         android:showAsAction="withText" />
-
+    <item
+        android:id="@+id/menu_call_with_note"
+        android:title="@string/call_with_a_note"
+        android:showAsAction="withText" />
 </menu>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index e2f0ff5..d8f4aa8 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -17,4 +17,5 @@
 <resources>
     <item type="id" name="context_menu_copy_to_clipboard" />
     <item type="id" name="context_menu_edit_before_call" />
+    <item type="id" name="settings_header_sounds_and_vibration" />
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a197da2..5e2476d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -20,7 +20,7 @@
 
     <!-- Application name used in Settings/Apps. Default label for activities
          that don't specify a label. -->
-    <string name="applicationLabel">Dialer</string>
+    <string name="applicationLabel">Phone</string>
 
     <!-- Title for the activity that dials the phone.  This is the name
          used in the Launcher icon. -->
@@ -70,7 +70,10 @@
     <string name="recentCalls_shareVoicemail">Share voicemail</string>
 
     <!-- Text displayed when the call log is empty. -->
-    <string name="recentCalls_empty">No calls</string>
+    <string name="recentCalls_empty">Your call log is empty</string>
+
+    <!-- Label of the button displayed when the call log is empty. Allows the user to make a call. -->
+    <string name="recentCalls_empty_action">Make a call</string>
 
     <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37]  -->
     <string name="clearCallLogConfirmation_title">Clear call history?</string>
@@ -466,10 +469,10 @@
     <string name="view_full_call_history_font_family">sans-serif</string>
 
     <!-- Text displayed when the list of missed calls is empty -->
-    <string name="recentMissed_empty">No calls</string>
+    <string name="recentMissed_empty">You have no missed calls.</string>
 
     <!-- Text displayed when the list of voicemails is empty -->
-    <string name="recentVoicemails_empty">No recent voicemails</string>
+    <string name="recentVoicemails_empty">Your voicemail inbox is empty.</string>
 
     <!--  Menu option to show favorite contacts only -->
     <string name="show_favorites_only">Show favorites only</string>
@@ -533,10 +536,16 @@
     <string name="num_missed_calls"><xliff:g id="number">%s</xliff:g> new missed calls</string>
 
     <!-- Shown when there are no speed dial favorites. -->
-    <string name="speed_dial_empty">Speed dial is one\u2011touch dialing for favorites and numbers you call often</string>
+    <string name="speed_dial_empty">No one is on your speed dial yet</string>
+
+    <!-- Shown as an action when there are no speed dial favorites -->
+    <string name="speed_dial_empty_add_favorite_action">Add a favorite</string>
 
     <!-- Shown when there are no contacts in the all contacts list. -->
-    <string name="all_contacts_empty">No contacts</string>
+    <string name="all_contacts_empty">You don\'t have any contacts yet</string>
+
+    <!-- Shown as an action when the all contacts list is empty -->
+    <string name="all_contacts_empty_add_contact_action">Add a contact</string>
 
     <!-- Shows up as a tooltip to provide a hint to the user that the profile pic in a contact
          card can be tapped to bring up a list of all numbers, or long pressed to start reordering
@@ -780,4 +789,31 @@
     <string name="play_dtmf_preference_key" translatable="false">button_play_dtmf_tone</string>
     <!-- DO NOT TRANSLATE. Internal key for DTMF tone length preference. -->
     <string name="dtmf_tone_length_preference_key" translatable="false">button_dtmf_settings</string>
+
+    <!-- The label of the button used to turn on a single permission -->
+    <string name="permission_single_turn_on">Turn on</string>
+
+    <!--  The label of the button used to turn on multiple permissions -->
+    <string name="permission_multiple_turn_on">Set permissions</string>
+
+    <!-- Shown as a prompt to turn on the contacts permission to enable speed dial -->
+    <string name="permission_no_speeddial">To enable speed dial,\n turn on the Contacts permission.</string>
+
+    <!-- Shown as a prompt to turn on the phone permission to enable the call log -->
+    <string name="permission_no_calllog">To see your call log,\n turn on the Phone permission.</string>
+
+    <!-- Shown as a prompt to turn on the contacts permission to show all contacts -->
+    <string name="permission_no_contacts">To see your contacts,\n turn on the Contacts permission.</string>
+
+    <!-- Shown as a prompt to turn on the phone permission to show voicemails -->
+    <string name="permission_no_voicemail">To access your voicemail,\n turn on the Phone permission.</string>
+
+    <!-- Shown as a prompt to turn on contacts permissions to allow contact search -->
+    <string name="permission_no_search">To search your contacts, turn on the Contacts permissions.</string>
+
+    <!-- Shown as a prompt to turn on the phone permission to allow a call to be placed -->
+    <string name="permission_place_call">To place a call,\n turn on the Phone permission.</string>
+
+    <!-- Shown as a message that notifies the user that the Phone app cannot write to system settings, which is why the system settings app is being launched directly instead.-->
+    <string name="toast_cannot_write_system_settings">Phone app does not have permission to write to system settings.</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 957fabf..e3a2f99 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -218,20 +218,24 @@
         <item name="cardBackgroundColor">@color/background_dialer_call_log_list_item</item>
     </style>
 
-    <style name="PromoCardActionStyle">
+    <style name="TextActionStyle">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">@dimen/call_log_action_height</item>
         <item name="android:gravity">end|center_vertical</item>
         <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
         <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
-        <item name="android:textColor">@color/promo_card_text</item>
-        <item name="android:textSize">@dimen/call_log_list_item_actions_text_size</item>
+        <item name="android:textColor">@color/dialtacts_theme_color</item>
         <item name="android:fontFamily">"sans-serif-medium"</item>
         <item name="android:focusable">true</item>
         <item name="android:singleLine">true</item>
         <item name="android:textAllCaps">true</item>
     </style>
 
+    <style name="PromoCardActionStyle" parent="TextActionStyle">
+        <item name="android:textColor">@color/promo_card_text</item>
+        <item name="android:textSize">@dimen/call_log_list_item_actions_text_size</item>
+    </style>
+
     <style name="VoicemailPlaybackLayoutTextStyle">
         <item name="android:textSize">14sp</item>
     </style>
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index ec92f3f..56f2cb1 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -50,6 +50,7 @@
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.GeoUtil;
 import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.util.UriUtils;
 import com.android.dialer.calllog.CallDetailHistoryAdapter;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
@@ -162,7 +163,7 @@
                     new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details));
 
             String lookupKey = contactUri == null ? null
-                    : ContactInfoHelper.getLookupKeyFromUri(contactUri);
+                    : UriUtils.getLookupKeyFromUri(contactUri);
 
             final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType);
 
@@ -268,7 +269,6 @@
     @Override
     public void onResume() {
         super.onResume();
-        PermissionsUtil.updateCachedPermissions(this);
         getCallDetails();
     }
 
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index e714c78..69cc146 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -66,6 +66,7 @@
 import com.android.contacts.common.widget.FloatingActionButtonController;
 import com.android.contacts.commonbind.analytics.AnalyticsUtil;
 import com.android.dialer.calllog.CallLogActivity;
+import com.android.dialer.calllog.CallLogFragment;
 import com.android.dialer.database.DialerDatabaseHelper;
 import com.android.dialer.dialpad.DialpadFragment;
 import com.android.dialer.dialpad.SmartDialNameMatcher;
@@ -101,6 +102,8 @@
 public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
         DialpadFragment.OnDialpadQueryChangedListener,
         OnListFragmentScrolledListener,
+        CallLogFragment.HostInterface,
+        DialpadFragment.HostInterface,
         ListsFragment.HostInterface,
         SpeedDialFragment.HostInterface,
         SearchFragment.HostInterface,
@@ -484,8 +487,6 @@
                     }
                 });
 
-        setupActivityOverlay();
-
         Trace.endSection();
 
         Trace.beginSection(TAG + " initialize smart dialing");
@@ -495,25 +496,11 @@
         Trace.endSection();
     }
 
-    private void setupActivityOverlay() {
-        final View activityOverlay = findViewById(R.id.activity_overlay);
-        activityOverlay.setOnTouchListener(new OnTouchListener() {
-            @Override
-            public boolean onTouch(View v, MotionEvent event) {
-                if (!mIsDialpadShown) {
-                    maybeExitSearchUi();
-                }
-                return false;
-            }
-        });
-    }
-
     @Override
     protected void onResume() {
         Trace.beginSection(TAG + " onResume");
         super.onResume();
 
-        PermissionsUtil.updateCachedPermissions(this);
         mStateSaved = false;
         if (mFirstLaunch) {
             displayFragment(getIntent());
@@ -621,7 +608,8 @@
     public void onClick(View view) {
         switch (view.getId()) {
             case R.id.floating_action_button:
-                if (mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_ALL_CONTACTS) {
+                if (mListsFragment.getCurrentTabIndex()
+                        == ListsFragment.TAB_INDEX_ALL_CONTACTS && !mInRegularSearch) {
                     DialerUtils.startActivityWithErrorToast(
                             this,
                             IntentUtil.getNewContactIntent(),
@@ -1146,7 +1134,16 @@
         } catch (Exception ignored) {
             // Skip any exceptions for this piece of code
         }
+    }
 
+    @Override
+    public boolean onDialpadSpacerTouchWithEmptyQuery() {
+        if (mInDialpadSearch && mSmartDialSearchFragment != null
+                && !mSmartDialSearchFragment.isShowingPermissionRequest()) {
+            hideDialpadFragment(true /* animate */, true /* clearDialpad */);
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -1208,6 +1205,24 @@
         mListsFragment.getRemoveView().setDragDropController(dragController);
     }
 
+    /**
+     * Implemented to satisfy {@link SpeedDialFragment.HostInterface}
+     */
+    @Override
+    public void showAllContactsTab() {
+        if (mListsFragment != null) {
+            mListsFragment.showTab(ListsFragment.TAB_INDEX_ALL_CONTACTS);
+        }
+    }
+
+    /**
+     * Implemented to satisfy {@link CallLogFragment.HostInterface}
+     */
+    @Override
+    public void showDialpad() {
+        showDialpadFragment(true);
+    }
+
     @Override
     public void onPickPhoneNumberAction(Uri dataUri) {
         // Specify call-origin so that users will see the previous tab instead of
@@ -1323,7 +1338,6 @@
         return mActionBarHeight;
     }
 
-
     private int getFabAlignment() {
         if (!mIsLandscape && !isInSearchUi() &&
                 mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) {
diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java
index 0f2f838..1f6c490 100644
--- a/src/com/android/dialer/calllog/CallLogActivity.java
+++ b/src/com/android/dialer/calllog/CallLogActivity.java
@@ -151,7 +151,6 @@
     protected void onResume() {
         mIsResumed = true;
         super.onResume();
-        PermissionsUtil.updateCachedPermissions(this);
         sendScreenViewForChildFragment(mViewPager.getCurrentItem());
     }
 
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 458c90b..5a87bc8 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -22,6 +22,8 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.v7.widget.RecyclerView;
 import android.os.Bundle;
 import android.os.Trace;
@@ -65,7 +67,8 @@
  * Adapter class to fill in data for the Call Log.
  */
 public class CallLogAdapter extends GroupingListAdapter
-        implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+        implements CallLogGroupBuilder.GroupCreator,
+                VoicemailPlaybackPresenter.OnVoicemailDeletedListener {
 
     /** Interface used to initiate a refresh of the content. */
     public interface CallFetcher {
@@ -94,7 +97,6 @@
     private final ContactInfoHelper mContactInfoHelper;
     private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
     private final CallFetcher mCallFetcher;
-    private ViewTreeObserver mViewTreeObserver = null;
 
     protected ContactInfoCache mContactInfoCache;
 
@@ -151,7 +153,7 @@
 
             if (mVoicemailPlaybackPresenter != null) {
                 // Always reset the voicemail playback state on expand or collapse.
-                mVoicemailPlaybackPresenter.reset();
+                mVoicemailPlaybackPresenter.resetAll();
             }
 
             if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) {
@@ -292,16 +294,6 @@
                 }
             };
 
-    @Override
-    public boolean onPreDraw() {
-        // We only wanted to listen for the first draw (and this is it).
-        unregisterPreDrawListener();
-        if (PermissionsUtil.hasContactsPermissions(mContext)) {
-            mContactInfoCache.start();
-        }
-        return true;
-    }
-
     public CallLogAdapter(
             Context context,
             CallFetcher callFetcher,
@@ -314,6 +306,9 @@
         mCallFetcher = callFetcher;
         mContactInfoHelper = contactInfoHelper;
         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+        if (mVoicemailPlaybackPresenter != null) {
+            mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
+        }
         mIsShowingRecentsTab = isShowingRecentsTab;
 
         mContactInfoCache = new ContactInfoCache(
@@ -370,21 +365,14 @@
         }
     }
 
-    /**
-     * Stop receiving onPreDraw() notifications.
-     */
-    private void unregisterPreDrawListener() {
-        if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
-            mViewTreeObserver.removeOnPreDrawListener(this);
-        }
-        mViewTreeObserver = null;
-    }
-
     public void invalidateCache() {
         mContactInfoCache.invalidate();
+    }
 
-        // Restart the request-processing thread after the next draw.
-        unregisterPreDrawListener();
+    public void startCache() {
+        if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+            mContactInfoCache.start();
+        }
     }
 
     public void pauseCache() {
@@ -540,13 +528,16 @@
         views.rowId = c.getLong(CallLogQuery.ID);
         // Store values used when the actions ViewStub is inflated on expansion.
         views.number = number;
+        views.displayNumber = details.displayNumber;
         views.numberPresentation = numberPresentation;
         views.callType = c.getInt(CallLogQuery.CALL_TYPE);
         views.accountHandle = accountHandle;
         views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
         // Stash away the Ids of the calls so that we can support deleting a row in the call log.
         views.callIds = getCallIds(c, count);
-
+        views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
+        views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType,
+                details.numberLabel);
         // Default case: an item in the call log.
         views.primaryActionView.setVisibility(View.VISIBLE);
 
@@ -577,15 +568,9 @@
             nameForDefaultImage = info.name;
         }
         views.setPhoto(info.photoId, info.photoUri, info.lookupUri, nameForDefaultImage,
-                isVoicemailNumber, mContactInfoHelper.isBusiness(info.sourceType));
+                isVoicemailNumber, views.isBusiness);
 
         mCallLogListItemHelper.setPhoneCallDetails(views, details);
-
-        // Listen for the first draw
-        if (mViewTreeObserver == null) {
-            mViewTreeObserver = views.rootView.getViewTreeObserver();
-            mViewTreeObserver.addOnPreDrawListener(this);
-        }
     }
 
     @Override
@@ -619,6 +604,12 @@
         return mIsShowingRecentsTab;
     }
 
+    @Override
+    public void onVoicemailDeleted(Uri uri) {
+        mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+        mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
+    }
+
     /**
      * Retrieves the day group of the previous call in the call log.  Used to determine if the day
      * group has changed and to trigger display of the day group text.
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 5d7c408..e7b7764 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.dialer.calllog;
 
+import static android.Manifest.permission.READ_CALL_LOG;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
@@ -26,6 +28,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.graphics.Rect;
@@ -46,6 +49,7 @@
 import android.widget.TextView;
 
 import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.util.ViewUtil;
 import com.android.dialer.R;
 import com.android.dialer.list.ListsFragment.HostInterface;
@@ -55,6 +59,8 @@
 import com.android.dialer.voicemail.VoicemailStatusHelper;
 import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+import com.android.dialer.widget.EmptyContentView;
+import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
 import com.android.dialerbind.ObjectFactory;
 
 import java.util.List;
@@ -63,8 +69,8 @@
  * Displays a list of call log entries. To filter for a particular kind of call
  * (all, missed or voicemails), specify it in the constructor.
  */
-public class CallLogFragment extends Fragment
-        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
+public class CallLogFragment extends Fragment implements CallLogQueryHandler.Listener,
+        CallLogAdapter.CallFetcher, OnEmptyViewActionButtonClickedListener {
     private static final String TAG = "CallLogFragment";
 
     /**
@@ -81,6 +87,8 @@
     // No date-based filtering.
     private static final int NO_DATE_LIMIT = 0;
 
+    private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
+
     private RecyclerView mRecyclerView;
     private LinearLayoutManager mLayoutManager;
     private CallLogAdapter mAdapter;
@@ -91,7 +99,7 @@
     /** Whether there is at least one voicemail source installed. */
     private boolean mVoicemailSourcesAvailable = false;
 
-    private View mEmptyListView;
+    private EmptyContentView mEmptyListView;
     private KeyguardManager mKeyguardManager;
 
     private boolean mEmptyLoaderRunning;
@@ -116,6 +124,8 @@
     private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
     private boolean mRefreshDataRequired = true;
 
+    private boolean mHasReadCallLogPermission = false;
+
     // Exactly same variable is in Fragment as a package private.
     private boolean mMenuVisible = true;
 
@@ -130,6 +140,16 @@
     // the date filter are included.  If zero, no date-based filtering occurs.
     private long mDateLimit = NO_DATE_LIMIT;
 
+    /*
+     * True if this instance of the CallLogFragment is the Recents screen shown in
+     * DialtactsActivity.
+     */
+    private boolean mIsRecentsFragment;
+
+    public interface HostInterface {
+        public void showDialpad();
+    }
+
     public CallLogFragment() {
         this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
     }
@@ -139,9 +159,7 @@
     }
 
     public CallLogFragment(int filterType, int logLimit) {
-        super();
-        mCallTypeFilter = filterType;
-        mLogLimit = logLimit;
+        this(filterType, logLimit, NO_DATE_LIMIT);
     }
 
     /**
@@ -162,7 +180,8 @@
      * @param dateLimit limits results to calls occurring on or after the specified date.
      */
     public CallLogFragment(int filterType, int logLimit, long dateLimit) {
-        this(filterType, logLimit);
+        mCallTypeFilter = filterType;
+        mLogLimit = logLimit;
         mDateLimit = dateLimit;
     }
 
@@ -175,6 +194,8 @@
             mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
         }
 
+        mIsRecentsFragment = mLogLimit != NO_LOG_LIMIT;
+
         final Activity activity = getActivity();
         final ContentResolver resolver = activity.getContentResolver();
         String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
@@ -268,7 +289,9 @@
         mRecyclerView.setHasFixedSize(true);
         mLayoutManager = new LinearLayoutManager(getActivity());
         mRecyclerView.setLayoutManager(mLayoutManager);
-        mEmptyListView = view.findViewById(R.id.empty_list_view);
+        mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+        mEmptyListView.setImage(R.drawable.empty_call_log);
+        mEmptyListView.setActionClickedListener(this);
 
         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
         boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT;
@@ -287,7 +310,6 @@
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-
         updateEmptyMessage(mCallTypeFilter);
         mAdapter.onRestoreInstanceState(savedInstanceState);
     }
@@ -305,7 +327,18 @@
     @Override
     public void onResume() {
         super.onResume();
+        final boolean hasReadCallLogPermission =
+                PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
+        if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
+            // We didn't have the permission before, and now we do. Force a refresh of the call log.
+            // Note that this code path always happens on a fresh start, but mRefreshDataRequired
+            // is already true in that case anyway.
+            mRefreshDataRequired = true;
+            updateEmptyMessage(mCallTypeFilter);
+        }
+        mHasReadCallLogPermission = hasReadCallLogPermission;
         refreshData();
+        mAdapter.startCache();
     }
 
     @Override
@@ -359,6 +392,17 @@
     }
 
     private void updateEmptyMessage(int filterType) {
+        final Context context = getActivity();
+        if (context == null) {
+            return;
+        }
+
+        if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
+            mEmptyListView.setDescription(R.string.permission_no_calllog);
+            mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+            return;
+        }
+
         final int messageId;
         switch (filterType) {
             case Calls.MISSED_TYPE:
@@ -374,8 +418,12 @@
                 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
                         + filterType);
         }
-        DialerUtils.configureEmptyListView(
-                mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
+        mEmptyListView.setDescription(messageId);
+        if (mIsRecentsFragment) {
+            mEmptyListView.setActionLabel(R.string.recentCalls_empty_action);
+        } else {
+            mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+        }
     }
 
     CallLogAdapter getAdapter() {
@@ -437,4 +485,30 @@
             CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
         }
     }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
+            requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
+        } else if (mIsRecentsFragment) {
+            // Show dialpad if we are the recents fragment.
+            ((HostInterface) activity).showDialpad();
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
+            if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+                // Force a refresh of the data since we were missing the permission before this.
+                mRefreshDataRequired = true;
+            }
+        }
+    }
 }
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index 361e1c7..0fa5e6d 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.calllog;
 
+import android.app.Activity;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.Intent;
@@ -29,18 +30,16 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
-import android.view.ViewTreeObserver;
 import android.widget.QuickContactBadge;
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.contacts.common.testing.NeededForTesting;
 import com.android.contacts.common.util.UriUtils;
 import com.android.dialer.R;
-import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
@@ -80,6 +79,7 @@
     public View addToExistingContactButtonView;
     public View sendMessageView;
     public View detailsButtonView;
+    public View callWithNoteButtonView;
 
     /**
      * The row Id for the first call associated with the call log entry.  Used as a key for the
@@ -100,12 +100,22 @@
     public String number;
 
     /**
+     * The formatted phone number to display.
+     */
+    public String displayNumber;
+
+    /**
      * The phone number presentation for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
     public int numberPresentation;
 
     /**
+     * The type of the phone number (e.g. main, work, etc).
+     */
+    public String numberType;
+
+    /**
      * The type of call for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
@@ -131,6 +141,11 @@
     public CharSequence nameOrNumber;
 
     /**
+     * Whether this row is for a business or not.
+     */
+    public boolean isBusiness;
+
+    /**
      * The contact info for the contact displayed in this list item.
      */
     public ContactInfo info;
@@ -245,6 +260,9 @@
 
             detailsButtonView = actionsView.findViewById(R.id.details_action);
             detailsButtonView.setOnClickListener(this);
+
+            callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action);
+            callWithNoteButtonView.setOnClickListener(this);
         }
 
         bindActionButtons();
@@ -349,6 +367,13 @@
         sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number));
 
         mCallLogListItemHelper.setActionContentDescriptions(this);
+
+        boolean supportsCallSubject =
+                mTelecomCallLogCache.doesAccountSupportCallSubject(accountHandle);
+        boolean isVoicemailNumber =
+                mTelecomCallLogCache.isVoicemailNumber(accountHandle, number);
+        callWithNoteButtonView.setVisibility(
+                supportsCallSubject && !isVoicemailNumber ? View.VISIBLE : View.GONE);
     }
 
     /**
@@ -403,7 +428,7 @@
 
         String lookupKey = null;
         if (contactUri != null) {
-            lookupKey = ContactInfoHelper.getLookupKeyFromUri(contactUri);
+            lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
         }
 
         DefaultImageRequest request = new DefaultImageRequest(
@@ -423,6 +448,19 @@
         if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) {
             mVoicemailPrimaryActionButtonClicked = true;
             mExpandCollapseListener.onClick(primaryActionView);
+        } else if (view.getId() == R.id.call_with_note_action) {
+            CallSubjectDialog.start(
+                    (Activity) mContext,
+                    info.photoId,
+                    info.photoUri,
+                    info.lookupUri,
+                    (String) nameOrNumber /* top line of contact view in call subject dialog */,
+                    isBusiness,
+                    number, /* callable number used for ACTION_CALL intent */
+                    TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact
+                                                                           view in dialog. */
+                    numberType, /* phone number type (e.g. mobile) in second line of contact view */
+                    accountHandle);
         } else {
             final IntentProvider intentProvider = (IntentProvider) view.getTag();
             if (intentProvider != null) {
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index 9a660e1..2e07a03 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -298,6 +298,10 @@
      */
     public void updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo,
             ContactInfo callLogInfo) {
+        if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
+            return;
+        }
+
         final ContentValues values = new ContentValues();
         boolean needsUpdate = false;
 
@@ -390,24 +394,6 @@
     }
 
     /**
-     * Parses the given URI to determine the original lookup key of the contact.
-     */
-    public static String getLookupKeyFromUri(Uri lookupUri) {
-        // Would be nice to be able to persist the lookup key somehow to avoid having to parse
-        // the uri entirely just to retrieve the lookup key, but every uri is already parsed
-        // once anyway to check if it is an encoded JSON uri, so this has negligible effect
-        // on performance.
-        if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) {
-            final List<String> segments = lookupUri.getPathSegments();
-            // This returns the third path segment of the uri, where the lookup key is located.
-            // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
-            return (segments.size() < 3) ? null : Uri.encode(segments.get(2));
-        } else {
-            return null;
-        }
-    }
-
-    /**
      * Returns the contact information stored in an entry of the call log.
      *
      * @param c A cursor pointing to an entry in the call log.
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index d0553b4..a6d165e 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -302,6 +302,9 @@
                     newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
                 }
                 return newCalls;
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+                return null;
             } finally {
                 MoreCloseables.closeQuietly(cursor);
             }
@@ -371,6 +374,9 @@
                         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();
diff --git a/src/com/android/dialer/calllog/PhoneAccountUtils.java b/src/com/android/dialer/calllog/PhoneAccountUtils.java
index 7eaa523..143d13e 100644
--- a/src/com/android/dialer/calllog/PhoneAccountUtils.java
+++ b/src/com/android/dialer/calllog/PhoneAccountUtils.java
@@ -84,6 +84,21 @@
     }
 
     /**
+     * Determine whether a phone account supports call subjects.
+     *
+     * @return {@code true} if call subjects are supported, {@code false} otherwise.
+     */
+    public static boolean getAccountSupportsCallSubject(Context context,
+            PhoneAccountHandle accountHandle) {
+        TelecomManager telecomManager =
+                (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+        final PhoneAccount account = telecomManager.getPhoneAccount(accountHandle);
+
+        return account == null ? false :
+                account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+    }
+
+    /**
      * Retrieve the account metadata, but if the account does not exist or the device has only a
      * single registered and enabled account, return null.
      */
diff --git a/src/com/android/dialer/calllog/TelecomCallLogCache.java b/src/com/android/dialer/calllog/TelecomCallLogCache.java
index ec1d241..7071669 100644
--- a/src/com/android/dialer/calllog/TelecomCallLogCache.java
+++ b/src/com/android/dialer/calllog/TelecomCallLogCache.java
@@ -52,6 +52,7 @@
             new HashMap<>();
     private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
     private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
+    private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>();
 
     private boolean mHasCheckedForVideoEnabled;
     private boolean mIsVideoEnabled;
@@ -64,6 +65,7 @@
         mVoicemailQueryCache.clear();
         mPhoneAccountLabelCache.clear();
         mPhoneAccountColorCache.clear();
+        mPhoneAccountCallWithNoteCache.clear();
 
         mHasCheckedForVideoEnabled = false;
         mIsVideoEnabled = false;
@@ -121,4 +123,22 @@
         }
         return mIsVideoEnabled;
     }
+
+    /**
+     * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note)
+     * for outgoing calls.
+     *
+     * @param accountHandle The PhoneAccount handle.
+     * @return {@code true} if calling with a note is supported, {@code false} otherwise.
+     */
+    public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) {
+        if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) {
+            return mPhoneAccountCallWithNoteCache.get(accountHandle);
+        } else {
+            Boolean supportsCallWithNote =
+                    PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle);
+            mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote);
+            return supportsCallWithNote;
+        }
+    }
 }
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index b18069f..3792a1d 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -50,6 +50,7 @@
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -67,7 +68,9 @@
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import com.android.contacts.common.CallUtil;
 import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.common.util.PhoneNumberFormatter;
 import com.android.contacts.common.util.StopWatch;
@@ -80,7 +83,6 @@
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
 import com.android.phone.common.CallLogAsync;
-import com.android.phone.common.HapticFeedback;
 import com.android.phone.common.animation.AnimUtils;
 import com.android.phone.common.dialpad.DialpadKeyButton;
 import com.android.phone.common.dialpad.DialpadView;
@@ -135,6 +137,15 @@
         void onDialpadQueryChanged(String query);
     }
 
+    public interface HostInterface {
+        /**
+         * Notifies the parent activity that the space above the dialpad has been tapped with
+         * no query in the dialpad present. In most situations this will cause the dialpad to
+         * be dismissed, unless there happens to be content showing.
+         */
+        boolean onDialpadSpacerTouchWithEmptyQuery();
+    }
+
     private static final boolean DEBUG = DialtactsActivity.DEBUG;
 
     // This is the amount of screen the dialpad fragment takes up when fully displayed
@@ -197,9 +208,6 @@
     // determines if we want to playback local DTMF tones.
     private boolean mDTMFToneEnabled;
 
-    // Vibration (haptic feedback) for dialer key presses.
-    private final HapticFeedback mHaptic = new HapticFeedback();
-
     /** Identifier for the "Add Call" intent extra. */
     private static final String ADD_CALL_MODE_KEY = "add_call_mode";
 
@@ -319,13 +327,6 @@
 
         mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
 
-        try {
-            mHaptic.init(getActivity(),
-                         getResources().getBoolean(R.bool.config_enable_dialer_key_vibration));
-        } catch (Resources.NotFoundException nfe) {
-             Log.e(TAG, "Vibrate control bool missing.", nfe);
-        }
-
         mProhibitedPhoneNumberRegexp = getResources().getString(
                 R.string.config_prohibited_phone_number_regexp);
 
@@ -385,7 +386,9 @@
             @Override
             public boolean onTouch(View v, MotionEvent event) {
                 if (isDigitsEmpty()) {
-                    hideAndClearDialpad(true);
+                    if (getActivity() != null) {
+                        return ((HostInterface) getActivity()).onDialpadSpacerTouchWithEmptyQuery();
+                    }
                     return true;
                 }
                 return false;
@@ -638,9 +641,6 @@
 
         stopWatch.lap("dtwd");
 
-        // Retrieve the haptic feedback setting.
-        mHaptic.checkSystemSetting();
-
         stopWatch.lap("hptc");
 
         mPressedDialpadKeys.clear();
@@ -774,7 +774,7 @@
                 break;
         }
 
-        mHaptic.vibrate();
+        getView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
         KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
         mDigits.onKeyDown(keyCode, event);
 
@@ -885,9 +885,12 @@
 
                 boolean enable = !isDigitsEmpty();
                 for (int i = 0; i < menu.size(); i++) {
-                    menu.getItem(i).setEnabled(enable);
+                    MenuItem item = menu.getItem(i);
+                    item.setEnabled(enable);
+                    if (item.getItemId() == R.id.menu_call_with_note) {
+                        item.setVisible(CallUtil.isCallWithSubjectSupported(getContext()));
+                    }
                 }
-
                 super.show();
             }
         };
@@ -900,7 +903,7 @@
     public void onClick(View view) {
         switch (view.getId()) {
             case R.id.dialpad_floating_action_button:
-                mHaptic.vibrate();
+                view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                 handleDialButtonPressed();
                 break;
             case R.id.deleteButton: {
@@ -1459,6 +1462,10 @@
             case R.id.menu_add_wait:
                 updateDialString(WAIT);
                 return true;
+            case R.id.menu_call_with_note:
+                CallSubjectDialog.start(getActivity(), mDigits.getText().toString());
+                hideAndClearDialpad(false);
+                return true;
             default:
                 return false;
         }
diff --git a/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java b/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
index fd3d512..172a4ef 100644
--- a/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
+++ b/src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
@@ -16,6 +16,9 @@
 
 package com.android.dialer.interactions;
 
+import static android.Manifest.permission.READ_CONTACTS;
+import static android.Manifest.permission.WRITE_CONTACTS;
+
 import android.content.BroadcastReceiver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -41,7 +44,8 @@
 
     @Override
     public void onReceive(final Context context, Intent intent) {
-        if (!PermissionsUtil.hasContactsPermissions(context)) {
+        if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)
+            || !PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
             return;
         }
         if (intent != null && Intent.ACTION_NEW_OUTGOING_CALL.equals(intent.getAction())) {
@@ -65,14 +69,29 @@
         // If the contact is not demoted, this will not do anything. Otherwise, it will
         // restore it to an unpinned position. If it was a frequently called contact, it will
         // show up once again show up on the favorites screen.
-        PinnedPositions.undemote(context.getContentResolver(), id);
+        if (PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
+            try {
+                PinnedPositions.undemote(context.getContentResolver(), id);
+            } catch (SecurityException e) {
+                // Just in case
+            }
+        }
     }
 
     private long getContactIdFromPhoneNumber(Context context, String number) {
+        if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)) {
+            return NO_CONTACT_FOUND;
+        }
         final Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
                 Uri.encode(number));
-        final Cursor cursor = context.getContentResolver().query(contactUri, new String[] {
-                PhoneLookup._ID}, null, null, null);
+        final Cursor cursor;
+        try {
+            cursor = context.getContentResolver().query(contactUri, new String[] {
+                    PhoneLookup._ID}, null, null, null);
+        } catch (SecurityException e) {
+            // Just in case
+            return NO_CONTACT_FOUND;
+        }
         if (cursor == null) {
             return NO_CONTACT_FOUND;
         }
diff --git a/src/com/android/dialer/list/AllContactsFragment.java b/src/com/android/dialer/list/AllContactsFragment.java
index d34250b..0f31ff8 100644
--- a/src/com/android/dialer/list/AllContactsFragment.java
+++ b/src/com/android/dialer/list/AllContactsFragment.java
@@ -16,7 +16,14 @@
 
 package com.android.dialer.list;
 
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
 import android.content.Loader;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -34,13 +41,30 @@
 import com.android.contacts.common.util.ViewUtil;
 import com.android.dialer.R;
 import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.widget.EmptyContentView;
+import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
 
 /**
  * Fragments to show all contacts with phone numbers.
  */
-public class AllContactsFragment extends ContactEntryListFragment<ContactEntryListAdapter> {
+public class AllContactsFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+        implements OnEmptyViewActionButtonClickedListener {
 
-    private View mEmptyListView;
+    private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
+
+    private EmptyContentView mEmptyListView;
+
+    /**
+     * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS
+     * permission is granted via the UI in another fragment.
+     */
+    private BroadcastReceiver mReadContactsPermissionGrantedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            reloadData();
+        }
+    };
 
     public AllContactsFragment() {
         setQuickContactEnabled(false);
@@ -55,9 +79,10 @@
     public void onViewCreated(View view, android.os.Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
-        mEmptyListView = view.findViewById(R.id.empty_list_view);
-        DialerUtils.configureEmptyListView(mEmptyListView, R.drawable.empty_contacts,
-                R.string.all_contacts_empty, getResources());
+        mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+        mEmptyListView.setImage(R.drawable.empty_contacts);
+        mEmptyListView.setDescription(R.string.all_contacts_empty);
+        mEmptyListView.setActionClickedListener(this);
         getListView().setEmptyView(mEmptyListView);
         mEmptyListView.setVisibility(View.GONE);
 
@@ -65,9 +90,29 @@
     }
 
     @Override
+    public void onStart() {
+        super.onStart();
+        PermissionsUtil.registerPermissionReceiver(getActivity(),
+                mReadContactsPermissionGrantedReceiver, READ_CONTACTS);
+    }
+
+    @Override
+    public void onStop() {
+        PermissionsUtil.unregisterPermissionReceiver(getActivity(),
+                mReadContactsPermissionGrantedReceiver);
+        super.onStop();
+    }
+
+    @Override
     protected void startLoading() {
-        if (PermissionsUtil.hasContactsPermissions(getActivity())) {
+        if (PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
             super.startLoading();
+            mEmptyListView.setDescription(R.string.all_contacts_empty);
+            mEmptyListView.setActionLabel(R.string.all_contacts_empty_add_contact_action);
+        } else {
+            mEmptyListView.setDescription(R.string.permission_no_contacts);
+            mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+            mEmptyListView.setVisibility(View.VISIBLE);
         }
     }
 
@@ -82,10 +127,6 @@
 
     @Override
     protected ContactEntryListAdapter createListAdapter() {
-        if (!PermissionsUtil.hasContactsPermissions(getActivity())) {
-            return new EmptyContactsListAdapter(getActivity());
-        }
-
         final DefaultContactListAdapter adapter = new DefaultContactListAdapter(getActivity()) {
             @Override
             protected void bindView(View itemView, int partition, Cursor cursor, int position) {
@@ -102,7 +143,7 @@
 
     @Override
     protected View inflateView(LayoutInflater inflater, ViewGroup container) {
-        return inflater.inflate(R.layout.show_all_contacts_fragment, null);
+        return inflater.inflate(R.layout.all_contacts_fragment, null);
     }
 
     @Override
@@ -118,4 +159,31 @@
     protected void onItemClick(int position, long id) {
         // Do nothing. Implemented to satisfy ContactEntryListFragment.
     }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
+            requestPermissions(new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+        } else {
+            // Add new contact
+            DialerUtils.startActivityWithErrorToast(activity, IntentUtil.getNewContactIntent(),
+                    R.string.add_contact_not_available);
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+            if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+                // Force a refresh of the data since we were missing the permission before this.
+                reloadData();
+            }
+        }
+    }
 }
diff --git a/src/com/android/dialer/list/EmptyContactsListAdapter.java b/src/com/android/dialer/list/EmptyContactsListAdapter.java
deleted file mode 100644
index 54bd477..0000000
--- a/src/com/android/dialer/list/EmptyContactsListAdapter.java
+++ /dev/null
@@ -1,47 +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.list;
-
-import android.content.Context;
-import android.content.CursorLoader;
-
-import com.android.contacts.common.list.ContactEntryListAdapter;
-
-/**
- * Used to display an empty contact list when we don't have the permissions to read contacts.
- */
-public class EmptyContactsListAdapter extends ContactEntryListAdapter {
-
-    public EmptyContactsListAdapter(Context context) {
-        super(context);
-    }
-
-    @Override
-    public String getContactDisplayName(int position) {
-        return null;
-    }
-
-    @Override
-    public void configureLoader(CursorLoader loader, long directoryId) {
-        loader.setUri(null);
-    }
-
-    @Override
-    public int getCount() {
-        return 0;
-    }
-}
diff --git a/src/com/android/dialer/list/RegularSearchFragment.java b/src/com/android/dialer/list/RegularSearchFragment.java
index 19c7321..b7e26d6 100644
--- a/src/com/android/dialer/list/RegularSearchFragment.java
+++ b/src/com/android/dialer/list/RegularSearchFragment.java
@@ -15,16 +15,29 @@
  */
 package com.android.dialer.list;
 
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.READ_CONTACTS;
+
+import android.app.Activity;
+import android.content.pm.PackageManager;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
 import com.android.contacts.common.list.ContactEntryListAdapter;
 import com.android.contacts.common.list.PinnedHeaderListView;
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.commonbind.analytics.AnalyticsUtil;
 import com.android.dialerbind.ObjectFactory;
-import com.android.dialer.service.CachedNumberLookupService;
 
-public class RegularSearchFragment extends SearchFragment {
+import com.android.dialer.R;
+import com.android.dialer.service.CachedNumberLookupService;
+import com.android.dialer.widget.EmptyContentView;
+import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+
+public class RegularSearchFragment extends SearchFragment
+        implements OnEmptyViewActionButtonClickedListener {
+
+    private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
 
     private static final int SEARCH_DIRECTORY_RESULT_LIMIT = 5;
 
@@ -68,4 +81,38 @@
                     adapter.getContactInfo(mCachedNumberLookupService, position));
         }
     }
+
+    @Override
+    protected void setupEmptyView() {
+        if (mEmptyView != null && getActivity() != null) {
+            if (!PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
+                mEmptyView.setImage(R.drawable.empty_contacts);
+                mEmptyView.setActionLabel(R.string.permission_single_turn_on);
+                mEmptyView.setDescription(R.string.permission_no_search);
+                mEmptyView.setActionClickedListener(this);
+            } else {
+                mEmptyView.setImage(EmptyContentView.NO_IMAGE);
+                mEmptyView.setActionLabel(EmptyContentView.NO_LABEL);
+                mEmptyView.setDescription(EmptyContentView.NO_LABEL);
+            }
+        }
+    }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        requestPermissions(new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+            setupEmptyView();
+        }
+    }
 }
diff --git a/src/com/android/dialer/list/SearchFragment.java b/src/com/android/dialer/list/SearchFragment.java
index 77ab291..315cfb9 100644
--- a/src/com/android/dialer/list/SearchFragment.java
+++ b/src/com/android/dialer/list/SearchFragment.java
@@ -15,6 +15,8 @@
  */
 package com.android.dialer.list;
 
+import static android.Manifest.permission.READ_CONTACTS;
+
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
@@ -50,6 +52,7 @@
 import com.android.dialer.R;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
+import com.android.dialer.widget.EmptyContentView;
 import com.android.phone.common.animation.AnimUtils;
 
 public class SearchFragment extends PhoneNumberPickerFragment {
@@ -79,6 +82,8 @@
 
     private HostInterface mActivity;
 
+    protected EmptyContentView mEmptyView;
+
     public interface HostInterface {
         public boolean isActionBarShowing();
         public boolean isDialpadShown();
@@ -125,6 +130,13 @@
 
         final ListView listView = getListView();
 
+        if (mEmptyView == null) {
+            mEmptyView = new EmptyContentView(getActivity());
+            ((ViewGroup) getListView().getParent()).addView(mEmptyView);
+            getListView().setEmptyView(mEmptyView);
+            setupEmptyView();
+        }
+
         listView.setBackgroundColor(res.getColor(R.color.background_dialer_results));
         listView.setClipToPadding(false);
         setVisibleScrollbarEnabled(false);
@@ -341,7 +353,7 @@
 
     @Override
     protected void startLoading() {
-        if (PermissionsUtil.hasContactsPermissions(getActivity())) {
+        if (PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) {
             super.startLoading();
         } else if (TextUtils.isEmpty(getQueryString())) {
             // Clear out any existing call shortcuts.
@@ -354,6 +366,8 @@
             // list.
             getAdapter().notifyDataSetChanged();
         }
+
+        setupEmptyView();
     }
 
     public void setOnTouchListener(View.OnTouchListener onTouchListener) {
@@ -371,4 +385,6 @@
         }
         return parent;
     }
+
+    protected void setupEmptyView() {}
 }
diff --git a/src/com/android/dialer/list/SmartDialSearchFragment.java b/src/com/android/dialer/list/SmartDialSearchFragment.java
index 082bc43..72d3abf 100644
--- a/src/com/android/dialer/list/SmartDialSearchFragment.java
+++ b/src/com/android/dialer/list/SmartDialSearchFragment.java
@@ -15,21 +15,33 @@
  */
 package com.android.dialer.list;
 
+import static android.Manifest.permission.CALL_PHONE;
+
+import android.app.Activity;
 import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.View;
 
 import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.dialpad.SmartDialCursorLoader;
+import com.android.dialer.R;
+import com.android.dialer.widget.EmptyContentView;
+
+import java.util.ArrayList;
 
 /**
  * Implements a fragment to load and display SmartDial search results.
  */
-public class SmartDialSearchFragment extends SearchFragment {
+public class SmartDialSearchFragment extends SearchFragment
+        implements EmptyContentView.OnEmptyViewActionButtonClickedListener {
     private static final String TAG = SmartDialSearchFragment.class.getSimpleName();
 
+    private static final int CALL_PHONE_PERMISSION_REQUEST_CODE = 1;
+
     /**
      * Creates a SmartDialListAdapter to display and operate on search results.
      */
@@ -69,4 +81,42 @@
         final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter();
         return adapter.getDataUri(position);
     }
+
+    @Override
+    protected void setupEmptyView() {
+        if (mEmptyView != null && getActivity() != null) {
+            if (!PermissionsUtil.hasPermission(getActivity(), CALL_PHONE)) {
+                mEmptyView.setImage(R.drawable.empty_contacts);
+                mEmptyView.setActionLabel(R.string.permission_single_turn_on);
+                mEmptyView.setDescription(R.string.permission_place_call);
+                mEmptyView.setActionClickedListener(this);
+            } else {
+                mEmptyView.setImage(EmptyContentView.NO_IMAGE);
+                mEmptyView.setActionLabel(EmptyContentView.NO_LABEL);
+                mEmptyView.setDescription(EmptyContentView.NO_LABEL);
+            }
+        }
+    }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        requestPermissions(new String[] {CALL_PHONE}, CALL_PHONE_PERMISSION_REQUEST_CODE);
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        if (requestCode == CALL_PHONE_PERMISSION_REQUEST_CODE) {
+            setupEmptyView();
+        }
+    }
+
+    public boolean isShowingPermissionRequest() {
+        return mEmptyView != null && mEmptyView.isShowingContent();
+    }
 }
diff --git a/src/com/android/dialer/list/SpeedDialFragment.java b/src/com/android/dialer/list/SpeedDialFragment.java
index bf95758..324caef 100644
--- a/src/com/android/dialer/list/SpeedDialFragment.java
+++ b/src/com/android/dialer/list/SpeedDialFragment.java
@@ -15,6 +15,8 @@
  */
 package com.android.dialer.list;
 
+import static android.Manifest.permission.READ_CONTACTS;
+
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -23,6 +25,7 @@
 import android.app.LoaderManager;
 import android.content.CursorLoader;
 import android.content.Loader;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.graphics.Rect;
 import android.net.Uri;
@@ -50,6 +53,7 @@
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.R;
 import com.android.dialer.util.DialerUtils;
+import com.android.dialer.widget.EmptyContentView;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -58,7 +62,10 @@
  * This fragment displays the user's favorite/frequent contacts in a grid.
  */
 public class SpeedDialFragment extends Fragment implements OnItemClickListener,
-        PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener {
+        PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener,
+        EmptyContentView.OnEmptyViewActionButtonClickedListener {
+
+    private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
 
     /**
      * By default, the animation code assumes that all items in a list view are of the same height
@@ -81,6 +88,7 @@
 
     public interface HostInterface {
         public void setDragDropController(DragDropController controller);
+        public void showAllContactsTab();
     }
 
     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
@@ -157,7 +165,7 @@
     /**
      * Layout used when there are no favorites.
      */
-    private View mEmptyView;
+    private EmptyContentView mEmptyView;
 
     private final ContactTileView.Listener mContactTileAdapterListener =
             new ContactTileAdapterListener();
@@ -197,9 +205,16 @@
             if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) {
                 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null,
                         mContactTileLoaderListener);
+
             } else {
                 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
             }
+
+            mEmptyView.setDescription(R.string.speed_dial_empty);
+            mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action);
+        } else {
+            mEmptyView.setDescription(R.string.permission_no_speeddial);
+            mEmptyView.setActionLabel(R.string.permission_single_turn_on);
         }
         Trace.endSection();
     }
@@ -221,9 +236,9 @@
                 (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay);
         mListView.setDragShadowOverlay(dragShadowOverlay);
 
-        mEmptyView = mParentView.findViewById(R.id.empty_list_view);
-        DialerUtils.configureEmptyListView(
-                mEmptyView, R.drawable.empty_speed_dial, R.string.speed_dial_empty, getResources());
+        mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view);
+        mEmptyView.setImage(R.drawable.empty_speed_dial);
+        mEmptyView.setActionClickedListener(this);
 
         mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame);
 
@@ -449,4 +464,29 @@
     public AbsListView getListView() {
         return mListView;
     }
+
+    @Override
+    public void onEmptyViewActionButtonClicked() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
+
+        if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
+            requestPermissions(new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+        } else {
+            // Switch tabs
+            ((HostInterface) activity).showAllContactsTab();
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+            if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+                PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS);
+            }
+        }
+    }
 }
diff --git a/src/com/android/dialer/settings/DefaultRingtonePreference.java b/src/com/android/dialer/settings/DefaultRingtonePreference.java
index c12e717..a174381 100644
--- a/src/com/android/dialer/settings/DefaultRingtonePreference.java
+++ b/src/com/android/dialer/settings/DefaultRingtonePreference.java
@@ -16,12 +16,17 @@
 
 package com.android.dialer.settings;
 
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.preference.RingtonePreference;
+import android.provider.Settings;
 import android.util.AttributeSet;
+import android.widget.Toast;
+
+import com.android.dialer.R;
 
 /**
  * RingtonePreference which doesn't show default ringtone setting.
@@ -44,6 +49,13 @@
 
     @Override
     protected void onSaveRingtone(Uri ringtoneUri) {
+        if (!Settings.System.canWrite(getContext())) {
+            Toast.makeText(
+                    getContext(),
+                    getContext().getResources().getString(R.string.toast_cannot_write_system_settings),
+                    Toast.LENGTH_SHORT).show();
+            return;
+        }
         RingtoneManager.setActualDefaultRingtoneUri(getContext(), getRingtoneType(), ringtoneUri);
     }
 
diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java
index d54053b..c459d35 100644
--- a/src/com/android/dialer/settings/DialerSettingsActivity.java
+++ b/src/com/android/dialer/settings/DialerSettingsActivity.java
@@ -1,23 +1,22 @@
 package com.android.dialer.settings;
 
-import com.google.common.collect.Lists;
-
-import android.content.ComponentName;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.os.UserHandle;
+import android.os.Process;
 import android.os.UserManager;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
-import android.preference.PreferenceActivity.Header;
+import android.provider.Settings;
 import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
-import android.text.TextUtils;
+import android.util.Log;
 import android.view.MenuItem;
+import android.widget.Toast;
 
-import com.android.dialer.DialtactsActivity;
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.R;
 
 import java.util.List;
@@ -26,8 +25,6 @@
 
     protected SharedPreferences mPreferences;
 
-    private static final int OWNER_HANDLE_ID = 0;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -44,6 +41,7 @@
         Header soundSettingsHeader = new Header();
         soundSettingsHeader.titleRes = R.string.sounds_and_vibration_title;
         soundSettingsHeader.fragment = SoundSettingsFragment.class.getName();
+        soundSettingsHeader.id = R.id.settings_header_sounds_and_vibration;
         target.add(soundSettingsHeader);
 
         Header quickResponseSettingsHeader = new Header();
@@ -91,6 +89,24 @@
     }
 
     @Override
+    public void onHeaderClick(Header header, int position) {
+        if (header.id == R.id.settings_header_sounds_and_vibration) {
+            // If we don't have the permission to write to system settings, go to system sound
+            // settings instead. Otherwise, perform the super implementation (which launches our
+            // own preference fragment.
+            if (!Settings.System.canWrite(this)) {
+                Toast.makeText(
+                        this,
+                        getResources().getString(R.string.toast_cannot_write_system_settings),
+                        Toast.LENGTH_SHORT).show();
+                startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
+                return;
+            }
+        }
+        super.onHeaderClick(header, position);
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == android.R.id.home) {
             onBackPressed();
diff --git a/src/com/android/dialer/settings/SoundSettingsFragment.java b/src/com/android/dialer/settings/SoundSettingsFragment.java
index c7bb2d3..8384700 100644
--- a/src/com/android/dialer/settings/SoundSettingsFragment.java
+++ b/src/com/android/dialer/settings/SoundSettingsFragment.java
@@ -16,7 +16,9 @@
 
 package com.android.dialer.settings;
 
+import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.Intent;
 import android.media.RingtoneManager;
 import android.os.Bundle;
 import android.os.Handler;
@@ -30,7 +32,10 @@
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyManager;
+import android.view.MenuItem;
+import android.widget.Toast;
 
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.R;
 import com.android.phone.common.util.SettingsUtil;
 
@@ -51,6 +56,9 @@
     private static final int NO_VIBRATION_FOR_CALLS = 0;
     private static final int DO_VIBRATION_FOR_CALLS = 1;
 
+
+    private static final int DTMF_TONE_TYPE_NORMAL = 0;
+
     private static final int SHOW_CARRIER_SETTINGS = 0;
     private static final int HIDE_CARRIER_SETTINGS = 1;
 
@@ -112,7 +120,8 @@
             mDtmfToneLength.setOnPreferenceChangeListener(this);
             mDtmfToneLength.setValueIndex(
                     Settings.System.getInt(context.getContentResolver(),
-                        Settings.System.DTMF_TONE_WHEN_DIALING, PLAY_DTMF_TONE));
+                        Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+                        DTMF_TONE_TYPE_NORMAL));
         } else {
             getPreferenceScreen().removePreference(mDtmfToneLength);
             mDtmfToneLength = null;
@@ -123,6 +132,13 @@
     public void onResume() {
         super.onResume();
 
+        if (!Settings.System.canWrite(getContext())) {
+            // If the user launches this setting fragment, then toggles the WRITE_SYSTEM_SETTINGS
+            // AppOp, then close the fragment since there is nothing useful to do.
+            getActivity().onBackPressed();
+            return;
+        }
+
         if (mVibrateWhenRinging != null) {
             mVibrateWhenRinging.setChecked(shouldVibrateWhenRinging());
         }
@@ -139,6 +155,14 @@
      */
     @Override
     public boolean onPreferenceChange(Preference preference, Object objValue) {
+        if (!Settings.System.canWrite(getContext())) {
+            // A user shouldn't be able to get here, but this protects against monkey crashes.
+            Toast.makeText(
+                    getContext(),
+                    getResources().getString(R.string.toast_cannot_write_system_settings),
+                    Toast.LENGTH_SHORT).show();
+            return true;
+        }
         if (preference == mVibrateWhenRinging) {
             boolean doVibrate = (Boolean) objValue;
             Settings.System.putInt(getActivity().getContentResolver(),
@@ -157,6 +181,13 @@
      */
     @Override
     public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+        if (!Settings.System.canWrite(getContext())) {
+            Toast.makeText(
+                    getContext(),
+                    getResources().getString(R.string.toast_cannot_write_system_settings),
+                    Toast.LENGTH_SHORT).show();
+            return true;
+        }
         if (preference == mPlayDtmfTone) {
             Settings.System.putInt(getActivity().getContentResolver(),
                     Settings.System.DTMF_TONE_WHEN_DIALING,
diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java
index a44c2ee..e25ada5 100644
--- a/src/com/android/dialer/util/DialerUtils.java
+++ b/src/com/android/dialer/util/DialerUtils.java
@@ -40,6 +40,7 @@
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.interactions.TouchPointManager;
 import com.android.dialer.R;
+import com.android.dialer.widget.EmptyContentView;
 import com.android.incallui.CallCardFragment;
 import com.android.incallui.Log;
 
@@ -116,27 +117,6 @@
     }
 
     /**
-     * Sets the image asset and text for an empty list view (see empty_list_view.xml).
-     *
-     * @param emptyListView The empty list view.
-     * @param imageResId The resource id for the drawable to set as the image.
-     * @param strResId The resource id for the string to set as the message.
-     * @param res The resources to obtain the image and string from.
-     */
-    public static void configureEmptyListView(
-            View emptyListView, int imageResId, int strResId, Resources res) {
-        ImageView emptyListViewImage =
-                (ImageView) emptyListView.findViewById(R.id.emptyListViewImage);
-
-        emptyListViewImage.setImageDrawable(res.getDrawable(imageResId));
-        emptyListViewImage.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-
-        TextView emptyListViewMessage =
-                (TextView) emptyListView.findViewById(R.id.emptyListViewMessage);
-        emptyListViewMessage.setText(res.getString(strResId));
-    }
-
-    /**
      * Closes an {@link AutoCloseable}, silently ignoring any checked exceptions. Does nothing if
      * null.
      *
diff --git a/src/com/android/dialer/util/IntentUtil.java b/src/com/android/dialer/util/IntentUtil.java
index 7ed99c9..2ce3bd1 100644
--- a/src/com/android/dialer/util/IntentUtil.java
+++ b/src/com/android/dialer/util/IntentUtil.java
@@ -146,7 +146,7 @@
     }
 
     public static Intent getSendSmsIntent(CharSequence phoneNumber) {
-        return new Intent(Intent.ACTION_VIEW, Uri.parse(SMS_URI_PREFIX + phoneNumber));
+        return new Intent(Intent.ACTION_SENDTO, Uri.parse(SMS_URI_PREFIX + phoneNumber));
     }
 
     public static Intent getNewContactIntent() {
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 2017bc5..158ed58 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -183,7 +183,9 @@
             if (mPresenter == null) {
                 return;
             }
+            mPresenter.pausePlayback();
             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null);
+            mPresenter.onVoicemailDeleted();
         }
     };
 
@@ -277,7 +279,7 @@
         }
 
         disableUiElements();
-        mPositionText.setText(getString(R.string.voicemail_playback_error));
+        mStateText.setText(getString(R.string.voicemail_playback_error));
     }
 
 
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index f76af59..9319b6e 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -92,6 +92,10 @@
         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
     }
 
+    public interface OnVoicemailDeletedListener {
+        void onVoicemailDeleted(Uri uri);
+    }
+
     /** The enumeration of {@link AsyncTask} objects we use in this class. */
     public enum Tasks {
         CHECK_FOR_CONTENT,
@@ -155,6 +159,8 @@
     private PowerManager.WakeLock mProximityWakeLock;
     private AudioManager mAudioManager;
 
+    private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+
     /**
      * Obtain singleton instance of this class. Use a single instance to provide a consistent
      * listener to the AudioManager when requesting and abandoning audio focus.
@@ -260,7 +266,17 @@
     }
 
     /**
-     * Reset the presenter for playback.
+     * Reset the presenter for playback back to its original state.
+     */
+    public void resetAll() {
+        reset();
+
+        mView = null;
+        mVoicemailUri = null;
+    }
+
+    /**
+     * Reset the presenter such that it is as if the voicemail has not been played.
      */
     public void reset() {
         if (mMediaPlayer != null) {
@@ -270,13 +286,15 @@
 
         disableProximitySensor(false /* waitForFarState */);
 
-        mView = null;
-        mVoicemailUri = null;
-
         mIsPrepared = false;
         mIsPlaying = false;
         mPosition = 0;
         mDuration.set(0);
+
+        if (mView != null) {
+            mView.onPlaybackStopped();
+            mView.setClipPosition(0, mDuration.get());
+        }
     }
 
     /**
@@ -291,16 +309,11 @@
         }
 
         // Release the media player, otherwise there may be failures.
-        if (mMediaPlayer != null) {
-            mMediaPlayer.release();
-            mMediaPlayer = null;
-            mIsPrepared = false;
-        }
+        reset();
 
         if (mActivity != null) {
             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
         }
-        disableProximitySensor(false /* waitForFarState */);
     }
 
     /**
@@ -404,9 +417,11 @@
             super(handler);
             mFetchResultHandler = handler;
 
-            mContext.getContentResolver().registerContentObserver(
-                    voicemailUri, false, this);
-            mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
+            if (mContext != null) {
+                mContext.getContentResolver().registerContentObserver(
+                        voicemailUri, false, this);
+                mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
+            }
         }
 
         /**
@@ -627,7 +642,8 @@
             mMediaPlayer.pause();
         }
 
-        mPosition = mMediaPlayer.getCurrentPosition();
+        mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
+
         Log.d(TAG, "Paused playback at " + mPosition + ".");
 
         if (mView != null) {
@@ -646,7 +662,9 @@
      * playing to know whether to resume playback once the user selects a new position.
      */
     public void pausePlaybackForSeeking() {
-        mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+        if (mMediaPlayer != null) {
+            mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+        }
         pausePlayback();
     }
 
@@ -705,10 +723,21 @@
         return mAudioManager.isSpeakerphoneOn();
     }
 
+    public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
+        mOnVoicemailDeletedListener = listener;
+    }
+
     public int getMediaPlayerPosition() {
         return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
     }
 
+    /* package */ void onVoicemailDeleted() {
+        // Trampoline the event notification to the interested listener
+        if (mOnVoicemailDeletedListener != null) {
+            mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
+        }
+    }
+
     private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
         if (mScheduledExecutorService == null) {
             mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
diff --git a/src/com/android/dialer/widget/EmptyContentView.java b/src/com/android/dialer/widget/EmptyContentView.java
new file mode 100644
index 0000000..f248967
--- /dev/null
+++ b/src/com/android/dialer/widget/EmptyContentView.java
@@ -0,0 +1,117 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+
+public class EmptyContentView extends LinearLayout implements View.OnClickListener {
+
+    public static final int NO_LABEL = 0;
+    public static final int NO_IMAGE = 0;
+
+    private ImageView mImageView;
+    private TextView mDescriptionView;
+    private TextView mActionView;
+    private OnEmptyViewActionButtonClickedListener mOnActionButtonClickedListener;
+
+    public interface OnEmptyViewActionButtonClickedListener {
+        public void onEmptyViewActionButtonClicked();
+    }
+
+    public EmptyContentView(Context context) {
+        this(context, null);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        setOrientation(LinearLayout.VERTICAL);
+
+        final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.empty_content_view, this);
+        // Don't let touches fall through the empty view.
+        setClickable(true);
+        mImageView = (ImageView) findViewById(R.id.emptyListViewImage);
+        mDescriptionView = (TextView) findViewById(R.id.emptyListViewMessage);
+        mActionView = (TextView) findViewById(R.id.emptyListViewAction);
+        mActionView.setOnClickListener(this);
+    }
+
+    public void setDescription(int resourceId) {
+        if (resourceId == NO_LABEL) {
+            mDescriptionView.setText(null);
+            mDescriptionView.setVisibility(View.GONE);
+        } else {
+            mDescriptionView.setText(resourceId);
+            mDescriptionView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void setImage(int resourceId) {
+        mImageView.setImageResource(resourceId);
+        if (resourceId == NO_LABEL) {
+            mImageView.setVisibility(View.GONE);
+        } else {
+            mImageView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void setActionLabel(int resourceId) {
+        if (resourceId == NO_LABEL) {
+            mActionView.setText(null);
+            mActionView.setVisibility(View.GONE);
+        } else {
+            mActionView.setText(resourceId);
+            mActionView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public boolean isShowingContent() {
+        return mImageView.getVisibility() == View.VISIBLE
+                || mDescriptionView.getVisibility() == View.VISIBLE
+                || mActionView.getVisibility() == View.VISIBLE;
+    }
+
+    public void setActionClickedListener(OnEmptyViewActionButtonClickedListener listener) {
+        mOnActionButtonClickedListener = listener;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mOnActionButtonClickedListener != null) {
+            mOnActionButtonClickedListener.onEmptyViewActionButtonClicked();
+        }
+    }
+}