Merge branch 'dev/13/fp3/lineage-20.0' into int/13/fp3

* dev/13/fp3/lineage-20.0: (23 commits)
  proguard.flags: Don't optimize fragment constructors
  fixup! Add a permission to guard receiving intents in StatusSmsFetcher.
  Dialer: Notify content observers upon call log entry deletion
  Dialer: improve search bar
  Dialer: set statusbar color same as activity background
  Dialer: update ripple color
  Dialer: update dark theme colors
  Dialer: use black/white navigation colors
  Dialer: handle database downgrade from lineage-17.1
  Dialer: Use a DayNight launch theme
  Dialer: allow framework to do dark theming automatically
  Dialer: AudioModeProvider: use wired route for usb headsets
  Control dialer's incoming call proximity sensor check via an overlay
  Re-add dialer lookup.
  Re-add call statistics.
  Allow per-call account selection.
  Re-add call recording.
  Add setting to enable Do Not Disturb during calls
  Generalize the in-call vibration settings category
  Add back in-call vibration features
  ...

Change-Id: I6bd56338a9c7aa05a575232471124cbe57475647
diff --git a/Android.mk b/Android.mk
index c842c5e..b58b72a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -79,6 +79,8 @@
 LOCAL_SRC_FILES += $(call all-Iaidl-files-under, $(BASE_DIR))
 LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
 
+LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/java
+
 LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)
 
 LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(RES_DIRS))
@@ -176,6 +178,7 @@
 LOCAL_PRODUCT_MODULE := true
 LOCAL_USE_AAPT2 := true
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.dialer
+LOCAL_REQUIRED_MODULES += privapp_whitelist_com.android.dialer-ext.xml
 LOCAL_USES_LIBRARIES := org.apache.http.legacy
 
 LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
@@ -183,6 +186,15 @@
 LOCAL_NOTICE_FILE := $(LOCAL_PATH)/LICENSE
 include $(BUILD_PACKAGE)
 
+include $(CLEAR_VARS)
+LOCAL_MODULE := privapp_whitelist_com.android.dialer-ext.xml
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT_ETC)/permissions
+LOCAL_PRODUCT_MODULE := true
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+include $(BUILD_PREBUILT)
+
 # Cleanup local state
 BASE_DIR :=
 EXCLUDE_FILES :=
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a106e12..39d1c1e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -78,8 +78,9 @@
   <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
 
   <!-- Used for sending PendingIntents to dynamically registered receivers -->
-  <uses-permission android:name="com.android.dialer.permission.DIALER_ORIGIN"
+  <permission android:name="com.android.dialer.permission.DIALER_ORIGIN"
     android:protectionLevel="signature" />
+  <uses-permission android:name="com.android.dialer.permission.DIALER_ORIGIN"/>
 
   <!-- Permissions needed for badger count showing on launch icon. -->
 
@@ -118,9 +119,11 @@
     android:icon="@mipmap/ic_launcher_phone"
     android:label="@string/applicationLabel"
     android:name="com.android.dialer.binary.aosp.AospDialerApplication"
+    android:appCategory="social"
     android:supportsRtl="true"
     android:usesCleartextTraffic="false"
-    android:extractNativeLibs="false">
+    android:extractNativeLibs="false"
+    android:requestLegacyExternalStorage="true">
   </application>
 
 </manifest>
diff --git a/assets/quantum/res/drawable/quantum_ic_record_white_36.xml b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
new file mode 100644
index 0000000..35aaa41
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="36dp"
+    android:width="36dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,12C19,15.86 15.86,19 12,19C8.14,19 5,15.86 5,12C5,8.14 8.14,5 12,5C15.86,5 19,8.14 19,12Z" />
+</vector>
diff --git a/java/com/android/dialer/app/AccountSelectionActivity.java b/java/com/android/dialer/app/AccountSelectionActivity.java
new file mode 100644
index 0000000..de84a36
--- /dev/null
+++ b/java/com/android/dialer/app/AccountSelectionActivity.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.util.CallUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountSelectionActivity extends AppCompatActivity {
+  public static Intent createIntent(Context context, String number,
+          CallInitiationType.Type initiationType) {
+    if (TextUtils.isEmpty(number)) {
+      return null;
+    }
+
+    List<PhoneAccount> accounts =
+        CallUtil.getCallCapablePhoneAccounts(context, PhoneAccount.SCHEME_TEL);
+    if (accounts == null || accounts.size() <= 1) {
+      return null;
+    }
+    ArrayList<PhoneAccountHandle> accountHandles = new ArrayList<>();
+    for (PhoneAccount account : accounts) {
+      accountHandles.add(account.getAccountHandle());
+    }
+
+    return new Intent(context, AccountSelectionActivity.class)
+        .putExtra("number", number)
+        .putExtra("accountHandles", accountHandles)
+        .putExtra("type", initiationType.ordinal());
+  }
+
+  private String number;
+  private CallInitiationType.Type initiationType;
+
+  private SelectPhoneAccountDialogFragment.SelectPhoneAccountListener listener =
+      new SelectPhoneAccountDialogFragment.SelectPhoneAccountListener() {
+    @Override
+    public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
+        boolean setDefault, String callId) {
+      Intent intent = new CallIntentBuilder(number, initiationType)
+          .setPhoneAccountHandle(selectedAccountHandle)
+          .build();
+      startActivity(intent);
+      finish();
+    }
+
+    @Override
+    public void onDialogDismissed(String callId) {
+      finish();
+    }
+  };
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    number = getIntent().getStringExtra("number");
+    initiationType = CallInitiationType.Type.values()[getIntent().getIntExtra("type", 0)];
+
+    if (getFragmentManager().findFragmentByTag("dialog") == null) {
+      List<PhoneAccountHandle> handles = getIntent().getParcelableArrayListExtra("accountHandles");
+      SelectPhoneAccountDialogOptions options = SelectPhoneAccountDialogOptionsUtil
+          .builderWithAccounts(handles)
+          .setTitle(R.string.call_via_dialog_title)
+          .setCanSetDefault(false)
+          .build();
+      SelectPhoneAccountDialogFragment dialog =
+          SelectPhoneAccountDialogFragment.newInstance(options, listener);
+
+      dialog.show(getFragmentManager(), "dialog");
+    }
+  }
+}
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index ade5bd4..93dedb6 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -78,6 +78,10 @@
       android:theme="@style/DialtactsTheme">
     </activity>
 
+    <activity android:name="com.android.dialer.app.AccountSelectionActivity"
+      android:theme="@style/TransparentTheme"
+      android:exported="false" />
+
     <receiver android:name="com.android.dialer.app.calllog.CallLogReceiver"
         android:exported="true">
       <intent-filter>
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 25a6956..318f9a1 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -769,9 +769,10 @@
         String number = data.getStringExtra(OldCallDetailsActivity.EXTRA_PHONE_NUMBER);
         int snackbarDurationMillis = 5_000;
         Snackbar.make(parentLayout, getString(R.string.ec_data_deleted), snackbarDurationMillis)
-            .setAction(
-                R.string.view_conversation,
-                v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this)))
+            .setAction(R.string.view_conversation, v -> {
+                IntentProvider provider = IntentProvider.getSendSmsIntentProvider(number);
+                startActivity(provider.getClickIntent(this));
+            })
             .setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color))
             .show();
       }
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java
index fdfb3ab..ff3ff55 100644
--- a/java/com/android/dialer/app/calllog/CallLogActivity.java
+++ b/java/com/android/dialer/app/calllog/CallLogActivity.java
@@ -33,6 +33,8 @@
 import com.android.contacts.common.list.ViewPagerTabs;
 import com.android.dialer.app.R;
 import com.android.dialer.calldetails.OldCallDetailsActivity;
+import com.android.dialer.callstats.CallStatsFragment;
+import com.android.dialer.callstats.DoubleDatePickerDialog;
 import com.android.dialer.common.Assert;
 import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.database.CallLogQueryHandler;
@@ -45,17 +47,19 @@
 import com.android.dialer.util.ViewUtil;
 
 /** Activity for viewing call history. */
-public class CallLogActivity extends TransactionSafeActivity
-    implements ViewPager.OnPageChangeListener {
+public class CallLogActivity extends TransactionSafeActivity implements
+    ViewPager.OnPageChangeListener, DoubleDatePickerDialog.OnDateSetListener {
 
   @VisibleForTesting static final int TAB_INDEX_ALL = 0;
   @VisibleForTesting static final int TAB_INDEX_MISSED = 1;
-  private static final int TAB_INDEX_COUNT = 2;
+  private static final int TAB_INDEX_STATS = 2;
+  private static final int TAB_INDEX_COUNT = 3;
   private ViewPager viewPager;
   private ViewPagerTabs viewPagerTabs;
   private ViewPagerAdapter viewPagerAdapter;
   private CallLogFragment allCallsFragment;
   private CallLogFragment missedCallsFragment;
+  private CallStatsFragment statsFragment;
   private String[] tabTitles;
   private boolean isResumed;
   private int selectedPageIndex;
@@ -86,6 +90,7 @@
     tabTitles = new String[TAB_INDEX_COUNT];
     tabTitles[0] = getString(R.string.call_log_all_title);
     tabTitles[1] = getString(R.string.call_log_missed_title);
+    tabTitles[2] = getString(R.string.call_log_stats_title);
 
     viewPager = (ViewPager) findViewById(R.id.call_log_pager);
 
@@ -187,6 +192,15 @@
     viewPagerTabs.onPageScrollStateChanged(state);
   }
 
+  @Override
+  public void onDateSet(long from, long to) {
+    switch (viewPager.getCurrentItem()) {
+      case TAB_INDEX_STATS:
+        statsFragment.onDateSet(from, to);
+        break;
+    }
+  }
+
   private void sendScreenViewForChildFragment() {
     Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this);
   }
@@ -213,6 +227,8 @@
           missedCallsFragment.markMissedCallsAsReadAndRemoveNotifications();
         }
         break;
+      case TAB_INDEX_STATS:
+        break;
       default:
         throw Assert.createIllegalStateFailException("Invalid position: " + position);
     }
@@ -244,6 +260,8 @@
               CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */);
         case TAB_INDEX_MISSED:
           return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */);
+        case TAB_INDEX_STATS:
+          return new CallStatsFragment();
         default:
           throw new IllegalStateException("No fragment at position " + position);
       }
@@ -251,13 +269,16 @@
 
     @Override
     public Object instantiateItem(ViewGroup container, int position) {
-      final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position);
+      final Object fragment = super.instantiateItem(container, position);
       switch (getRtlPosition(position)) {
         case TAB_INDEX_ALL:
-          allCallsFragment = fragment;
+          allCallsFragment = (CallLogFragment) fragment;
           break;
         case TAB_INDEX_MISSED:
-          missedCallsFragment = fragment;
+          missedCallsFragment = (CallLogFragment) fragment;
+          break;
+        case TAB_INDEX_STATS:
+          statsFragment = (CallStatsFragment) fragment;
           break;
         default:
           throw Assert.createIllegalStateFailException("Invalid position: " + position);
@@ -284,9 +305,10 @@
           && data.getBooleanExtra(OldCallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) {
         String number = data.getStringExtra(OldCallDetailsActivity.EXTRA_PHONE_NUMBER);
         Snackbar.make(findViewById(R.id.calllog_frame), getString(R.string.ec_data_deleted), 5_000)
-            .setAction(
-                R.string.view_conversation,
-                v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this)))
+            .setAction(R.string.view_conversation, v -> {
+                IntentProvider provider = IntentProvider.getSendSmsIntentProvider(number);
+                startActivity(provider.getClickIntent(this));
+            })
             .setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color))
             .show();
       }
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 05011d1..af4fb29 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -106,6 +106,7 @@
  */
 public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
     implements View.OnClickListener,
+        View.OnLongClickListener,
         MenuItem.OnMenuItemClickListener,
         View.OnCreateContextMenuListener {
 
@@ -306,6 +307,7 @@
     quickContactView.setOverlay(null);
     quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
     primaryActionButtonView.setOnClickListener(this);
+    primaryActionButtonView.setOnLongClickListener(this);
     primaryActionView.setOnClickListener(this.expandCollapseListener);
     if (this.voicemailPlaybackPresenter != null
         && ConfigProviderComponent.get(this.context)
@@ -519,6 +521,7 @@
         primaryActionButtonView.setContentDescription(
             TextUtils.expandTemplate(
                 context.getString(R.string.description_voicemail_action), validNameOrNumber));
+        primaryActionButtonView.setTag(null);
         primaryActionButtonView.setVisibility(View.VISIBLE);
       } else {
         primaryActionButtonView.setVisibility(View.GONE);
@@ -1053,7 +1056,8 @@
       return;
     }
     intentProvider.logInteraction(context);
-    final Intent intent = intentProvider.getIntent(context);
+
+    final Intent intent = intentProvider.getClickIntent(context);
     // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
     if (intent == null) {
       return;
@@ -1073,6 +1077,18 @@
     }
   }
 
+  @Override
+  public boolean onLongClick(View view) {
+    final IntentProvider intentProvider = (IntentProvider) view.getTag();
+    final Intent intent = intentProvider != null
+        ? intentProvider.getLongClickIntent(context) : null;
+    if (intent != null) {
+      DialerUtils.startActivityWithErrorToast(context, intent);
+      return true;
+    }
+    return false;
+  }
+
   private static boolean isNonContactEntry(ContactInfo info) {
     if (info == null || info.sourceType != Type.SOURCE_TYPE_DIRECTORY) {
       return true;
@@ -1379,6 +1395,9 @@
                 Calls.CONTENT_URI,
                 CallLog.Calls._ID + " IN (" + callIdsStr + ")" /* where */,
                 null /* selectionArgs */);
+        context
+            .getContentResolver()
+            .notifyChange(Calls.CONTENT_URI, null);
       }
 
       return null;
diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
index 197d2da..bea06d3 100644
--- a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
+++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
@@ -101,6 +101,7 @@
     @Override
     public Void doInBackground(@Nullable Void unused) throws Throwable {
       appContext.getContentResolver().delete(Calls.CONTENT_URI, null, null);
+      appContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null);
       CachedNumberLookupService cachedNumberLookupService =
           PhoneNumberCache.get(appContext).getCachedNumberLookupService();
       if (cachedNumberLookupService != null) {
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 423b49c..4c5a98b 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -26,6 +26,7 @@
 import android.telephony.TelephonyManager;
 import com.android.contacts.common.model.Contact;
 import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.app.AccountSelectionActivity;
 import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.calldetails.OldCallDetailsActivity;
 import com.android.dialer.callintent.CallInitiationType;
@@ -55,12 +56,18 @@
       final String number, final PhoneAccountHandle accountHandle) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return PreCall.getIntent(
             context,
             new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
                 .setPhoneAccountHandle(accountHandle));
       }
+
+      @Override
+      public Intent getLongClickIntent(Context context) {
+        return AccountSelectionActivity.createIntent(context, number,
+            CallInitiationType.Type.CALL_LOG);
+      }
     };
   }
 
@@ -68,12 +75,13 @@
       final String number, final Context context, final TelephonyManager telephonyManager) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return PreCall.getIntent(
             context,
             new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
                 .setAllowAssistedDial(true));
       }
+
     };
   }
 
@@ -85,7 +93,7 @@
       final String number, final PhoneAccountHandle accountHandle) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return PreCall.getIntent(
             context,
             new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
@@ -98,7 +106,7 @@
   public static IntentProvider getDuoVideoIntentProvider(String number, boolean isNonContact) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return PreCall.getIntent(
             context,
             new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
@@ -122,7 +130,7 @@
   public static IntentProvider getInstallDuoIntentProvider() {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return DuoComponent.get(context).getDuo().getInstallDuoIntent().orNull();
       }
 
@@ -136,7 +144,7 @@
   public static IntentProvider getSetUpDuoIntentProvider() {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return DuoComponent.get(context).getDuo().getActivateIntent().orNull();
       }
 
@@ -150,7 +158,7 @@
   public static IntentProvider getDuoInviteIntentProvider(String number) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return DuoComponent.get(context).getDuo().getInviteIntent(number).orNull();
       }
 
@@ -164,7 +172,7 @@
   public static IntentProvider getReturnVoicemailCallIntentProvider() {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return PreCall.getIntent(
             context,
             CallIntentBuilder.forVoicemail(CallInitiationType.Type.CALL_LOG));
@@ -175,7 +183,7 @@
   public static IntentProvider getSendSmsIntentProvider(final String number) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return IntentUtil.getSendSmsIntent(number);
       }
     };
@@ -197,7 +205,7 @@
       boolean canSupportAssistedDialing) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         return OldCallDetailsActivity.newInstance(
             context, callDetailsEntries, contact, canReportCallerId, canSupportAssistedDialing);
       }
@@ -213,7 +221,7 @@
       final boolean isNewContact) {
     return new IntentProvider() {
       @Override
-      public Intent getIntent(Context context) {
+      public Intent getClickIntent(Context context) {
         Contact contactToSave = null;
 
         if (lookupUri != null) {
@@ -274,7 +282,9 @@
     };
   }
 
-  public abstract Intent getIntent(Context context);
-
+  public abstract Intent getClickIntent(Context context);
+  public Intent getLongClickIntent(Context context) {
+    return null;
+  }
   public void logInteraction(Context context) {}
 }
diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
index aed51b5..267dc62 100644
--- a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
+++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
@@ -34,7 +34,8 @@
   private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment";
   private static final int CONTACT_INFO_CACHE_SIZE = 100;
 
-  private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache;
+  private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache =
+      ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
 
   @NonNull
   public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) {
@@ -57,7 +58,6 @@
   @Override
   public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
-    retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
     setRetainInstance(true);
   }
 
diff --git a/java/com/android/dialer/app/res/values-night/colors.xml b/java/com/android/dialer/app/res/values-night/colors.xml
new file mode 100644
index 0000000..1d165af
--- /dev/null
+++ b/java/com/android/dialer/app/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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
+-->
+
+<resources>
+  <color name="nav_item">#80FFFFFF</color>
+  <color name="nav_item_selected">#FFFFFF</color>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_arrays.xml b/java/com/android/dialer/app/res/values/cm_arrays.xml
new file mode 100644
index 0000000..a788fd3
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_arrays.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string-array name="call_recording_encoder_entries" translatable="false">
+        <item>@string/wb_amr_format</item>
+        <item>@string/aac_format</item>
+    </string-array>
+
+    <string-array name="call_recording_encoder_values" translatable="false">
+        <item>"0"</item>
+        <item>"1"</item>
+    </string-array>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_attrs.xml b/java/com/android/dialer/app/res/values/cm_attrs.xml
new file mode 100644
index 0000000..3155845
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_attrs.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources>
+    <declare-styleable name="LinearColorBar">
+        <attr name="redColor" format="color" />
+        <attr name="greenColor" format="color" />
+        <attr name="blueColor" format="color" />
+        <attr name="orangeColor" format="color" />
+        <attr name="backgroundColor" format="color" />
+    </declare-styleable>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_strings.xml b/java/com/android/dialer/app/res/values/cm_strings.xml
new file mode 100644
index 0000000..1dcdb2b
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="incall_category_key" translatable="false">dialer_general_incall_category_key</string>
+    <string name="incall_category_title">In-call</string>
+    <string name="incall_enable_dnd_title">Enable Do Not Disturb during calls</string>
+    <string name="incall_vibrate_outgoing_key" translatable="false">incall_vibrate_outgoing</string>
+    <string name="incall_vibrate_outgoing_title">Vibrate on answer</string>
+    <string name="incall_vibrate_call_waiting_key" translatable="false">incall_vibrate_call_waiting</string>
+    <string name="incall_vibrate_call_waiting_title">Vibrate on call waiting</string>
+    <string name="incall_vibrate_hangup_key" translatable="false">incall_vibrate_hangup</string>
+    <string name="incall_vibrate_hangup_title">Vibrate on hang up</string>
+    <string name="incall_vibrate_45_key" translatable="false">incall_vibrate_45secs</string>
+    <string name="incall_vibrate_45_title">Vibrate every minute</string>
+    <string name="incall_vibrate_45_summary">Vibrates at the 45 second mark of every minute during outgoing calls</string>
+    <string name="incall_dnd_dialog_message">In order to enable Do Not Disturb, the Phone app needs to be granted the permission to control the Do Not Disturb status.\nPlease allow it.</string>
+    <string name="allow">Allow</string>
+    <string name="deny">Deny</string>
+
+    <string name="call_recording_category_key" translatable="false">call_recording_category</string>
+    <string name="call_recording_category_title">Call recording</string>
+    <string name="call_recording_format_key" translatable="false">call_recording_format</string>
+    <string name="call_recording_format">Audio format</string>
+    <string name="wb_amr_format" translatable="false">AMR-WB</string>
+    <string name="aac_format" translatable="false">AAC</string>
+
+    <string name="call_via">Call via</string>
+    <string name="call_via_dialog_title">Call via\u2026</string>
+
+    <string name="call_log_stats_title">Statistics</string>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
index a971347..f59f4f1 100644
--- a/java/com/android/dialer/app/res/values/colors.xml
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -18,9 +18,14 @@
   <color name="voicemail_icon_disabled_tint">#80000000</color>
   <color name="voicemail_playpause_icon_tint">?colorIcon</color>
 
+  <color name="call_record_playback_icon_color">#8a000000</color>
+
   <!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
   <color name="remove_highlighted_text_color">#FF3F3B</color>
 
   <!--  Colors for blocked numbers list -->
   <color name="blocked_number_block_color">#F44336</color>
+
+  <color name="nav_item">#80666666</color>
+  <color name="nav_item_selected">#666666</color>
 </resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
index c2cad31..0fa3503 100644
--- a/java/com/android/dialer/app/res/values/styles.xml
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -23,6 +23,16 @@
     <item name="android:textColorPrimary">?android:attr/textColorPrimaryInverse</item>
   </style>
 
+  <style name="TransparentTheme" parent="DialtactsTheme">
+    <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+    <item name="android:windowBackground">@android:color/transparent</item>
+    <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
+    <item name="android:windowContentOverlay">@null</item>
+    <item name="android:windowIsTranslucent">true</item>
+    <item name="android:windowNoTitle">true</item>
+    <item name="android:windowIsFloating">true</item>
+  </style>
+
   <style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode">
     <item name="android:background">?android:attr/colorPrimary</item>
     <item name="background">?android:attr/colorPrimary</item>
diff --git a/java/com/android/dialer/app/res/xml/file_paths.xml b/java/com/android/dialer/app/res/xml/file_paths.xml
index 0dd41a0..b43f450 100644
--- a/java/com/android/dialer/app/res/xml/file_paths.xml
+++ b/java/com/android/dialer/app/res/xml/file_paths.xml
@@ -22,4 +22,8 @@
   <files-path
     name="voicemails"
     path="voicemails/"/>
+  <!-- Offer access to saved call recordings -->
+  <external-path
+    name="recordings"
+    path="CallRecordings/"/>
 </paths>
diff --git a/java/com/android/dialer/app/res/xml/sound_settings.xml b/java/com/android/dialer/app/res/xml/sound_settings.xml
index d9afb74..aa02587 100644
--- a/java/com/android/dialer/app/res/xml/sound_settings.xml
+++ b/java/com/android/dialer/app/res/xml/sound_settings.xml
@@ -43,4 +43,46 @@
     android:key="@string/dtmf_tone_length_preference_key"
     android:title="@string/dtmf_tone_length_title"/>
 
+  <PreferenceCategory
+    android:key="@string/incall_category_key"
+    android:title="@string/incall_category_title">
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="incall_enable_dnd"
+        android:title="@string/incall_enable_dnd_title"/>
+
+    <SwitchPreference
+      android:key="@string/incall_vibrate_outgoing_key"
+      android:title="@string/incall_vibrate_outgoing_title" />
+
+    <SwitchPreference
+      android:key="@string/incall_vibrate_call_waiting_key"
+      android:title="@string/incall_vibrate_call_waiting_title" />
+
+    <SwitchPreference
+      android:key="@string/incall_vibrate_hangup_key"
+      android:title="@string/incall_vibrate_hangup_title" />
+
+    <SwitchPreference
+      android:key="@string/incall_vibrate_45_key"
+      android:title="@string/incall_vibrate_45_title"
+      android:summary="@string/incall_vibrate_45_summary" />
+
+  </PreferenceCategory>
+
+  <PreferenceCategory
+    android:key="@string/call_recording_category_key"
+    android:title="@string/call_recording_category_title">
+
+    <ListPreference
+      android:key="@string/call_recording_format_key"
+      android:title="@string/call_recording_format"
+      android:summary="%s"
+      android:entries="@array/call_recording_encoder_entries"
+      android:entryValues="@array/call_recording_encoder_values"
+      android:defaultValue="0" />
+
+  </PreferenceCategory>
+
 </PreferenceScreen>
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index d9bcd6b..6ffa62a 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -39,6 +39,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
 import com.android.dialer.configprovider.ConfigProviderComponent;
+import com.android.dialer.lookup.LookupSettingsFragment;
 import com.android.dialer.proguard.UsedByReflection;
 import com.android.dialer.util.PermissionsUtil;
 import com.android.dialer.voicemail.settings.VoicemailSettingsFragment;
@@ -102,6 +103,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);
 
@@ -112,6 +114,11 @@
     quickResponseSettingsHeader.intent = quickResponseSettingsIntent;
     target.add(quickResponseSettingsHeader);
 
+    final Header lookupSettingsHeader = new Header();
+    lookupSettingsHeader.titleRes = R.string.lookup_settings_label;
+    lookupSettingsHeader.fragment = LookupSettingsFragment.class.getName();
+    target.add(lookupSettingsHeader);
+
     TelephonyManager telephonyManager =
         (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
 
@@ -271,32 +278,22 @@
         && getResources().getBoolean(R.bool.config_sort_order_user_changeable);
   }
 
-  /**
-   * For the "sounds and vibration" setting, we go directly to the system sound settings fragment.
-   * This helps since:
-   * <li>We don't need a separate Dialer sounds and vibrations fragment, as everything we need is
-   *     present in the system sounds fragment.
-   * <li>OEM's e.g Moto that support dual sim ring-tones no longer need to update the dialer sound
-   *     and settings fragment.
-   *
-   *     <p>For all other settings, we launch our our preferences fragment.
-   */
   @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;
       }
-
-      startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
-      return;
     }
-
     super.onHeaderClick(header, position);
   }
 
diff --git a/java/com/android/dialer/app/settings/SoundSettingsFragment.java b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
new file mode 100644
index 0000000..f7fc0d0
--- /dev/null
+++ b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.app.settings;
+
+import android.app.AlertDialog;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+import com.android.dialer.util.SettingsUtil;
+
+public class SoundSettingsFragment extends PreferenceFragment
+    implements Preference.OnPreferenceChangeListener {
+
+  private static final int NO_DTMF_TONE = 0;
+  private static final int PLAY_DTMF_TONE = 1;
+
+  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 MSG_UPDATE_RINGTONE_SUMMARY = 1;
+
+  private Preference ringtonePreference;
+  private final Handler ringtoneLookupComplete =
+      new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+          switch (msg.what) {
+            case MSG_UPDATE_RINGTONE_SUMMARY:
+              ringtonePreference.setSummary((CharSequence) msg.obj);
+              break;
+          }
+        }
+      };
+  private final Runnable ringtoneLookupRunnable =
+      new Runnable() {
+        @Override
+        public void run() {
+          updateRingtonePreferenceSummary();
+        }
+      };
+  private SwitchPreference vibrateWhenRinging;
+  private SwitchPreference playDtmfTone;
+  private ListPreference dtmfToneLength;
+  private SwitchPreference enableDndInCall;
+
+  private NotificationManager notificationManager;
+
+  @Override
+  public Context getContext() {
+    return getActivity();
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    addPreferencesFromResource(R.xml.sound_settings);
+
+    Context context = getActivity();
+
+    ringtonePreference = findPreference(context.getString(R.string.ringtone_preference_key));
+    vibrateWhenRinging =
+        (SwitchPreference) findPreference(context.getString(R.string.vibrate_on_preference_key));
+    playDtmfTone =
+        (SwitchPreference) findPreference(context.getString(R.string.play_dtmf_preference_key));
+    dtmfToneLength =
+        (ListPreference)
+            findPreference(context.getString(R.string.dtmf_tone_length_preference_key));
+    enableDndInCall = (SwitchPreference) findPreference("incall_enable_dnd");
+
+    if (hasVibrator()) {
+      vibrateWhenRinging.setOnPreferenceChangeListener(this);
+    } else {
+      PreferenceScreen ps = getPreferenceScreen();
+      Preference inCallVibrateOutgoing = findPreference(
+          context.getString(R.string.incall_vibrate_outgoing_key));
+      Preference inCallVibrateCallWaiting = findPreference(
+          context.getString(R.string.incall_vibrate_call_waiting_key));
+      Preference inCallVibrateHangup = findPreference(
+          context.getString(R.string.incall_vibrate_hangup_key));
+      Preference inCallVibrate45Secs = findPreference(
+          context.getString(R.string.incall_vibrate_45_key));
+      ps.removePreference(vibrateWhenRinging);
+      ps.removePreference(inCallVibrateOutgoing);
+      ps.removePreference(inCallVibrateCallWaiting);
+      ps.removePreference(inCallVibrateHangup);
+      ps.removePreference(inCallVibrate45Secs);
+      vibrateWhenRinging = null;
+    }
+
+    playDtmfTone.setOnPreferenceChangeListener(this);
+    playDtmfTone.setChecked(shouldPlayDtmfTone());
+
+    enableDndInCall.setOnPreferenceChangeListener(this);
+
+    TelephonyManager telephonyManager =
+        (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+    if (telephonyManager.canChangeDtmfToneLength()
+        && (telephonyManager.isWorldPhone() || !shouldHideCarrierSettings())) {
+      dtmfToneLength.setOnPreferenceChangeListener(this);
+      dtmfToneLength.setValueIndex(
+          Settings.System.getInt(
+              context.getContentResolver(),
+              Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+              DTMF_TONE_TYPE_NORMAL));
+    } else {
+      getPreferenceScreen().removePreference(dtmfToneLength);
+      dtmfToneLength = null;
+    }
+    if (!CallRecorderService.isEnabled(getActivity())) {
+      getPreferenceScreen().removePreference(
+          findPreference(context.getString(R.string.call_recording_category_key)));
+    }
+    notificationManager = context.getSystemService(NotificationManager.class);
+  }
+
+  @Override
+  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 (vibrateWhenRinging != null) {
+      vibrateWhenRinging.setChecked(shouldVibrateWhenRinging());
+    }
+
+    // Lookup the ringtone name asynchronously.
+    new Thread(ringtoneLookupRunnable).start();
+  }
+
+  /**
+   * Supports onPreferenceChangeListener to look for preference changes.
+   *
+   * @param preference The preference to be changed
+   * @param objValue The value of the selection, NOT its localized display value.
+   */
+  @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 == vibrateWhenRinging) {
+      boolean doVibrate = (Boolean) objValue;
+      Settings.System.putInt(
+          getActivity().getContentResolver(),
+          Settings.System.VIBRATE_WHEN_RINGING,
+          doVibrate ? DO_VIBRATION_FOR_CALLS : NO_VIBRATION_FOR_CALLS);
+    } else if (preference == dtmfToneLength) {
+      int index = dtmfToneLength.findIndexOfValue((String) objValue);
+      Settings.System.putInt(
+          getActivity().getContentResolver(), Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index);
+    } else if (preference == enableDndInCall) {
+      boolean newValue = (Boolean) objValue;
+      if (newValue && !notificationManager.isNotificationPolicyAccessGranted()) {
+        new AlertDialog.Builder(getContext())
+            .setMessage(R.string.incall_dnd_dialog_message)
+            .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() {
+              @Override
+              public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+                Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
+                startActivity(intent);
+              }
+            })
+            .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() {
+              @Override
+              public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+              }
+            })
+            .show();
+
+        // At this time, it is unknown whether the user granted the permission
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /** Click listener for toggle events. */
+  @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 == playDtmfTone) {
+      Settings.System.putInt(
+          getActivity().getContentResolver(),
+          Settings.System.DTMF_TONE_WHEN_DIALING,
+          playDtmfTone.isChecked() ? PLAY_DTMF_TONE : NO_DTMF_TONE);
+    }
+    return true;
+  }
+
+  /** Updates the summary text on the ringtone preference with the name of the ringtone. */
+  private void updateRingtonePreferenceSummary() {
+    SettingsUtil.updateRingtoneName(
+        getActivity(),
+        ringtoneLookupComplete,
+        RingtoneManager.TYPE_RINGTONE,
+        ringtonePreference.getKey(),
+        MSG_UPDATE_RINGTONE_SUMMARY);
+  }
+
+  /**
+   * Obtain the value for "vibrate when ringing" setting. The default value is false.
+   *
+   * <p>Watch out: if the setting is missing in the device, this will try obtaining the old "vibrate
+   * on ring" setting from AudioManager, and save the previous setting to the new one.
+   */
+  private boolean shouldVibrateWhenRinging() {
+    int vibrateWhenRingingSetting =
+        Settings.System.getInt(
+            getActivity().getContentResolver(),
+            Settings.System.VIBRATE_WHEN_RINGING,
+            NO_VIBRATION_FOR_CALLS);
+    return hasVibrator() && (vibrateWhenRingingSetting == DO_VIBRATION_FOR_CALLS);
+  }
+
+  /** Obtains the value for dialpad/DTMF tones. The default value is true. */
+  private boolean shouldPlayDtmfTone() {
+    int dtmfToneSetting =
+        Settings.System.getInt(
+            getActivity().getContentResolver(),
+            Settings.System.DTMF_TONE_WHEN_DIALING,
+            PLAY_DTMF_TONE);
+    return dtmfToneSetting == PLAY_DTMF_TONE;
+  }
+
+  /** Whether the device hardware has a vibrator. */
+  private boolean hasVibrator() {
+    Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
+    return vibrator != null && vibrator.hasVibrator();
+  }
+
+  private boolean shouldHideCarrierSettings() {
+    CarrierConfigManager configManager =
+        (CarrierConfigManager) getActivity().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+    return configManager
+        .getConfig()
+        .getBoolean(CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL);
+  }
+}
diff --git a/java/com/android/dialer/binary/aosp/AospDialerApplication.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
index 4ca94e2..8d94bb9 100644
--- a/java/com/android/dialer/binary/aosp/AospDialerApplication.java
+++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
@@ -16,15 +16,34 @@
 
 package com.android.dialer.binary.aosp;
 
+import android.content.Context;
+import android.net.Uri;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.contacts.common.extensions.PhoneDirectoryExtender;
+import com.android.contacts.common.extensions.PhoneDirectoryExtenderFactory;
 import com.android.dialer.binary.common.DialerApplication;
 import com.android.dialer.inject.ContextModule;
+import com.android.dialer.lookup.LookupCacheService;
+import com.android.dialer.lookup.LookupProvider;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.ReverseLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.PhoneNumberCacheBindings;
+import com.android.dialer.phonenumbercache.PhoneNumberCacheBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindings;
+import com.android.incallui.bindings.InCallUiBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindingsStub;
+import com.android.incallui.bindings.PhoneNumberService;
+
+import java.util.List;
 
 /**
  * The application class for the AOSP Dialer. This is a version of the Dialer app that has no
  * dependency on Google Play Services.
  */
-public class AospDialerApplication extends DialerApplication {
+public class AospDialerApplication extends DialerApplication implements
+    PhoneNumberCacheBindingsFactory, PhoneDirectoryExtenderFactory, InCallUiBindingsFactory {
 
   /** Returns a new instance of the root component for the AOSP Dialer. */
   @Override
@@ -32,4 +51,43 @@
   protected Object buildRootComponent() {
     return DaggerAospDialerRootComponent.builder().contextModule(new ContextModule(this)).build();
   }
+
+  @Override
+  public PhoneDirectoryExtender newPhoneDirectoryExtender() {
+    return new PhoneDirectoryExtender() {
+      @Override
+      public boolean isEnabled(Context context) {
+        return LookupSettings.isForwardLookupEnabled(AospDialerApplication.this)
+            || LookupSettings.isPeopleLookupEnabled(AospDialerApplication.this);
+      }
+
+      @Override
+      @Nullable
+      public Uri getContentUri() {
+        return LookupProvider.NEARBY_AND_PEOPLE_LOOKUP_URI;
+      }
+    };
+  }
+
+  @Override
+  public InCallUiBindings newInCallUiBindings() {
+    return new InCallUiBindingsStub() {
+      @Override
+      @Nullable
+      public PhoneNumberService newPhoneNumberService(Context context) {
+        return new ReverseLookupService(context);
+      }
+    };
+  }
+
+  @Override
+  public PhoneNumberCacheBindings newPhoneNumberCacheBindings() {
+    return new PhoneNumberCacheBindings() {
+      @Override
+      @Nullable
+      public CachedNumberLookupService getCachedNumberLookupService() {
+        return new LookupCacheService();
+      }
+    };
+  }
 }
diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java
index 31d4d82..0f15025 100644
--- a/java/com/android/dialer/binary/common/DialerApplication.java
+++ b/java/com/android/dialer/binary/common/DialerApplication.java
@@ -26,6 +26,7 @@
 import com.android.dialer.calllog.CallLogFramework;
 import com.android.dialer.calllog.config.CallLogConfig;
 import com.android.dialer.calllog.config.CallLogConfigComponent;
+import com.android.dialer.callrecord.CallRecordingAutoMigrator;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
 import com.android.dialer.inject.HasRootComponent;
@@ -48,6 +49,10 @@
             new FilteredNumberAsyncQueryHandler(this),
             DialerExecutorComponent.get(this).dialerExecutorFactory())
         .asyncAutoMigrate();
+    new CallRecordingAutoMigrator(
+            this.getApplicationContext(),
+            DialerExecutorComponent.get(this).dialerExecutorFactory())
+        .asyncAutoMigrate();
     initializeAnnotatedCallLog();
     PersistentLogger.initialize(this);
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index 36b8308..c1acbc3 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -28,6 +28,7 @@
 import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
 import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.common.Assert;
 import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.protos.ProtoParsers;
@@ -93,7 +94,8 @@
       CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderListener callDetailsHeaderListener,
       ReportCallIdListener reportCallIdListener,
-      DeleteCallDetailsListener deleteCallDetailsListener) {
+      DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore) {
     return new CallDetailsAdapter(
         this,
         headerInfo,
@@ -101,7 +103,8 @@
         callDetailsEntryListener,
         callDetailsHeaderListener,
         reportCallIdListener,
-        deleteCallDetailsListener);
+        deleteCallDetailsListener,
+        callRecordingDataStore);
   }
 
   @Override
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
index a26f322..808d08c 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
@@ -38,6 +38,7 @@
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutor.FailureListener;
@@ -96,6 +97,7 @@
   private CallDetailsAdapterCommon adapter;
   private CallDetailsEntries callDetailsEntries;
   private UiListener<ImmutableSet<String>> checkRttTranscriptAvailabilityListener;
+  private CallRecordingDataStore callRecordingDataStore;
 
   /**
    * Handles the intent that launches {@link OldCallDetailsActivity} or {@link CallDetailsActivity},
@@ -108,7 +110,8 @@
       CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener,
       CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
-      CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener);
+      CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore);
 
   /** Returns the phone number of the call details. */
   protected abstract String getNumber();
@@ -129,12 +132,20 @@
     checkRttTranscriptAvailabilityListener =
         DialerExecutorComponent.get(this)
             .createUiListener(getFragmentManager(), "Query RTT transcript availability");
+    callRecordingDataStore = new CallRecordingDataStore();
     handleIntent(getIntent());
     setupRecyclerViewForEntries();
   }
 
   @Override
   @CallSuper
+  protected void onDestroy() {
+    super.onDestroy();
+    callRecordingDataStore.close();
+  }
+
+  @Override
+  @CallSuper
   protected void onResume() {
     super.onResume();
 
@@ -205,7 +216,8 @@
             callDetailsEntryListener,
             callDetailsHeaderListener,
             reportCallIdListener,
-            deleteCallDetailsListener);
+            deleteCallDetailsListener,
+            callRecordingDataStore);
 
     RecyclerView recyclerView = findViewById(R.id.recycler_view);
     recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -265,6 +277,9 @@
       context
           .getContentResolver()
           .delete(Calls.CONTENT_URI, selection.getSelection(), selection.getSelectionArgs());
+      context
+          .getContentResolver()
+          .notifyChange(Calls.CONTENT_URI, null);
       return null;
     }
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
index 40d856f..7e5ebe1 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -23,6 +23,7 @@
 import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
 import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
 import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.glidephotomanager.PhotoInfo;
 
 /**
@@ -43,14 +44,16 @@
       CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderListener callDetailsHeaderListener,
       CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
-      DeleteCallDetailsListener deleteCallDetailsListener) {
+      DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore) {
     super(
         context,
         callDetailsEntries,
         callDetailsEntryListener,
         callDetailsHeaderListener,
         reportCallIdListener,
-        deleteCallDetailsListener);
+        deleteCallDetailsListener,
+        callRecordingDataStore);
     this.headerInfo = calldetailsHeaderInfo;
   }
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
index ec9263f..d33fea8 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
@@ -32,6 +32,7 @@
 import com.android.dialer.calllogutils.CallTypeHelper;
 import com.android.dialer.calllogutils.CallbackActionHelper;
 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.common.Assert;
 import com.android.dialer.duo.DuoComponent;
 import com.android.dialer.glidephotomanager.PhotoInfo;
@@ -51,6 +52,7 @@
   private final ReportCallIdListener reportCallIdListener;
   private final DeleteCallDetailsListener deleteCallDetailsListener;
   private final CallTypeHelper callTypeHelper;
+  private final CallRecordingDataStore callRecordingDataStore;
 
   private CallDetailsEntries callDetailsEntries;
 
@@ -75,12 +77,14 @@
       CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderListener callDetailsHeaderListener,
       ReportCallIdListener reportCallIdListener,
-      DeleteCallDetailsListener deleteCallDetailsListener) {
+      DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore) {
     this.callDetailsEntries = callDetailsEntries;
     this.callDetailsEntryListener = callDetailsEntryListener;
     this.callDetailsHeaderListener = callDetailsHeaderListener;
     this.reportCallIdListener = reportCallIdListener;
     this.deleteCallDetailsListener = deleteCallDetailsListener;
+    this.callRecordingDataStore = callRecordingDataStore;
     this.callTypeHelper =
         new CallTypeHelper(context.getResources(), DuoComponent.get(context).getDuo());
   }
@@ -123,6 +127,7 @@
           getPhotoInfo(),
           entry,
           callTypeHelper,
+          callRecordingDataStore,
           !entry.getHistoryResultsList().isEmpty() && position != getItemCount() - 2);
     }
   }
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
index 05957ae..a9be544 100644
--- a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -16,23 +16,36 @@
 
 package com.android.dialer.calldetails;
 
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
 import android.content.Context;
+import android.content.Intent;
 import android.net.Uri;
 import android.provider.CallLog.Calls;
+import android.provider.MediaStore;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
 import android.support.v4.os.BuildCompat;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.view.Menu;
 import android.view.View;
+import android.webkit.MimeTypeMap;
 import android.widget.ImageView;
+import android.widget.PopupMenu;
 import android.widget.TextView;
+import android.widget.Toast;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
 import com.android.dialer.calllogutils.CallLogDates;
 import com.android.dialer.calllogutils.CallLogDurations;
 import com.android.dialer.calllogutils.CallTypeHelper;
 import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.CallRecordingDataStore;
+import com.android.dialer.callrecord.impl.CallRecorderService;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type;
@@ -41,6 +54,11 @@
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
 
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
 /** ViewHolder for call entries in {@link OldCallDetailsActivity} or {@link CallDetailsActivity}. */
 public class CallDetailsEntryViewHolder extends ViewHolder {
 
@@ -66,6 +84,7 @@
   private final TextView rttTranscript;
 
   private final ImageView multimediaImage;
+  private final TextView playbackButton;
 
   // TODO(maxwelb): Display this when location is stored - a bug
   @SuppressWarnings("unused")
@@ -83,6 +102,7 @@
     callTime = (TextView) container.findViewById(R.id.call_time);
     callDuration = (TextView) container.findViewById(R.id.call_duration);
 
+    playbackButton = (TextView) container.findViewById(R.id.play_recordings);
     multimediaImageContainer = container.findViewById(R.id.multimedia_image_container);
     multimediaDetailsContainer = container.findViewById(R.id.ec_container);
     multimediaDivider = container.findViewById(R.id.divider);
@@ -101,6 +121,7 @@
       PhotoInfo photoInfo,
       CallDetailsEntry entry,
       CallTypeHelper callTypeHelper,
+      CallRecordingDataStore callRecordingDataStore,
       boolean showMultimediaDivider) {
     int callType = entry.getCallType();
     boolean isVideoCall = (entry.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO;
@@ -139,6 +160,22 @@
           CallLogDurations.formatDurationAndDataUsageA11y(
               context, entry.getDuration(), entry.getDataUsage()));
     }
+
+    // do this synchronously to prevent recordings from "popping in" after detail item is displayed
+    final List<CallRecording> recordings;
+    if (CallRecorderService.isEnabled(context)) {
+      callRecordingDataStore.open(context); // opens unless already open
+      recordings = callRecordingDataStore.getRecordings(number, entry.getDate());
+    } else {
+      recordings = null;
+    }
+
+    int count = recordings != null ? recordings.size() : 0;
+    playbackButton.setOnClickListener(v -> handleRecordingClick(v, recordings));
+    playbackButton.setText(
+        context.getResources().getQuantityString(R.plurals.play_recordings, count, count));
+    playbackButton.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+
     setMultimediaDetails(number, entry, showMultimediaDivider);
     if (isRttCall) {
       if (entry.getHasRttTranscript()) {
@@ -209,6 +246,46 @@
     DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number));
   }
 
+  private void handleRecordingClick(View v, List<CallRecording> recordings) {
+    final Context context = v.getContext();
+    if (recordings.size() == 1) {
+      playRecording(context, recordings.get(0));
+    } else {
+      PopupMenu menu = new PopupMenu(context, v);
+      String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+          DateFormat.is24HourFormat(context) ? "Hmss" : "hmssa");
+      SimpleDateFormat format = new SimpleDateFormat(pattern);
+
+      for (int i = 0; i < recordings.size(); i++) {
+        final long startTime = recordings.get(i).startRecordingTime;
+        final String formattedDate = format.format(new Date(startTime));
+        menu.getMenu().add(Menu.NONE, i, i, formattedDate);
+      }
+      menu.setOnMenuItemClickListener(item -> {
+        playRecording(context, recordings.get(item.getItemId()));
+        return true;
+      });
+      menu.show();
+    }
+ }
+
+  private void playRecording(Context context, CallRecording recording) {
+    Uri uri = ContentUris.withAppendedId(
+        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, recording.mediaId);
+    String extension = MimeTypeMap.getFileExtensionFromUrl(recording.fileName);
+    String mime = !TextUtils.isEmpty(extension)
+        ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+    try {
+      Intent intent = new Intent(Intent.ACTION_VIEW)
+          .setDataAndType(uri, mime)
+          .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+      context.startActivity(intent);
+    } catch (ActivityNotFoundException e) {
+      Toast.makeText(context, R.string.call_playback_no_app_found_toast, Toast.LENGTH_LONG)
+          .show();
+    }
+  }
+
   private static boolean isIncoming(@NonNull HistoryResult historyResult) {
     return historyResult.getType() == Type.INCOMING_POST_CALL
         || historyResult.getType() == Type.INCOMING_CALL_COMPOSER;
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index f678061..13b03d0 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -17,18 +17,22 @@
 package com.android.dialer.calldetails;
 
 import android.content.Context;
+import android.content.Intent;
 import android.net.Uri;
 import android.support.v7.widget.RecyclerView;
 import android.telecom.PhoneAccount;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
+import com.android.dialer.app.AccountSelectionActivity;
 import com.android.dialer.calldetails.CallDetailsActivityCommon.AssistedDialingNumberParseWorker;
 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
@@ -41,6 +45,7 @@
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
 import com.android.dialer.widget.BidiTextView;
 
 /**
@@ -49,7 +54,7 @@
  * <p>The header contains contact info and the primary callback button.
  */
 public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder
-    implements OnClickListener, FailureListener {
+    implements OnClickListener, OnLongClickListener, FailureListener {
 
   private final CallDetailsHeaderListener callDetailsHeaderListener;
   private final ImageView callbackButton;
@@ -86,6 +91,7 @@
         callDetailsHeaderListener::openAssistedDialingSettings);
 
     callbackButton.setOnClickListener(this);
+    callbackButton.setOnLongClickListener(this);
 
     this.number = number;
     this.postDialDigits = postDialDigits;
@@ -250,6 +256,19 @@
     }
   }
 
+  @Override
+  public boolean onLongClick(View view) {
+    if (view == callbackButton) {
+      Intent intent = AccountSelectionActivity.createIntent(view.getContext(),
+          number, CallInitiationType.Type.CALL_DETAILS);
+      if (intent != null) {
+        DialerUtils.startActivityWithErrorToast(view.getContext(), intent);
+        return true;
+      }
+    }
+    return false;
+  }
+
   /** Listener for the call details header */
   interface CallDetailsHeaderListener {
 
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
index 26217ab..0f53d69 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
@@ -22,6 +22,7 @@
 import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
 import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
 import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.common.Assert;
 import com.android.dialer.dialercontact.DialerContact;
 import com.android.dialer.protos.ProtoParsers;
@@ -80,7 +81,8 @@
       CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderListener callDetailsHeaderListener,
       ReportCallIdListener reportCallIdListener,
-      DeleteCallDetailsListener deleteCallDetailsListener) {
+      DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore) {
     return new OldCallDetailsAdapter(
         /* context = */ this,
         contact,
@@ -88,7 +90,8 @@
         callDetailsEntryListener,
         callDetailsHeaderListener,
         reportCallIdListener,
-        deleteCallDetailsListener);
+        deleteCallDetailsListener,
+        callRecordingDataStore);
   }
 
   @Override
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
index 878803c..af54538 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
@@ -23,6 +23,7 @@
 import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
 import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
 import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
 import com.android.dialer.dialercontact.DialerContact;
 import com.android.dialer.glidephotomanager.PhotoInfo;
 import com.android.dialer.lettertile.LetterTileDrawable;
@@ -45,14 +46,16 @@
       CallDetailsEntryListener callDetailsEntryListener,
       CallDetailsHeaderListener callDetailsHeaderListener,
       CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
-      DeleteCallDetailsListener deleteCallDetailsListener) {
+      DeleteCallDetailsListener deleteCallDetailsListener,
+      CallRecordingDataStore callRecordingDataStore) {
     super(
         context,
         callDetailsEntries,
         callDetailsEntryListener,
         callDetailsHeaderListener,
         reportCallIdListener,
-        deleteCallDetailsListener);
+        deleteCallDetailsListener,
+        callRecordingDataStore);
     this.contact = contact;
   }
 
diff --git a/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
new file mode 100644
index 0000000..c6fb87f
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The CyanogenMod 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:fillColor="@color/call_record_playback_icon_color"
+        android:pathData="M 21,30.75 L 30,24 21,17.25 21,30.75 Z M 24,9 C 15.7125,9 9,15.7125 9,24 9,32.2875 15.7125,39 24,39 32.2875,39 39,32.2875 39,24 39,15.7125 32.2875,9 24,9 Z m 0,27 c -6.615,0 -12,-5.385 -12,-12 0,-6.615 5.385,-12 12,-12 6.615,0 12,5.385 12,12 0,6.615 -5.385,12 -12,12 z" />
+</vector>
+
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
index bfbb4f8..ffe3ade 100644
--- a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
@@ -19,7 +19,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:paddingTop="@dimen/call_entry_padding">
+    android:paddingTop="@dimen/call_entry_padding"
+    android:paddingBottom="@dimen/call_entry_bottom_padding">
 
   <com.android.dialer.calllogutils.CallTypeIconsView
       android:id="@+id/call_direction"
@@ -43,7 +44,6 @@
       style="@style/Dialer.TextAppearance.Secondary"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
-      android:layout_marginBottom="@dimen/call_entry_bottom_padding"
       android:layout_marginStart="@dimen/call_entry_text_left_margin"
       android:layout_marginEnd="16dp"
       android:layout_below="@+id/call_type"/>
@@ -56,12 +56,27 @@
       android:layout_marginEnd="@dimen/call_entry_padding"
       android:layout_alignParentEnd="true"/>
 
+  <TextView
+      android:id="@+id/play_recordings"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_below="@id/call_time"
+      android:paddingStart="@dimen/call_entry_text_left_margin"
+      android:paddingTop="8dp"
+      android:paddingBottom="8dp"
+      android:gravity="center_vertical"
+      android:drawableStart="@drawable/recording_playback_button"
+      android:drawablePadding="4dp"
+      android:background="?attr/selectableItemBackground"
+      android:visibility="gone"
+      style="@style/Dialer.TextAppearance.Secondary"/>
+
   <include
       android:id="@+id/ec_container"
       layout="@layout/ec_data_container"
       android:layout_width="match_parent"
       android:layout_height="@dimen/ec_container_height"
-      android:layout_below="@+id/call_time"
+      android:layout_below="@id/play_recordings"
       android:visibility="gone"/>
 
   <TextView
@@ -97,4 +112,4 @@
       android:layout_below="@id/rtt_transcript"
       android:background="@color/dialer_divider_line_color"
       android:visibility="gone"/>
-</RelativeLayout>
\ No newline at end of file
+</RelativeLayout>
diff --git a/java/com/android/dialer/calldetails/res/values/cm_strings.xml b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
new file mode 100644
index 0000000..076a494
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="play_recordings">
+        <item quantity="one">Play recording</item>
+        <item quantity="other">Play recordings</item>
+    </plurals>
+    <string name="call_playback_no_app_found_toast">No app could be found for playback of the selected recording.</string>
+</resources>
diff --git a/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java b/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
index a929961..c5ade3d 100644
--- a/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
+++ b/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
@@ -100,6 +100,9 @@
           context
               .getContentResolver()
               .delete(Calls.CONTENT_URI, selection.getSelection(), selection.getSelectionArgs());
+      context
+          .getContentResolver()
+          .notifyChange(Calls.CONTENT_URI, null);
 
       if (numRowsDeleted != coalescedIds.getCoalescedIdCount()) {
         LogUtil.e(
diff --git a/java/com/android/dialer/calllogutils/CallTypeIconsView.java b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
index 19c30c5..79d1e6e 100644
--- a/java/com/android/dialer/calllogutils/CallTypeIconsView.java
+++ b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
@@ -290,13 +290,15 @@
       int iconId = R.drawable.quantum_ic_call_received_white_24;
       Drawable drawable = largeIcons ? r.getDrawable(iconId) : getScaledBitmap(context, iconId);
       incoming = drawable.mutate();
-      incoming.setColorFilter(r.getColor(R.color.dialer_call_green), PorterDuff.Mode.MULTIPLY);
+      incoming.setColorFilter(r.getColor(R.color.answered_incoming_call),
+          PorterDuff.Mode.MULTIPLY);
 
       // Create a rotated instance of the call arrow for outgoing calls.
       iconId = R.drawable.quantum_ic_call_made_white_24;
       drawable = largeIcons ? r.getDrawable(iconId) : getScaledBitmap(context, iconId);
       outgoing = drawable.mutate();
-      outgoing.setColorFilter(r.getColor(R.color.dialer_call_green), PorterDuff.Mode.MULTIPLY);
+      outgoing.setColorFilter(r.getColor(R.color.answered_outgoing_call),
+          PorterDuff.Mode.MULTIPLY);
 
       // Need to make a copy of the arrow drawable, otherwise the same instance colored
       // above will be recolored here.
diff --git a/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java b/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java
new file mode 100644
index 0000000..a6ae552
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.calllogutils;
+
+import android.content.Context;
+import android.provider.CallLog;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import com.android.dialer.R;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilterSpinnerHelper implements AdapterView.OnItemSelectedListener {
+  private static String TAG = FilterSpinnerHelper.class.getSimpleName();
+
+  public interface OnFilterChangedListener {
+    void onFilterChanged(PhoneAccountHandle account, int callType);
+  }
+
+  private OnFilterChangedListener mListener;
+  private Spinner mAccountSpinner;
+  private ArrayAdapter<AccountItem> mAccountAdapter;
+  private Spinner mTypeSpinner;
+  private ArrayAdapter<TypeItem> mTypeAdapter;
+
+  public FilterSpinnerHelper(View rootView, boolean includeVoicemailType,
+      OnFilterChangedListener listener) {
+    mListener = listener;
+
+    mAccountAdapter = createAccountAdapter(rootView.getContext());
+    mAccountSpinner = initSpinner(rootView, R.id.filter_account_spinner, mAccountAdapter);
+
+    mTypeAdapter = createTypeAdapter(rootView.getContext(), includeVoicemailType);
+    mTypeSpinner = initSpinner(rootView, R.id.filter_status_spinner, mTypeAdapter);
+  }
+
+  @Override
+  public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+    int selectedAccountPos = Math.max(mAccountSpinner.getSelectedItemPosition(), 0);
+    int selectedTypePos = Math.max(mTypeSpinner.getSelectedItemPosition(), 0);
+    PhoneAccountHandle selectedAccount = mAccountAdapter.getItem(selectedAccountPos).account;
+    int selectedType = mTypeAdapter.getItem(selectedTypePos).value;
+    mListener.onFilterChanged(selectedAccount, selectedType);
+  }
+
+  @Override
+  public void onNothingSelected(AdapterView<?> parent) {
+  }
+
+  private Spinner initSpinner(View rootView, int spinnerResId, ArrayAdapter<?> adapter) {
+    Spinner spinner = rootView.findViewById(spinnerResId);
+    if (spinner == null) {
+      throw new IllegalArgumentException("Could not find spinner "
+          + rootView.getContext().getResources().getResourceName(spinnerResId));
+    }
+    spinner.setAdapter(adapter);
+    spinner.setOnItemSelectedListener(this);
+    if (adapter.getCount() <= 1) {
+      spinner.setVisibility(View.GONE);
+    }
+    return spinner;
+  }
+
+  private ArrayAdapter<AccountItem> createAccountAdapter(Context context) {
+    ArrayList<AccountItem> items = new ArrayList<>();
+    items.add(new AccountItem(null, context.getString(R.string.call_log_show_all_accounts)));
+    if (PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)) {
+      TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+      for (PhoneAccountHandle account : tm.getCallCapablePhoneAccounts()) {
+        String displayName = PhoneAccountUtils.getAccountLabel(context, account);
+        if (!TextUtils.isEmpty(displayName)) {
+          items.add(new AccountItem(account, displayName));
+        }
+      }
+    }
+
+    return new ArrayAdapter<AccountItem>(context, R.layout.call_log_filter_spinner_item, items);
+  }
+
+  private ArrayAdapter<TypeItem> createTypeAdapter(Context context, boolean includeVoicemail) {
+    ArrayList<TypeItem> items = new ArrayList<>();
+    items.add(new TypeItem(-1, context.getString(R.string.call_log_all_calls_header)));
+    items.add(new TypeItem(CallLog.Calls.INCOMING_TYPE,
+        context.getString(R.string.call_log_incoming_header)));
+    items.add(new TypeItem(CallLog.Calls.OUTGOING_TYPE,
+        context.getString(R.string.call_log_outgoing_header)));
+    items.add(new TypeItem(CallLog.Calls.MISSED_TYPE,
+        context.getString(R.string.call_log_missed_header)));
+    items.add(new TypeItem(CallLog.Calls.BLOCKED_TYPE,
+        context.getString(R.string.call_log_blacklist_header)));
+    if (includeVoicemail) {
+      items.add(new TypeItem(CallLog.Calls.VOICEMAIL_TYPE,
+          context.getString(R.string.call_log_voicemail_header)));
+    }
+
+    return new ArrayAdapter<TypeItem>(context, R.layout.call_log_filter_spinner_item, items);
+  }
+
+  private final class AccountItem {
+    public final PhoneAccountHandle account;
+    public final String label;
+
+    private AccountItem(PhoneAccountHandle account, String label) {
+      this.account = account;
+      this.label = label;
+    }
+
+    @Override
+    public String toString() {
+      return label;
+    }
+  }
+
+  private final class TypeItem {
+    public final int value;
+    public final String label;
+
+    private TypeItem(int value, String label) {
+      this.value = value;
+      this.label = label;
+    }
+
+    @Override
+    public String toString() {
+      return label;
+    }
+  }
+}
diff --git a/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
index 6fe3a82..6509af3 100644
--- a/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
+++ b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
@@ -64,7 +64,7 @@
    * @param number the number to display
    * @param formattedNumber the formatted number if available, may be null
    */
-  static CharSequence getDisplayNumber(
+  public static CharSequence getDisplayNumber(
       Context context,
       CharSequence number,
       int presentation,
diff --git a/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml
new file mode 100644
index 0000000..d7fdb46
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2013-2014, The Linux Foundation. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of The Linux Foundation, Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+  style="@style/Dialer.TextAppearance.Primary"
+  android:layout_width="match_parent"
+  android:layout_height="40dip"
+  android:paddingLeft="8dip"
+  android:paddingRight="8dip"
+  android:textSize="14sp"
+  android:textStyle="bold"
+  android:gravity="center_vertical"
+  android:singleLine="true"
+  android:textAllCaps="true" />
diff --git a/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml
new file mode 100644
index 0000000..ecaf1d1
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:orientation="horizontal"
+  android:paddingStart="@dimen/call_log_outer_margin"
+  android:paddingEnd="@dimen/call_log_outer_margin">
+  <Spinner
+    android:id="@+id/filter_account_spinner"
+    android:layout_width="0dip"
+    android:layout_height="@dimen/list_section_divider_min_height"
+    android:layout_weight="1.2"
+    android:layout_marginTop="6dip"
+    android:layout_marginBottom="6dip" />
+  <Spinner
+    android:id="@+id/filter_status_spinner"
+    android:layout_width="0dip"
+    android:layout_height="@dimen/list_section_divider_min_height"
+    android:layout_weight="1.8"
+    android:layout_marginTop="6dip"
+    android:layout_marginBottom="6dip" />
+</LinearLayout>
diff --git a/java/com/android/dialer/calllogutils/res/values/cm_strings.xml b/java/com/android/dialer/calllogutils/res/values/cm_strings.xml
new file mode 100644
index 0000000..d30aa17
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/cm_strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="call_log_show_all_accounts">All accounts</string>
+    <string name="call_log_all_calls_header">All calls</string>
+    <string name="call_log_incoming_header">Incoming calls only</string>
+    <string name="call_log_outgoing_header">Outgoing calls only</string>
+    <string name="call_log_missed_header">Missed calls only</string>
+    <string name="call_log_voicemail_header">Calls with voicemail only</string>
+    <string name="call_log_blacklist_header">Blocked calls only</string>
+</resources>
diff --git a/java/com/android/dialer/calllogutils/res/values/colors.xml b/java/com/android/dialer/calllogutils/res/values/colors.xml
index 3a9e3ae..40a522b 100644
--- a/java/com/android/dialer/calllogutils/res/values/colors.xml
+++ b/java/com/android/dialer/calllogutils/res/values/colors.xml
@@ -15,4 +15,12 @@
  ~ limitations under the License
  -->
 <resources>
-</resources>
\ No newline at end of file
+  <!-- Color for missed call icons. -->
+  <color name="missed_call">#C53929</color>
+  <!-- Color for answered call icons. -->
+  <color name="answered_incoming_call">#00a8ff</color>
+  <!-- Color for outgoing call icons. -->
+  <color name="answered_outgoing_call">#00c853</color>
+  <!-- Color for blocked call icons. -->
+  <color name="blocked_call">@color/dialer_secondary_text_color</color>
+</resources>
diff --git a/java/com/android/dialer/callrecord/AndroidManifest.xml b/java/com/android/dialer/callrecord/AndroidManifest.xml
new file mode 100644
index 0000000..5e25c73
--- /dev/null
+++ b/java/com/android/dialer/callrecord/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!-- Copyright (C) 2018 The LineageOS 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.dialer">
+
+  <uses-permission android:name="android.permission.RECORD_AUDIO" />
+  <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
+
+  <application>
+    <service android:name="com.android.dialer.callrecord.impl.CallRecorderService"
+        android:process="com.android.incallui" />
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/callrecord/CallRecording.aidl b/java/com/android/dialer/callrecord/CallRecording.aidl
new file mode 100644
index 0000000..e8d65be
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.aidl
@@ -0,0 +1,3 @@
+package com.android.dialer.callrecord;
+
+parcelable CallRecording;
diff --git a/java/com/android/dialer/callrecord/CallRecording.java b/java/com/android/dialer/callrecord/CallRecording.java
new file mode 100644
index 0000000..a594213
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.callrecord;
+
+import android.content.ContentValues;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public final class CallRecording implements Parcelable {
+  public String phoneNumber;
+  public long creationTime;
+  public String fileName;
+  public long startRecordingTime;
+  public long mediaId;
+
+  public static final Parcelable.Creator<CallRecording> CREATOR =
+      new Parcelable.Creator<CallRecording>() {
+    @Override
+    public CallRecording createFromParcel(Parcel in) {
+      return new CallRecording(in);
+    }
+
+    @Override
+    public CallRecording[] newArray(int size) {
+      return new CallRecording[size];
+    }
+  };
+
+  public CallRecording(String phoneNumber, long creationTime,
+      String fileName, long startRecordingTime, long mediaId) {
+    this.phoneNumber = phoneNumber;
+    this.creationTime = creationTime;
+    this.fileName = fileName;
+    this.startRecordingTime = startRecordingTime;
+    this.mediaId = mediaId;
+  }
+
+  public CallRecording(Parcel in) {
+    phoneNumber = in.readString();
+    creationTime = in.readLong();
+    fileName = in.readString();
+    startRecordingTime = in.readLong();
+    mediaId = in.readLong();
+  }
+
+  public static ContentValues generateMediaInsertValues(String fileName, long creationTime) {
+    final ContentValues cv = new ContentValues(5);
+
+    cv.put(MediaStore.Audio.Media.RELATIVE_PATH, "Recordings/Call recordings");
+    cv.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
+    cv.put(MediaStore.Audio.Media.DATE_TAKEN, creationTime);
+    cv.put(MediaStore.Audio.Media.IS_PENDING, 1);
+
+    final String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+    final String mime = !TextUtils.isEmpty(extension)
+        ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+    cv.put(MediaStore.Audio.Media.MIME_TYPE, mime);
+
+    return cv;
+  }
+
+  public static ContentValues generateCompletedValues() {
+    final ContentValues cv = new ContentValues(1);
+    cv.put(MediaStore.Audio.Media.IS_PENDING, 0);
+    return cv;
+  }
+
+  @Override
+  public void writeToParcel(Parcel out, int flags) {
+    out.writeString(phoneNumber);
+    out.writeLong(creationTime);
+    out.writeString(fileName);
+    out.writeLong(startRecordingTime);
+    out.writeLong(mediaId);
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public String toString() {
+    return "phoneNumber=" + phoneNumber + ", creationTime=" + creationTime +
+        ", fileName=" + fileName + ", startRecordingTime=" + startRecordingTime;
+  }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
new file mode 100644
index 0000000..81c1612
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The LineageOS 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.callrecord;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorFactory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+
+public class CallRecordingAutoMigrator {
+  private static final String TAG = "CallRecordingAutoMigrator";
+
+  @NonNull
+  private final Context appContext;
+  @NonNull private final DialerExecutorFactory dialerExecutorFactory;
+
+  public CallRecordingAutoMigrator(
+      @NonNull Context appContext,
+      @NonNull DialerExecutorFactory dialerExecutorFactory) {
+    this.appContext = Assert.isNotNull(appContext);
+    this.dialerExecutorFactory = Assert.isNotNull(dialerExecutorFactory);
+  }
+
+  public void asyncAutoMigrate() {
+    dialerExecutorFactory
+        .createNonUiTaskBuilder(new ShouldAttemptAutoMigrate(appContext))
+        .onSuccess(this::autoMigrate)
+        .build()
+        .executeParallel(null);
+  }
+
+  @TargetApi(26)
+  private void autoMigrate(boolean shouldAttemptAutoMigrate) {
+    if (!shouldAttemptAutoMigrate) {
+      return;
+    }
+
+    final CallRecordingDataStore store = new CallRecordingDataStore();
+    store.open(appContext);
+
+    final ContentResolver cr = appContext.getContentResolver();
+    final SparseArray<CallRecording> oldRecordingData = store.getUnmigratedRecordingData();
+    final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+    for (File recording : dir.listFiles()) {
+      OutputStream os = null;
+      try {
+        // determine data store ID and call creation time of recording
+        int id = -1;
+        long creationTime = System.currentTimeMillis();
+        for (int i = 0; i < oldRecordingData.size(); i++) {
+          if (TextUtils.equals(recording.getName(), oldRecordingData.valueAt(i).fileName)) {
+            creationTime = oldRecordingData.valueAt(i).creationTime;
+            id = oldRecordingData.keyAt(i);
+            break;
+          }
+        }
+
+        // create media store entry for recording
+        Uri uri = cr.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+            CallRecording.generateMediaInsertValues(recording.getName(), creationTime));
+        os = cr.openOutputStream(uri);
+
+        // copy file contents to media store stream
+        Files.copy(recording.toPath(), os);
+
+        // insert media store id to store
+        if (id >= 0) {
+          store.updateMigratedRecording(id, Integer.parseInt(uri.getLastPathSegment()));
+        }
+
+        // mark recording as complete
+        cr.update(uri, CallRecording.generateCompletedValues(), null, null);
+
+        // delete file
+        LogUtils.i(TAG, "Successfully migrated recording " + recording + " (ID " + id + ")");
+        recording.delete();
+      } catch (IOException e) {
+        LogUtils.w(TAG, "Failed migrating call recording " + recording, e);
+      } finally {
+        if (os != null) {
+          IOUtils.closeQuietly(os);
+        }
+      }
+    }
+
+    if (dir.listFiles().length == 0) {
+      dir.delete();
+    }
+
+    store.close();
+  }
+
+  private static class ShouldAttemptAutoMigrate implements Worker<Void, Boolean> {
+    private final Context appContext;
+
+    ShouldAttemptAutoMigrate(Context appContext) {
+      this.appContext = appContext;
+    }
+
+    @Nullable
+    @Override
+    public Boolean doInBackground(@Nullable Void input) {
+      if (Build.VERSION.SDK_INT < 26) {
+        return false;
+      }
+      if (appContext.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+          != PackageManager.PERMISSION_GRANTED) {
+        LogUtil.i(TAG, "not attempting auto-migrate: no storage permission");
+        return false;
+      }
+
+      final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+      if (!dir.exists()) {
+        LogUtil.i(TAG, "not attempting auto-migrate: no recordings present");
+        return false;
+      }
+
+      return true;
+    }
+  }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingDataStore.java b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
new file mode 100644
index 0000000..88b603b
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.callrecord;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Persistent data store for call recordings.  Usage:
+ * open()
+ * read/write operations
+ * close()
+ */
+public class CallRecordingDataStore {
+  private static final String TAG = "CallRecordingStore";
+  private SQLiteOpenHelper mOpenHelper = null;
+  private SQLiteDatabase mDatabase = null;
+
+  /**
+   * Open before reading/writing.  Will not open handle if one is already open.
+   */
+  public void open(Context context) {
+    if (mDatabase == null) {
+      mOpenHelper = new CallRecordingSQLiteOpenHelper(context);
+      mDatabase = mOpenHelper.getWritableDatabase();
+    }
+  }
+
+  /**
+   * close when finished reading/writing
+   */
+  public void close() {
+    if (mDatabase != null) {
+      mDatabase.close();
+    }
+    if (mOpenHelper != null) {
+      mOpenHelper.close();
+    }
+    mDatabase = null;
+    mOpenHelper = null;
+  }
+
+  /**
+   * Save a recording in the data store
+   *
+   * @param recording the recording to store
+   */
+  public void putRecording(CallRecording recording) {
+    final String insertSql = "INSERT INTO " +
+        CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ", " +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + ", " +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + ", " +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + ") " +
+        " VALUES (?, ?, ?, ?, ?)";
+
+    try {
+      SQLiteStatement stmt = mDatabase.compileStatement(insertSql);
+      int idx = 1;
+      stmt.bindString(idx++, recording.phoneNumber);
+      stmt.bindLong(idx++, recording.creationTime);
+      stmt.bindString(idx++, recording.fileName);
+      stmt.bindLong(idx++, System.currentTimeMillis());
+      stmt.bindLong(idx++, recording.mediaId);
+      long id = stmt.executeInsert();
+      Log.i(TAG, "Saved recording " + recording + " with id " + id);
+    } catch (SQLiteException e) {
+      Log.w(TAG, "Failed to save recording " + recording, e);
+    }
+  }
+
+  /**
+   * Get all recordings associated with a phone call
+   *
+   * @param phoneNumber phone number no spaces
+   * @param callCreationDate time that the call was created
+   * @return list of recordings
+   */
+  public List<CallRecording> getRecordings(String phoneNumber, long callCreationDate) {
+    List<CallRecording> resultList = new ArrayList<CallRecording>();
+
+    final String query = "SELECT " +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + "," +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+        " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+        " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " = ?" +
+        " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " = ?" +
+        " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " != 0" +
+        " ORDER BY " + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE;
+
+    String args[] = {
+      phoneNumber, String.valueOf(callCreationDate)
+    };
+
+    try {
+      Cursor cursor = mDatabase.rawQuery(query, args);
+      while (cursor.moveToNext()) {
+        String fileName = cursor.getString(0);
+        long creationDate = cursor.getLong(1);
+        long mediaId = cursor.getLong(2);
+        // FIXME: need to check whether media entry still exists?
+        resultList.add(
+            new CallRecording(phoneNumber, callCreationDate, fileName, creationDate, mediaId));
+      }
+      cursor.close();
+    } catch (SQLiteException e) {
+      Log.w(TAG, "Failed to fetch recordings for number " + phoneNumber +
+          ", date " + callCreationDate, e);
+    }
+
+    return resultList;
+  }
+
+  public SparseArray<CallRecording> getUnmigratedRecordingData() {
+    final String query = "SELECT " +
+        CallRecordingsContract.CallRecording._ID + "," +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + "," +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+        CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE +
+        " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+        " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " == 0";
+    final SparseArray<CallRecording> result = new SparseArray<>();
+
+    try {
+      Cursor cursor = mDatabase.rawQuery(query, null);
+      while (cursor.moveToNext()) {
+        int id = cursor.getInt(0);
+        String phoneNumber = cursor.getString(1);
+        String fileName = cursor.getString(2);
+        long creationDate = cursor.getLong(3);
+        CallRecording recording = new CallRecording(
+            phoneNumber, creationDate, fileName, creationDate, 0);
+        result.put(id, recording);
+      }
+      cursor.close();
+    } catch (SQLiteException e) {
+      Log.w(TAG, "Failed to fetch recordings for migration", e);
+    }
+
+    return result;
+  }
+
+  public void updateMigratedRecording(int id, int mediaId) {
+    ContentValues cv = new ContentValues(1);
+    cv.put(CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID, mediaId);
+    mDatabase.update(CallRecordingsContract.CallRecording.TABLE_NAME, cv,
+        CallRecordingsContract.CallRecording._ID + " = ?", new String[] { String.valueOf(id) });
+  }
+
+  static class CallRecordingsContract {
+    static interface CallRecording extends BaseColumns {
+      static final String TABLE_NAME = "call_recordings";
+      static final String COLUMN_NAME_PHONE_NUMBER = "phone_number";
+      static final String COLUMN_NAME_CALL_DATE = "call_date";
+      static final String COLUMN_NAME_RECORDING_FILENAME = "recording_filename";
+      static final String COLUMN_NAME_CREATION_DATE = "creation_date";
+      static final String COLUMN_NAME_MEDIA_ID = "media_id";
+    }
+  }
+
+  static class CallRecordingSQLiteOpenHelper extends SQLiteOpenHelper {
+    private static final int VERSION = 2;
+    private static final String DB_NAME = "callrecordings.db";
+
+    public CallRecordingSQLiteOpenHelper(Context context) {
+      super(context, DB_NAME, null, VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+      db.execSQL("CREATE TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+          CallRecordingsContract.CallRecording._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " TEXT," +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " LONG," +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + " TEXT, " +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + " LONG," +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " INTEGER DEFAULT 0" +
+          ");"
+      );
+
+      db.execSQL("CREATE INDEX IF NOT EXISTS phone_number_call_date_index ON " +
+          CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+          CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ");"
+      );
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+      if (oldVersion < 2) {
+        db.execSQL("ALTER TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME +
+            " ADD COLUMN " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+            " INTEGER DEFAULT 0;");
+      }
+    }
+  }
+}
diff --git a/java/com/android/dialer/callrecord/ICallRecorderService.aidl b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
new file mode 100644
index 0000000..acbd5f8
--- /dev/null
+++ b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
@@ -0,0 +1,37 @@
+package com.android.dialer.callrecord;
+
+import com.android.dialer.callrecord.CallRecording;
+
+/**
+ * Service for recording phone calls.  Only one recording may be active at a time
+ * (i.e. every call to startRecording should be followed by a call to stopRecording).
+ */
+interface ICallRecorderService {
+  /**
+   * Start a recording.
+   *
+   * @return true if recording started successfully
+   */
+  boolean startRecording(String phoneNumber, long creationTime);
+
+  /**
+   * stops the current recording
+   *
+   * @return call recording data including the output filename
+   */
+  CallRecording stopRecording();
+
+  /**
+   * Recording status
+   *
+   * @return true if there is an active recording
+   */
+  boolean isRecording();
+
+  /**
+   * Get recording currently in progress
+   *
+   * @return call recording object
+   */
+  CallRecording getActiveRecording();
+}
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderService.java b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
new file mode 100644
index 0000000..298e8ad
--- /dev/null
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.callrecord.impl;
+
+import android.app.Service;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import com.android.dialer.R;
+
+public class CallRecorderService extends Service {
+  private static final String TAG = "CallRecorderService";
+  private static final boolean DBG = false;
+
+  private MediaRecorder mMediaRecorder = null;
+  private CallRecording mCurrentRecording = null;
+
+  private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd_HHmmssSSS");
+
+  private final ICallRecorderService.Stub mBinder = new ICallRecorderService.Stub() {
+    @Override
+    public CallRecording stopRecording() {
+      return stopRecordingInternal();
+    }
+
+    @Override
+    public boolean startRecording(String phoneNumber, long creationTime) throws RemoteException {
+      return startRecordingInternal(phoneNumber, creationTime);
+    }
+
+    @Override
+    public boolean isRecording() throws RemoteException {
+      return mMediaRecorder != null;
+    }
+
+    @Override
+    public CallRecording getActiveRecording() throws RemoteException {
+      return mCurrentRecording;
+    }
+  };
+
+  @Override
+  public void onCreate() {
+    if (DBG) Log.d(TAG, "Creating CallRecorderService");
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return mBinder;
+  }
+
+  private int getAudioSource() {
+    return getResources().getInteger(R.integer.call_recording_audio_source);
+  }
+
+  private int getAudioFormatChoice() {
+    // This replicates PreferenceManager.getDefaultSharedPreferences, except
+    // that we need multi process preferences, as the pref is written in a separate
+    // process (com.android.dialer vs. com.android.incallui)
+    final String prefName = getPackageName() + "_preferences";
+    final SharedPreferences prefs = getSharedPreferences(prefName, MODE_MULTI_PROCESS);
+
+    try {
+      String value = prefs.getString(getString(R.string.call_recording_format_key), null);
+      if (value != null) {
+        return Integer.parseInt(value);
+      }
+    } catch (NumberFormatException e) {
+      // ignore and fall through
+    }
+    return 0;
+  }
+
+  private synchronized boolean startRecordingInternal(String phoneNumber, long creationTime) {
+    if (mMediaRecorder != null) {
+      if (DBG) {
+        Log.d(TAG, "Start called with recording in progress, stopping  current recording");
+      }
+      stopRecordingInternal();
+    }
+
+    if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO)
+        != PackageManager.PERMISSION_GRANTED) {
+      Log.w(TAG, "Record audio permission not granted, can't record call");
+      return false;
+    }
+
+    if (DBG) Log.d(TAG, "Starting recording");
+
+    mMediaRecorder = new MediaRecorder();
+    try {
+      int audioSource = getAudioSource();
+      int formatChoice = getAudioFormatChoice();
+      if (DBG) Log.d(TAG, "Creating media recorder with audio source " + audioSource);
+      mMediaRecorder.setAudioSource(audioSource);
+      mMediaRecorder.setOutputFormat(formatChoice == 0
+          ? MediaRecorder.OutputFormat.AMR_WB : MediaRecorder.OutputFormat.MPEG_4);
+      mMediaRecorder.setAudioEncoder(formatChoice == 0
+          ? MediaRecorder.AudioEncoder.AMR_WB : MediaRecorder.AudioEncoder.AAC);
+    } catch (IllegalStateException e) {
+      Log.w(TAG, "Error initializing media recorder", e);
+      mMediaRecorder.reset();
+      mMediaRecorder.release();
+      mMediaRecorder = null;
+      return false;
+    }
+
+    String fileName = generateFilename(phoneNumber);
+    Uri uri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+            CallRecording.generateMediaInsertValues(fileName, creationTime));
+
+    try {
+      ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
+      if (pfd == null) {
+        throw new IOException("Opening file for URI " + uri + " failed");
+      }
+      mMediaRecorder.setOutputFile(pfd.getFileDescriptor());
+      mMediaRecorder.prepare();
+      mMediaRecorder.start();
+
+      long mediaId = Long.parseLong(uri.getLastPathSegment());
+      mCurrentRecording = new CallRecording(phoneNumber, creationTime,
+              fileName, System.currentTimeMillis(), mediaId);
+      return true;
+    } catch (IOException | IllegalStateException e) {
+      Log.w(TAG, "Could not start recording", e);
+      getContentResolver().delete(uri, null, null);
+    } catch (RuntimeException e) {
+      getContentResolver().delete(uri, null, null);
+      // only catch exceptions thrown by the MediaRecorder JNI code
+      if (e.getMessage().indexOf("start failed") >= 0) {
+        Log.w(TAG, "Could not start recording", e);
+      } else {
+        throw e;
+      }
+    }
+
+    mMediaRecorder.reset();
+    mMediaRecorder.release();
+    mMediaRecorder = null;
+
+    return false;
+  }
+
+  private synchronized CallRecording stopRecordingInternal() {
+    CallRecording recording = mCurrentRecording;
+    if (DBG) Log.d(TAG, "Stopping current recording");
+    if (mMediaRecorder != null) {
+      try {
+        mMediaRecorder.stop();
+        mMediaRecorder.reset();
+        mMediaRecorder.release();
+      } catch (IllegalStateException e) {
+        Log.e(TAG, "Exception closing media recorder", e);
+      }
+
+      Uri uri = ContentUris.withAppendedId(
+          MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCurrentRecording.mediaId);
+      getContentResolver().update(uri, CallRecording.generateCompletedValues(), null, null);
+
+      mMediaRecorder = null;
+      mCurrentRecording = null;
+    }
+    return recording;
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    if (DBG) Log.d(TAG, "Destroying CallRecorderService");
+  }
+
+  private String generateFilename(String number) {
+    String timestamp = DATE_FORMAT.format(new Date());
+
+    if (TextUtils.isEmpty(number)) {
+      number = "unknown";
+    }
+
+    int formatChoice = getAudioFormatChoice();
+    String extension = formatChoice == 0 ? ".amr" : ".m4a";
+    return number + "_" + timestamp + extension;
+  }
+
+  public static boolean isEnabled(Context context) {
+    return context.getResources().getBoolean(R.bool.call_recording_enabled);
+  }
+}
diff --git a/java/com/android/dialer/callrecord/res/values/config.xml b/java/com/android/dialer/callrecord/res/values/config.xml
new file mode 100644
index 0000000..7aabd6c
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/values/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     Copyright (C) 2015 The CyanogenMod 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.
+-->
+
+<resources>
+    <bool name="call_recording_enabled">false</bool>
+    <!-- 1 (MIC) for microphone audio source (default)
+         4 (VOICE_CALL) if supported by device for voice call uplink + downlink audio source -->
+    <integer name="call_recording_audio_source">1</integer>
+</resources>
diff --git a/java/com/android/dialer/callrecord/res/xml/call_record_states.xml b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
new file mode 100644
index 0000000..06309d7
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
@@ -0,0 +1,1341 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 The LineageOS 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.
+ */
+-->
+<call-record-allowed-flags>
+    <!-- Disable recording for Andorra:
+         Article 183 of the Andorran Penal Code sets a prison sentence of one to four years as the
+         punishment for attempting or succeeding to infringe on the privacy of another person
+         without his or her consent. This includes intercepting calls or using technical means to
+         listen, transmit, record or reproduce their calls. Article 185 and 188 of the Andorran
+         Penal Code describe the length of imprisonment when sharing recordings with third parties,
+         even if one has not partaken in their creation, unless the party was unaware of their
+         illicit origin. The attempt to do any of the above is also punishable by law.
+
+         Penal Code, as of 2014:
+           https://sherloc.unodc.org/res/cld/document/codi_penal_andorra_as_of_2014_html/Andorra_codi_penal_as_of_2014.pdf
+    -->
+    <country iso="ad" allowed="false" />
+
+    <!-- Enable recording for Albania:
+         Relevant laws and/or legal precedents:
+         Article 121, 122 and 123 of the Albanian Penal Code cover the right to privacy, wiretapping
+         and harming other via dissemination of their secrets. Based on these, it is not a criminal
+         offense to record your own calls, but sharing them with a third party or using them to harm
+         the other party in said calls is a criminal offense.
+
+         Penal Code: (nonencrypted link)
+           http://rai-see.org/wp-content/uploads/2015/08/Criminal-Code-11-06-2015-EN.pdf
+    -->
+    <country iso="al" allowed="true" />
+
+    <!-- Enable recording for Armenia:
+         The Armenian Criminal Code discusses the legality and punishment of call recordings, when
+         recorded by a third party, otherwise known as wiretapping. Since the Criminal Code does not
+         specifically mention call recording done by a person that is a party to the call, with or
+         without consent, then the act of doing so is not a criminal offense, as it carries no
+         punishment whatsoever.
+
+         Criminal Code:
+           https://www.unodc.org/res/cld/document/armenia_criminal_code_html/Armenia_Criminal_Code_of_the_Republic_of_Armenia_2009.pdf
+
+         Two examples of usage of call recordings, without persecution:
+           https://fip.am/803
+           https://fip.am/4500
+    -->
+    <country iso="am" allowed="true" />
+
+    <!-- Enable recording for Argentina:
+         Argentina follows the continental law system. If a law does not exist, which defines
+         something as a crime, it is not a crime. Judges in Argentina make decisions based on their
+         reading of the law, and not on precedents. Call recording is currently not defined as a
+         crime in any law. This is defined in Section 19 of the Constitution of Argentina.
+
+         Constitution: (nonencrypted link)
+           http://www.senadoctes.gov.ar/constitucion-arg/Constitution%20of%20the%20Argentine%20Nation.htm
+
+         Example of usage of call recordings as legal evidence:
+           https://www.scribd.com/document/326647534/H-P-C-F-s-recurso-de-casacion
+    -->
+    <country iso="ar" allowed="true" />
+
+    <!-- Enable recording for American Samoa:
+         Federal Law 18 USC § 2511(2)(d) defines the recording of a call as legal when one party to
+         the call agrees to it, if said call recording is not done with the intention of committing
+         a crime. This territory of the United States conforms with its Federal legislation.
+         For further information, check 'us'.
+
+         U.S. Code: Title 18 - Crimes and Criminal Procedure:
+           https://www.law.cornell.edu/uscode/text/18/2511
+    -->
+    <country iso="as" allowed="true" />
+
+    <!-- Enable recording for Austria
+         According to Article 93 (3) of Austrian Communications Law, known as TKG 2003
+         Kommunikationsgeheimnis, it is illegal to recordor pass on information about a call, unless
+         you are one of the parties in that call. While recording is not illegal, sharing the
+         recording might be a punishable offense, without the consent of both sides.
+
+         Communications Law:
+           https://www.jusline.at/gesetz/tkg/paragraf/93
+           https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20002849
+    -->
+    <country iso="at" allowed="true" />
+
+    <!-- Disable recording for Australia:
+         Australian Capital Territory:
+           Subsection 4(3)(b) Listening Devices Act 1992 (ACT)
+             https://www8.austlii.edu.au/cgi-bin/viewdoc/au/legis/act/consol_act/lda1992181/s4.html
+           A person must not use a listening device with the intention of recording a private
+           conversation to which the person is a party. This does not apply when said listening
+           device is used by, or on behalf of, a party to a private conversation if a principal
+           party to the conversation consents to the listening device being so used, and the
+           recording of the conversation is considered by that principal party, on reasonable
+           grounds, to be necessary for the protection of that principal party's lawful interests;
+           or the recording is not made for the purpose of communicating or publishing the
+           conversation, or a report of the conversation, to any person who is not a party to the
+           conversation.
+
+         New South Wales:
+           Subsection 7(3)(b) Surveillance Devices Act 2007 (NSW)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nsw/consol_act/sda2007210/s7.html
+           A person must not knowingly use a listening device to record a private conversation to
+           which the person is a party. This does not apply when a principal party to the
+           conversation consents to the listening device being so used and the recording of the
+           conversation is reasonably necessary for the protection of the lawful interests of that
+           principal party, or is not made for the purpose of communicating or publishing the
+           conversation, or a report of the conversation, to persons who are not parties to the
+           conversation.
+
+         Northern Territory:
+           Subsection 11(1)(a) Surveillance Devices Act 2007 (NT)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s11.html
+           Subsection 43, Emergency use of listening device in public interest
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s43.html
+           It is an offence to use a listening device to record a private conversation to which the
+           person is not a party and the device is used without the express or implied consent of
+           each party to the conversation. Under Section 43, a person may use a listening device to
+           record a private conversation if at the time of use there are reasonable grounds for
+           believing the circumstances are so serious and the matter is of such urgency that the use
+           of the device is in the public interest.
+
+         Queensland:
+           Subsection 43(2)(a) Invasion of Privacy Act 1971 (Qld)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/qld/consol_act/iopa1971222/s43.html
+           A person is guilty of an offence, if the person uses a listening device to record a
+           private conversation and is liable to a maximum penalty of 40 penalty units or
+           imprisonment for 2 years. This does not apply when the person using the listening device
+           is a party to the private conversation.
+
+         South Australia:
+           Subsection 4(2)(a)(ii) Surveillance Devices Act 2016 (SA)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/sa/consol_act/sda2016210/s4.html
+           A person must not knowingly use a listening device to record a private conversation to
+           which the person is, or is not a party. The maximum penalty is $15 000 or imprisonment
+           for 3 years. This does not apply if the use of a listening device is done by a party to
+           the private conversation if the use of the device is reasonably necessary for the
+           protection of the lawful interests of that person.
+
+         Tasmania:
+           Subsection 5(3)(b) Listening Devices Act 1991 (TAS)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/tas/consol_act/lda1991181/s5.html
+           A person shall not use a listening device to record a private conversation to which the
+           person is, or is not, a party. This does not apply when the listening device is used to
+           obtain evidence or information in connection with an imminent threat of serious violence
+           to persons or of substantial damage to property, or a serious narcotics offence, if the
+           person using the listening device believes on reasonable grounds that it was necessary to
+           use the device immediately to obtain that evidence or information. This does not apply if
+           a principal party to the conversation consents to the listening device being so used and
+           the recording of the conversation is reasonably necessary for the protection of the
+           lawful interests of that principal party or the recording of the conversation is not made
+           for the purpose of communicating or publishing the conversation, or a report of the
+           conversation, to persons who are not parties to the conversation.
+
+         Victoria:
+           Subsection 6(1) Surveillance Devices Act 1999 (NSW)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/vic/consol_act/sda1999210/s6.html
+           A person must not knowingly use a listening device to record a private conversation to
+           which the person is not a party. The penalty is up to 2 years imprisonment and up to 240
+           penalty units, or both.
+
+         Western Australia:
+           Subsection 5(3)(d) Surveillance Devices Act 1998 (WA)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s5.html
+           Subsection 26(1)(2)(3)(b)
+             https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s26.html
+           A person shall not use a listening device to record a conversation to which that person
+           is, or is not, a party. The penalty is $5 000 or imprisonment for 12 months, or both.
+           This does not apply to the use of a listening device by a party to a private conversation
+           if that principal party consents to its use, as reasonably necessary for the protection
+           of the lawful interests of that principal party. It is also not applicable in cases where
+           a person who is a party to a private conversation, or is acting on behalf of a party to a
+           private conversation, uses a listening device to record said private conversation,
+           believing that the use of the listening device is in the public interest.
+
+         Summary:
+           Most states and territories allow to make a recording of your personal conversations
+           under specific circumstances. The wording of the laws themselves are open to
+           legal interpretation and can be used against users. Until the laws above are presented
+           in a more clear way or enough evidence is shown to substantiate how the courts interpret
+           them when prosecuting private citizens, Australia shall not have call recording enabled.
+    -->
+    <country iso="au" allowed="false" />
+
+    <!-- Enable recording for Bosnia and Herzegovina:
+         Republika Srpska:
+           Article 155 - Unauthorized eavesdropping and tone recording.
+           Paragraph 1 specifically states 'which is not intended for him', which in context means
+           that a person may record anything which is intended for him or her. Paragraph 2 defines
+           the creation of a record with the intent to abuse and/or misuse it, or the act of sharing
+           it with a third party as a criminal offense.
+           Other Articles that might be relevant:
+             Article 153, Privacy of letters, telegrams and others.
+             Article 157, Unauthorized use of Personal Data.
+
+         Federation of Bosnia and Herzegovina:
+           Article 188, Unauthorized Tapping and Sound Recording, defines the recording of any call
+           'which is not intended for public or private knowledge', as a criminal offense.
+
+         Brčko District:
+           Article 185 of the Criminal Code specifically states 'which was not intended for him',
+           which in context means that a party may record any call which is indended for said party.
+           Wiretapping is considered a crime and is criminally punishable.
+
+         Further information:
+           The Criminal Codes of the legal entities of Bosnia and Herzegovina, namely Republika
+           Srpska, the Federation of Bosnia and Herzegovina and the Brčko District, can all be found
+           through the link below. Please note that these three entities currently have separate
+           laws, due to administrative and/or judicial autonomy. It should be noted that these
+           different entities have over 15 police forces, each with its own jurisdiction.
+           https://www.legislationline.org/documents/section/criminal-codes/country/40/Bosnia%20and%20Herzegovina/show
+    -->
+    <country iso="ba" allowed="true" />
+
+    <!-- Enable recording for Bangladesh:
+           Based on the Evidence Act of 1872, audio recordings may be admissible as a form of
+           evidence. In civil cases, call recording may be used, but judges decide on when they are
+           admissable. Recordings are considered admissible evidence when a witness is deposed and
+           testifies to their validity before a court. Parties may rely on recordings during civil
+           cases, in order to support their side's version of events. While the use of audio
+           recordings as a form of evidence is common, the Supreme Court has not stated whether
+           recordings can be used as evidence blindly, thus their use is on a case-to-case basis.
+
+           Legal discussion:
+             https://www.thedailystar.net/news/your-advocate-2
+
+           Evidence Act of 1872:
+             https://acc.portal.gov.bd/sites/default/files/files/acc.portal.gov.bd/law/7e753733_a368_471e_aef9_b4525cb5082c/The_Evidence_Act,_1872_(Act_No._I_of_1872).pdf
+    -->
+    <country iso="bd" allowed="true" />
+
+    <!-- Enable recording for Belgium:
+         As stated in the official response below, Belgian law does not consider the recording of
+         one's personal communications as a punishable offense. Using said recordings in a
+         fraudulent and/or demeaning way does carry the potential for liability and/or prosecution
+         by the state. The recording of one's own calls might be regarded as a form of personal data
+         processing, depending on the specifics of the case. Specific laws and cases are quoted
+         within the official response. The Belgian municipality of Baarle-Hertog, consisting of a
+         number of exclaves, has territory which is within the Dutch province of North Brabant, and
+         as such it may not be within the confines of the Belgian ISO and its inherent laws.
+
+         Official Response by Belgian Minister (QRVA 50 157, pages 20199-20202, 24/02/2003):
+           https://www.lachambre.be/QRVA/pdf/50/50K0157.pdf
+    -->
+    <country iso="be" allowed="true" />
+
+    <!-- Enable recording for Bulgaria:
+         Article 32(2) of the Bulgarian Constitution states that it is an inviolable right for
+         people to not be followed, photographed, recorded (audio and/or video) without being
+         notified and/or despite his or her explicit disagreement to said actions, except where the
+         law allows for said actions. The Code of Criminal Procedure, Part III, Articles 125 and
+         126, page 34, deal with the use of recordings as evidence. No law explicitly tackles the
+         issue of consent when recording one's personal telephone calls. Based on the available
+         documentation and the attached example of lack of state prosecution, recording one's own
+         calls is not legal, nor is it a criminal offense. Personal recordings are unlikely to be
+         a valid form of evidence in a court of law.
+
+         Constitution:
+           https://www.parliament.bg/bg/const
+
+         Code of Criminal Procedure:
+           https://www.mvr.bg/docs/default-source/normativnauredba/3da73fed-npk-pdf.pdf
+
+         Example of lack of state prosecution:
+           https://goo.gl/HQPUup
+    -->
+    <country iso="bg" allowed="true" />
+
+    <!-- Enable recording for Brazil:
+         Call recording is not a criminal offense when it the recording is made by one of the two
+         parties of said call. Interception by a third party is illegal and punishable by law,
+         unless done according to the requirements set out in Law 9296. There may be some debate, as
+         far as the use of a call recording as legitimate evidence. Further information is available
+         in the attached legal discussions below.
+
+         Law 9296 of the 24th of July 1996: (nonencrypted link)
+           http://www.planalto.gov.br/ccivil_03/leis/l9296.htm
+
+         Constitution of Brazil, Art 5º, X and XII:
+           https://www2.camara.leg.br/legin/fed/consti/1988/constituicao-1988-5-outubro-1988-322142-publicacaooriginal-1-pl.html
+
+         Legal discussions:
+           https://direitosbrasil.com/gravar-conversa-e-crime/
+           https://meusitejuridico.com.br/2018/04/02/stj-e-licita-gravacao-de-conversa-feita-pelo-destinatario-de-solicitacao-de-vantagem-indevida
+           https://moisesandrade.jusbrasil.com.br/artigos/121944095/constitucionalidade-do-uso-da-gravacao-clandestina-como-meio-de-prova
+    -->
+    <country iso="br" allowed="true" />
+
+    <!-- Enable recording for Belarus:
+         Article 28 of the Constitution of Belarus covers the right to privacy. Article 179 of the
+         Criminal Code of Belarus covers situations in which a person's privacy is violated by way
+         of any secret being shared without his or her consent, but no specific term of imprisonment
+         or fine is mentioned. The wording of the article is aimed at the collecting and sharing of
+         'a personal or family secret of another person'. Creating a call recording for personal use
+         is not covered by this article, as privacy is not inherently guaranteed. The use of call
+         recordings as evidence in a court of law is dubious. The sharing of a call recording could
+         be considered as punishable by law, depending on the circumstances.
+
+         Belarusian Constitution: (nonencrypted link)
+           http://www.pravo.by/pravovaya-informatsiya/normativnye-dokumenty/konstitutsiya-respubliki-belarus
+
+         Belarusian Criminal Code:
+           https://etalonline.by/?type=text&regnum=HK9900275#load_text_none_1_
+    -->
+    <country iso="by" allowed="true" />
+
+    <!-- Enable recording for Canada:
+         Any intended recipient of a communication is entitled to record it, based on Section 184(2)
+         Subsection (1) of the Criminal Code of Canada. There are numerous legal cases that validate
+         the interception of private communications by parties to the conversation as not illegal.
+         For a more in-depth look, refer to the LegalTree article below.
+
+         Criminal Code:
+           https://laws-lois.justice.gc.ca/eng/acts/C-46/page-1.html
+
+         LegalTree article:
+           https://legaltree.ca/node/908
+
+         Legal articles:
+           https://lambertavocatinc.com/avocat-montreal/enregistrer-conversation-legal/
+           https://www.avocat.qc.ca/affaires/iitelephone.htm
+    -->
+    <country iso="ca" allowed="true" />
+
+    <!-- Disable recording for Switzerland:
+         According to Article 179 of the Swiss Criminal Code of 21 December 1937, it is a criminal
+         offense to store, record, or share the recording of a call, even when one is part of said
+         call. Explicit consent is required by both parties for a recording to be legal.
+
+         Criminal Code:
+           https://www.admin.ch/opc/en/classified-compilation/19370083/index.html#a179ter
+    -->
+    <country iso="ch" allowed="false" />
+
+    <!-- Enable recording for Chile:
+         The Chilean law is considered a type of civil law, hence judges base their decisions on
+         their own reading of the law. The Chilean Supreme Court ruled in favor of accepting
+         one-party consent call recording as a form of legal evidence, hence the act of recording
+         your own calls is not criminally punishable, as can be seen in the Chilean Penal Code.
+
+         Penal Code:
+           https://www.leychile.cl/Navegar?idNorma=1984
+
+         Article on one-party consent:
+           https://radio.uchile.cl/2018/04/22/grabacion-es-aceptada-como-prueba-en-juicio-por-practicas-antisindicales/
+    -->
+    <country iso="cl" allowed="true" />
+
+    <!-- Enable recording for China:
+         No clear definition exists on the matter of call recordings being made by a private
+         citizen within the Criminal Law of the People's Republic of China. Depending on whether
+         said call recording was made and/or published with malicious intent, it may or may not be
+         admissible in court. For further information
+
+         Criminal Law of the People's Republic of China: (nonencrypted link)
+           http://english.court.gov.cn/2015-12/01/content_22595464.htm
+
+         Supreme People's Court Provisions on Evidence in Civil Procedures: (nonencrypted link)
+           http://en.pkulaw.cn/display.aspx?cgid=38083&lib=law
+    -->
+    <country iso="cn" allowed="true" />
+
+    <!-- Enable recording for Costa Rica:
+         One may record one's own calls, as long as they are calls between said person and only one
+         other party, that is two say two sides. Calls between 3 or more people can not be legally
+         recorded without all sides agreeing to one person doing so, as long as said person is a
+         part of the call and not wiretapping or eavesdropping. Recording calls with more than 2
+         participants requires the express consent of all other parties. Article 29 of the
+         Communication Law of 1994 specifies under what circumstances one may or may not do so.
+
+         Communication Law: (nonencrypted link)
+           http://www.pgrweb.go.cr/scij/Busqueda/Normativa/Normas/nrm_texto_completo.aspx?param1=NRM&nValor1=1&nValor2=16466&strTipM=FN
+
+         Article:
+           https://www.laprensalibre.cr/Noticias/detalle/75929/ojo-conversaciones-grabadas-pueden-usarse-como-prueba-en-juicio
+    -->
+    <country iso="cr" allowed="true" />
+
+    <!-- Enable recording for Cyprus:
+         The Cypriot Penal Code does not explicitly cover the act of wiretapping or recording one's
+         own calls. Based on this, it is not a criminal offense to record personal calls. Article
+         369 of the Cypriot Penal Code states that anyone who knows that another is planning to
+         commit a criminal offense, yet fails to use any reasonable means to prevent said crime, is
+         guilty of misconduct, which can be used as a reason for recording one's own calls, should
+         the need arise to quote a legal document.
+
+         Penal Code: (nonencrypted link)
+           http://www.cylaw.org/nomoi/enop/non-ind/0_154/index.html
+    -->
+    <country iso="cy" allowed="true" />
+
+    <!-- Enable recording for Czech Republic:
+         Case Law File Number 21 502/2000 of the Supreme Court specifies that even when evidence is
+         acquired or provided in contravention to legal regulations and/or personal rights, it shall
+         not be deemed as inadmissible. This, as well as other information, is accessible in the
+         Constitutional Court Finding 191/05 of the 13th of September 2006.
+
+         Constitutional Court Finding:
+           https://nalus.usoud.cz/Search/GetText.aspx?sz=1-191-05_2
+    -->
+    <country iso="cz" allowed="true" />
+
+    <!-- Disable recording for Germany:
+         According to Section 201 of the German Criminal Code - Violation of the privacy of the
+         spoken word, making an audio recording of the privately spoken words of another or making
+         such a recording accessible by a third party will result in up to three years of
+         imprisonment. Article 10 of the German Constitution explicitly states that the secrecy of
+         telecommunications is inviolable. There are notable exceptions, such as the use of
+         recordings when in a legitimate self-defense situation. Article 227 of the German Civil
+         Code notes that acting in one's own defense is not unlawful, which is also explained in
+         Article 32 of the German Criminal Code. Article 88 of the Telecommunications Act defines
+         telecommunications secrecy. The German municipality of Büsingen am Hochrhein is an exclave
+         within the territorial confines of Switzerland, and as such it may not be within the
+         confines of the German ISO and its inherent laws.
+
+         Civil Code:
+           https://www.gesetze-im-internet.de/bgb/__227.html
+
+         Criminal Code:
+           https://www.gesetze-im-internet.de/stgb/__32.html
+
+         Constitution:
+           https://www.gesetze-im-internet.de/gg/art_10.html
+
+         Telecommunications Act:
+           https://www.gesetze-im-internet.de/tkg_2004/__88.html
+
+         Wikipedia article on self-defense laws in Germany:
+           https://de.wikipedia.org/wiki/Notwehr_(Deutschland)
+
+         Explanation of lawful use of a recording in a legal dispute:
+           https://www.anwalt.de/rechtstipps/gespraechsmitschnitte-als-beweismittel-ungeeignet_057458.html
+    -->
+    <country iso="de" allowed="false" />
+
+    <!-- Enable recording for Denmark, Faroe Islands and Greeenland:
+         Chapter 27, Article 263(3) of the Criminal Code of Denmark denotes that a person is liable
+         for criminal punishment when he or she intercepts or records telephone conversations to
+         which he or she is not a party. The articles in Chapter 27 cover a lot of different
+         situations, including the dissemination of recordings, which may lead to a fine or prison
+         sentence. The act of recording a conversation that one is a part of is not covered
+         explicitly, hence it is not a criminal offense in the eyes of the law.
+
+         Criminal Code:
+           https://www.retsinformation.dk/Forms/r0710.aspx?id=164192#Kap27
+    -->
+    <country iso="dk,fo,gl" allowed="true" />
+
+    <!-- Enable recording for Estonia:
+         Recording your calls for personal use is not a criminal offense. Sharing said calls with a
+         third party is a criminal offence, hence punishable by law, except in cases where said
+         calls are shared by a journalist.
+
+         Constitution, Paragraph 43:
+           https://www.pohiseadus.ee/index.php?sid=1&p=43
+
+         Instructions for call recording (GDPR equivalent):
+           https://www.aki.ee/sites/www.aki.ee/files/elfinder/article_files/Telefonik%C3%B5nede%20salvestamise%20lubatavuse%20juhend.pdf
+
+         Legal article:
+           https://digi.geenius.ee/rubriik/uudis/millistel-juhtudel-tohib-eestis-telefonikone-salvestada-ja-selle-sisu-avaldada/
+    -->
+    <country iso="ee" allowed="true" />
+
+    <!-- Enable recording for Spain:
+         Based on the decision of the Spanish Constitutional Tribunal of November the 29th, 1984, it
+         is legal for a party to record his or her calls without notifying the other party. Sharing
+         said recording with a third party is not protected and may make the party that has shared
+         the recording liable to a civil suit, to be initiated by the aggrieved party. Unless done
+         so for judicial purposes, it is punishable to disclose or share the recording or the gist
+         of the recording to other parties. The town of Llívia is a Spanish exclave within the
+         territory of the Republic of France, and as such, it may not be within the confines of the
+         Spanish ISO and its inherent laws.
+
+         Decision of the Spanish Constitutional Tribunal:
+           https://hj.tribunalconstitucional.es/eu/Resolucion/Show/367
+
+         Legal articles: (nonencrypted link)
+           http://belegal.com/blog-by-antonio-flores/validity-of-recorded-telephone-conversations-in-spain/
+           https://www.fonvirtual.com/blog/la-grabacion-de-llamadas/
+           https://www.legalisconsultores.es/2014/04/es-legal-realizar-grabaciones-su-aportacion-en-juicios/
+    -->
+    <country iso="es" allowed="true" />
+
+    <!-- Enable recording for Finland:
+         As a private citizen, one may record any call they participate in. There is no requirement
+         to make other parties aware of the recording, but the use of said recordings, depending on
+         their content, may be subject to various laws, such as data protection (privacy)
+         legislation, libel laws, laws governing trade and national secrets, non-disclosure
+         agreements and so on.
+
+         Bureau of Data Ombudsman:
+         https://web.archive.org/web/20180517050133/http://www.tietosuoja.fi/sv/index/useinkysyttya/puheluidennauhoittaminen.html
+    -->
+    <country iso="fi" allowed="true" />
+
+    <!-- Enable recording for France, Saint Barthélemy, French Guayana, Guadeloupe, Saint Martin,
+                              Martinique, New Caledonia, French Polynesia, Saint Pierre & Miquelon,
+                              Réunion, Wallis-et-Futuna and Mayotte:
+         While recording calls without consent, as a third party, is punishable, it depends on
+         whether said recording was created or used with a malicious intent. Judges are free to view
+         said recordings as a form of evidence and base their final decisions with their help.
+         Recording your own calls as a private citizen is not a criminal offense. Sharing said
+         recordings, with the intent to harm the other party in any way, is a criminal offense.
+
+         Penal Code:
+           https://www.legifrance.gouv.fr/affichCode.do;jsessionid=3E84EAC0F63D49FC16A28B8D90EFF1D2.tplgfr44s_2?idSectionTA=LEGISCTA000006165309&cidTexte=LEGITEXT000006070719&dateTexte=20150413
+
+         Civil Code:
+           https://www.legifrance.gouv.fr/affichCode.do;jsessionid=1A7384A63066DBE1E1D8C732E698F844.tplgfr23s_3?idSectionTA=LEGISCTA000006117610&cidTexte=LEGITEXT000006070721&dateTexte=20190606
+
+         Legal article on call recordings as evidence:
+           https://www.annuaireavocats.fr/articles/enregistrer-une-conversation-a-linsu-dune-personne-est-ce-legal
+
+         Legal article on recording in the workplace:
+           https://www.cnil.fr/fr/lecoute-et-lenregistrement-des-appels-sur-le-lieu-de-travail
+    -->
+    <country iso="fr,bl,gf,gp,mf,mq,nc,pf,pm,re,wf,yt" allowed="true" />
+
+    <!-- Enable recording for United Kingdom:
+         Recording one's own calls is not a criminal offence and is not prohibited. As long as the
+         recording is for personal use, consent and/or notification of the other party are not
+         required. Call recordings can be used as evidence, since it is based on a trite law.
+         Sharing said call recordings with a third party, without consent, may be a criminal offence
+         and punishable.
+
+         Use as evidence (p. 3):
+           https://www.bailii.org/uk/cases/UKPC/1954/1954_43.pdf
+
+         Legal articles:
+           https://www.computertel.co.uk/article?ref=Call-Recording-Law-in-the-UK-2018-edition
+           https://www.dma-law.co.uk/is-it-illegal-to-record-conversations/
+    -->
+    <country iso="gb" allowed="true" />
+
+    <!-- Enable recording for Georgia:
+         The Constitution of Georgia, Chapter Two - Fundamental Human Rights, Article 15(2) states
+         that personal communication(s) are inviolable and that said right may only be restricted in
+         accordance with the law, to ensure national security or public safety, or to protect the
+         rights of other parties, insofar as it is necessary in a democratic society, based on a
+         court decision or without a court decision in cases of urgent necessity, as provided by the
+         law. Articles 157, 158 and 159 of the Criminal Code of Georgia deal with the disclosure of
+         private information, personal data, the violation of the secrecy of private communication
+         and the violation of secrecy of personal correspondence, phone conversations or other kinds
+         of communication. The document does not specify a situation in which one side of a
+         conversation records without the other side's knowledge or consent, thus the act of
+         recording one's conversations is in a legally gray area. All of the above articles
+         explicitly note that no criminal liability can be incurred if the gathered information is
+         submitted to investigative authorities.
+
+         Constitution:
+           https://matsne.gov.ge/en/document/view/30346?publication=35
+
+         Criminal Code of Georgia:
+           https://matsne.gov.ge/en/document/view/16426?publication=187&scroll=62067
+    -->
+    <country iso="ge" allowed="true" />
+
+    <!-- Enable recording for Greece:
+         Section 2 of Article 370A of the Greek Penal Code bans it, subarticle 4 offers exceptions
+         when no other evidence is present. Decision 53/2010 of the Supreme Criminal Court limits
+         evidence submitting to third parties that found the recording 'by accident'. Decision
+         277/2014 of the Supreme Criminal Court acquitted a guilty party and deemed the presented
+         recordings admissable. Article 25 of the Penal Code states that, any action is not illegal
+         if it was done so to protect the property or safety of oneself or of another party,
+         provided that the crime of sharing the recording is a lesser one in comparison.
+
+         Legal discussion:
+           https://uk.practicallaw.thomsonreuters.com/w-010-1738
+    -->
+    <country iso="gr" allowed="true" />
+
+    <!-- Enable recording for Croatia:
+         Article 143 of the Croatian Criminal Code, Paragraph 1 notes that the recording of another
+         person's privately uttered words is a criminal offense, when said words are not 'intended
+         for his or her attention' and could lead to imprisonment not exceeding three years.
+         Paragraph 2, which holds the same punishment, indicated that situations in which the
+         recording, its transcription or the 'gist' of said recording being shared as an equal
+         crime. Paragraph 4 states that there is no criminal offence if said acts are committed in
+         'the public interest or another interest prevailing over the interest to protect the
+         privacy of the person being recorded or eavesdropped on'. Prosecution is made per request
+         and the state does not initiate it, which renders the matter to the level of a civil case
+         and not to that of a criminal case.
+
+         Croatian Criminal Code:
+           https://www.legislationline.org/documents/section/criminal-codes/country/37/Croatia/show
+    -->
+    <country iso="hr" allowed="true" />
+
+    <!-- Enable recording for Hungary:
+         Sections 413 and 418 define the Breach of Trade and/or Business Secrecy as a criminal
+         offense. There is no mention of wiretapping and/or eavesdropping as a criminal offense.
+         The Hungarian Data Protection and Freedom of Information Agency (DPA) created a Guidance in
+         2016, for cases concerning situations which include an individual as one side of the
+         conversation, and a data processing entity as the other side. This guidance should not be
+         considered relevant, as it does not deal with the communications of individuals. No
+         pertinent articles or paragraphs were found in the Hungarian Criminal Code, which in effect
+         equates to there being no punishment for the recording of personal calls.
+
+         DPA 2016 Guidance:
+           https://www.naih.hu/files/2016_05_09_tajekoztato_hangfelvetelekrol.pdf
+
+         Hungarian Criminal Code:
+           https://www.legislationline.org/documents/section/criminal-codes/country/25/Hungary/show
+    -->
+    <country iso="hu" allowed="true" />
+
+    <!-- Disable recording for Indonesia:
+         Based on Article 26 of both Law Number 11 of 2008 and its revision, Law Number 19 of 2016,
+         call recording is defined on its own and requires one to obtain consent from the other
+         party when recording calls, although it can be used as a form of evidence. Whether
+         recording another person without his or her consent is a criminal offense that is
+         prosecuted by the country itself is not clear and further information should be gathered by
+         a native speaker.
+
+         Law 11 of 2008 (first file):
+           https://www.hukumonline.com/pusatdata/detail/27912/nprt/1011/uu-no-11-tahun-2008-informasi-dan-transaksi-elektronik
+
+         Law 19 of 2016:
+           https://jdih.kominfo.go.id/produk_hukum/view/id/555/t/undangundang+nomor+19+tahun+2016+tanggal+25+november+2016
+    -->
+    <country iso="id" allowed="false" />
+
+    <!-- Enable recording for Ireland:
+         The Irish Constitution does not specifically state a right to privacy. Subsection (6) of
+         section 98  of the Interception of Postal Packets and Telecommunications Messages
+         (Regulation) Act of 1993 defines interception of a call in such a way, that deems the
+         recording of a call by one party to the call legal. Whether said call recording can be used
+         as evidence or infringes upon a person's privacy is a complicated matter that can only be
+         decided on a case-by-case basis. Subsection (2) of section 98 goes on to elabore on cases
+         in which call recordings are legal, such as in the interests of the security of the State
+         (c), for the prevention or detection of crime or for the purpose of any criminal
+         proceedings (b) and others.
+
+         Telecommunications Messages Act of 1993: (nonencrypted link)
+           http://www.irishstatutebook.ie/eli/1993/act/10/enacted/en/print.html
+
+         Legal discussions:
+           https://www.mhc.ie/latest/insights/big-brother-is-watching-but-is-he-listening-too
+           https://www.irishtimes.com/news/crime-and-law/q-a-what-are-the-legal-implications-1.1740070
+    -->
+    <country iso="ie" allowed="true" />
+
+    <!-- Enable recording for Israel and Palestine:
+         Israeli law specifies that call recording is illegal and punishable when neither party in
+         said conversation is aware of said act of recording. Either party in a conversation can
+         record his or her calls without being legally required to inform the other party.
+         Due to legal ambiguity, it is currently impossible to determine which set of laws should be
+         taken under consideration when recording personal calls within the Palestinian territories.
+         This is relevant as the Occupied Palestinian Territory makes use of the Mobile Country Code
+         registered to Israel. Palestine's ISO is set as disabled, since if it is in use there is no
+         legal way to determine which set of laws are being used, due to the differing laws used
+         in parts of it.
+
+         The Wiretapping Law, 5739-1979:
+           https://www.nevo.co.il/law_html/law01/077_001.htm
+
+         Information on State of Palestine:
+           https://en.wikipedia.org/wiki/Palestinian_law#Statutes_and_legislation
+
+         News articles:
+           https://www.globes.co.il/news/article.aspx?did=1001066185
+           https://www.ynet.co.il/articles/0,7340,L-3043583,00.html
+    -->
+    <country iso="il" allowed="true" />
+    <country iso="ps" allowed="false" />
+
+    <!--Enable recording for India:
+         No clear definition exists on the matter of call recordings being made by one side.
+         Depending on whether said call recording was made and/or published with malicious intent,
+         it may or may not be admissible in court, and/or punishable by law. There are a number of
+         precedents and legal definitions, which are available below.
+
+         Legal discussion:
+           https://copyright.lawmatters.in/2012/02/recording-telephonic-conversations.html
+    -->
+    <country iso="in" allowed="true" />
+
+    <!-- Disable recording for Iceland:
+         According to the Electronic Communications Act, No. 81, recording one's own telephone
+         conversations without notifying the other party can make the recording party liable to
+         fines or imprisonment of up to six months in the case of serious or repeated violations, as
+         explicitly stated in Article 74. Article 48 covers the Recording of telephone calls and
+         states that the party to a telephone conversation that wishes to record said conversation
+         shall, when it commences, notify the opposite party of his or her intent to do so. This is
+         not required when the opposite party can clearly be assumed to be aware of the recording.
+
+         Electronic Communications Act:
+           https://www.government.is/Publications/Legislation/Lex/?newsid=86c9a6a9-fab5-11e7-9423-005056bc4d74
+    -->
+    <country iso="is" allowed="false" />
+
+    <!-- Enable recording for Iran:
+         Based on Article 25 of the Iranian Constitution, recording one's own calls, as a private
+         citizen for archival reasons, is not illegal. The sharing of said recordings with a third
+         party is forbidden based on the aforementioned legal document. According to Article 730 of
+         the Iranian Cybercrime Law, wiretapping a call which can be defined as non-public is a
+         crime and may lead to a punishment in the form of imprisonment for a period of six months
+         to two years, or a fine of ten to forty million rials, or both. There is currently no
+         punishment for the act of recording one's own calls in the Iranian Penal Code, thus the act
+         itself is not criminally punishable. Sharing said recordings in a way that causes injury to
+         the other party might be criminally punishable. Caution is advised, due to the geopolitical
+         situation surrounding the Islamic Republic of Iran.
+
+         Constitution:
+           https://www.wipo.int/edocs/lexdocs/laws/en/ir/ir001en.pdf
+         Penal Code of 2013 (in Persian):
+           https://www.refworld.org/cgi-bin/texis/vtx/rwmain/opendocpdf.pdf?reldoc=y&docid=5447c9274
+         Cybercrime Law (in Persian):
+           https://www.cyberpolice.ir/page/42981
+         Legal article (in Persian):
+           https://www.irna.ir/news/83268974/%D8%B6%D8%A8%D8%B7-%D9%85%D9%83%D8%A7%D9%84%D9%85%D9%87-%D8%AA%D9%84%D9%81%D9%86%DB%8C-%D8%AC%D8%B1%D9%85-%D9%86%DB%8C%D8%B3%D8%AA
+    -->
+    <country iso="ir" allowed="true" />
+
+    <!-- Enable recording for Italy and Vatican City State:
+         It is not illegal to record a conversation, as parties to calls automatically accept the
+         risk that a call may be recorded. Making a recording available to other parties is a
+         criminal offense, when done so for reasons other than protecting either one's own rights or
+         other parties' rights. Articles 23 and 167, in the Privacy Code, deem that the crimes
+         provided for therein are punishable only if said acts result in harm. According to the
+         Supreme Court of Cassation, recorded conversations are legal and can be used as evidence in
+         court, even if the other party is unaware of being recorded, provided that it is not
+         recorded by a third party. The Italian comune of Campione d'Italia features an exclave,
+         situated within the Swiss canton of Ticino, and as such it may not be within the confines
+         of the Italian ISO and its inherent laws.
+
+         Legal articles:
+           https://www.altalex.com/index.php?idnot=53369
+           https://web.archive.org/web/20161011100301/http://notizie.tiscali.it/socialnews/articoli/polimeni/13230/registrare-di-nascosto-per-la-cassazione-e-legale/
+    -->
+    <country iso="it,va" allowed="true" />
+
+    <!-- Enable recording for Japan:
+         Recording one's own calls is neither a criminal offense, nor illegal. Wiretapping and
+         leaking information gained from a recording is illegal and may be criminally punishable.
+         Recording as a third party is a criminal offense, when done so without the consent of at
+         least one party to the conversation. Recordings obtained without consent from both sides
+         will not be admitted as evidence in a criminal case, but are admitted as such in most civil
+         cases, unless it was obtained in a method, which the court deems as unacceptable. If the
+         recording infringes one's personal rights or discloses trade secrets, sharing said
+         recording might lead to civil cases. In work-related instances, one may record and divulge
+         information under the protection of the Whistleblower Protection Act of 2004. The Supreme
+         Court of Japan's Decision of the 12th of July 2000, case number 1999 (A) 96, was in favor
+         of admitting a tape recording as evidence, which was made by one party to a conversation,
+         without the other party's consent.
+
+         Whistleblower Protection Act: (nonencrypted link)
+           http://drasuszodis.lt/userfiles/Japan%20Whistleblower%20Protection%20Act.pdf
+
+         Decision of the Supreme Court of Japan: (nonencrypted link)
+           http://www.courts.go.jp/app/hanrei_en/detail?id=494
+
+         Legal articles: (nonencrypted link)
+           https://www.moneypost.jp/292939
+           https://president.jp/articles/-/15666
+           https://www.hrpro.co.jp/trend_news.php?news_no=636
+           https://kumaben.com/recording-audio-without-consent/
+           https://www.mot-net.com/blog/efficiency-of-operations/6737
+           https://milight-partners-law.hatenablog.com/entry/2015/08/31/152333
+
+         Legal discussion:
+           https://blogs.yahoo.co.jp/unyieldingspirit2007/24529523.html
+    -->
+    <country iso="jp" allowed="true" />
+
+    <!-- Enable recording for South Korea:
+         According to Article 3(1) of the Protection of Communications Secrets Act, it is forbidden
+         to wiretap, record or listen to any conversation between other parties. Article 4 defines
+         recordings obtained by way of illegal recording or wiretapping as inadmissible, hence they
+         can not be used as evidence in a trial or disciplinary procedure. Article 14 goes on to
+         specify that no person shall record a conversation between other parties, that is not
+         public, or listen to said parties' conversation through the use of electronic or mechanical
+         devices. Definitions of recording, wiretapping and other such terms may be found in Article
+         2. The Protection of Communications Secrets Act clearly defines that recording is not legal
+         when done by a third party, but does not specifically discuss whether whether both parties
+         to a conversation need to agree to a recording. Since there is no penalty listed, recording
+         one's own conversations should be in, at worst, a gray area that should still not make the
+         act punishable. Similarly, whether recordings made without consent can be used as evidence
+         is legally unclear.
+
+         Protection of Communications Secrets Act:
+           https://elaw.klri.re.kr/kor_service/lawView.do?hseq=31731&lang=ENG
+    -->
+    <country iso="kr" allowed="true" />
+
+    <!-- Enable recording for Liechtenstein:
+         Recording a call between an organization and an individual is illegal, when done without
+         notification and/or consent. Recording a call between individuals is illegal and punishable
+         when transmitting said recording or information to a third party, and/or when the person
+         that initiates the recording is not part of the conversation. This means that recording a
+         call when you are one of the two parties is legal, even without notifying the other party.
+         Legal action must be initiated by the aggrieved party. The following is defined in Article
+         120 of the Criminal Code of 24 June 1987 (StGB), points 1, 2, 2a and 3. Article 100 of the
+         Constitution may be pertinent to use of call recordings as evidence.
+
+         Criminal Code of 24 June 1987 (StGB):
+           https://www.regierung.li/media/medienarchiv/311_0_11_07_2017_en.pdf.
+
+         Constitution:
+           https://www.regierung.li/media/medienarchiv/101_01_01_2012_en.pdf?t=2.
+    -->
+    <country iso="li" allowed="true" />
+
+    <!-- Enable recording for Sri Lanka:
+         Part IV/59 of the Sri Lankan Telecommunications Act defines the penalty for eavesdropping
+         on a call. The Sri Lankan Penal Code does not cover the act of recording one's own calls,
+         hence the act is not criminally punishable.
+
+         Telecommunications Act:
+           https://www.lawnet.gov.lk/1947/12/31/sri-lanka-telecommunications-2/
+
+         Penal Code:
+           https://www.lawnet.gov.lk/penal-code-consolidated-2/
+
+         Article: (nonencrypted link)
+           http://www.dailymirror.lk/article/PTL-tampered-with-phone-recording-system-ASG-135574.html
+    -->
+    <country iso="lk" allowed="true" />
+
+    <!-- Enable recording for Lithuania:
+         Article 166 of the Lithuanian Criminal Code defines that violations of a person's
+         correspondence, by unlawfully wiretapping a person's conversations as a criminal offense,
+         which could lead to a term of imprisonment of up to two years, a fine or community service.
+         The wording of said article is unclear and only mentions electronic communication networks
+         and recording and/or wiretapping as a third party, and not as one of the two parties.
+         Article 61 of the Law on Electronic Communications defines confidentiality of
+         communications, as far as situations like those covered by GDPR, as in the handling of
+         information between individuals and legal entities, and should therefore not be taken into
+         account.
+
+         Lithuanian Criminal Code:
+           https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=q8i88l10w&documentId=a84fa232877611e5bca4ce385a9b7048&category=TAD
+
+         Lithuanian Law on Electronic Communications:
+            https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=-wd7z7kkgy&documentId=05cd4e020f0a11e7b6c9f69dc4ecf19f&category=TAD
+    -->
+    <country iso="lt" allowed="true" />
+
+    <!-- Enable recording for Luxembourg:
+         The Luxembourgish Penal Code does not specifically cover the right to privacy and its
+         infringement. Based on this, it is not a criminal offense to record one's personal calls,
+         although doing so in a public manner may lead to a civil case from the aggrieved party. One
+         should consult further with a lawyer whether sharing said recording or recordings would
+         constitute a criminal offense.
+
+         Penal Code: (nonencrypted link)
+           http://legilux.public.lu/eli/etat/leg/code/penal/20181101
+    -->
+    <country iso="lu" allowed="true" />
+
+    <!-- Enable recording for Latvia
+         There is no clear definition of call recording by itself within the Criminal Law of Latvia.
+         Article 144 of said law covers breach of information secrecy, when said information is in
+         the form of correspondence or data relayed by way of electronic communications networks.
+         Paragraph (1) defines the punishment for violating the secret of a person's correspondence
+         as a term of imprisonment for up to two years, or a fine, or others. In a 2014
+         e-Consultation, the Deputy Head of the Public Relations Department of the State Police,
+         Tom Sadovsky, defined the recording of calls with the intent to use as evidence as legal.
+         The Personal Data Protection Law does not apply, as it considers the communication between
+         individuals and legal entitites.
+
+         Latvian Criminal Law:
+           https://likumi.lv/doc.php?id=88966
+
+         Latvian Personal Data Protection Law:
+           https://likumi.lv/doc.php?id=4042
+
+         Legal consultation:
+           https://lvportals.lv/e-konsultacijas/4460-sarunas-drikst-ierakstit-2014.
+    -->
+    <country iso="lv" allowed="true" />
+
+    <!-- Enable recording for Morocco:
+         Call recording is not punishable as one side of a two-party conversation. Recordings are
+         not admissible in court, if the other party is not aware of the recording. Article 447 of
+         the Criminal Law of Morocco, states that the premeditated and unconsented publication of
+         video and/or audio files is a punishable offense.
+
+         Personal Data Law 09-08:
+           https://www.afapdp.org/wp-content/uploads/2018/05/Maroc-Loi-09-08-relative-a-la-protection-des-personnes-physiques-a-legard-du-traitement-des-DCP-2009.pdf
+
+         Moroccan Criminal Law:
+           https://www.h24info.ma/maroc/la-loi-sur-la-protection-des-donnees-personnelles-entre-en-vigueur-le-13-septembre/
+    -->
+    <country iso="ma" allowed="true" />
+
+    <!-- Disable recording for Monaco:
+         According to the Penal Code of Monaco, Article 308-2, a person may be punished with a
+         prison sentence of six months to three years, as well as a fine, for infinging or
+         attempting to infringe on a person's rights to privacy. This includes wiretapping,
+         recording or transmitting the words spoken by a person in a private place. Consent will be
+         presumed when such an action is done during a meeting, with the knowledge of the person
+         that is being recorded. Article 344 of the Penal Code mentions the same punishment for
+         purposeful wiretapping.
+
+         Penal Code:
+           https://www.legimonaco.mc/305/legismclois.nsf/ViewCode!OpenView&Start=1&Count=300&RestrictToCategory=CODE%20P%C3%89NAL
+    -->
+    <country iso="mc" allowed="false" />
+
+    <!-- Enable recording for Moldova:
+         Article 30 of the Constitution of Moldova ensures the privacy of correspondence. No
+         specific law has been enacted that defines recording calls, as an individual, as a criminal
+         offense. There are laws which define this for legal entities and for the government. Please
+         read the attached legal discussion for further information on the subject.
+
+         Constitution of Moldova:
+           https://www.presedinte.md/eng/constitution
+
+         Code of Criminal Procedure:
+           https://www.seepag.info/download/rep_moldova/Criminal%20Procedure%20Code%20RM.pdf
+
+         Regulations for legal entities:
+           https://www.anrceti.md/files/filefield/hca%20nr.48%20din%2010.09.2013%20regulam%20priv%20serv%20CE.pdf
+
+         Legal discussion:
+           https://jsa.md/2017/02/06/inregistrarea-convorbirilor-telefonice-cit-de-legala-este/
+    -->
+    <country iso="md" allowed="true" />
+
+    <!-- Enable recording for Montenegro:
+         Article 173 of the Criminal Code of Montenegro marks call recording as legal if the content
+         of the conversation was 'intended for your use'. It is also legal when it concerns the
+         prevention of crimes, which carry a sentence of 5 years minimum. Sharing a conversation to
+         a third party is a criminal offense.
+
+         Criminal Code of Montenegro:
+           https://www.pravda.gov.me/ResourceManager/FileDownload.aspx?rid=256001&rType=2&file=Krivi%C4%8Dni%20zakonik%20Crne%20Gore.pdf
+    -->
+    <country iso="me" allowed="true" />
+
+    <!-- Enable recording for North Macedonia:
+         As stated in the Macedonian Penal Code, if the recording is made available to a third party
+         or is created and/or distributed with a malicious intent, then the other party can sue you.
+         The state does not prosecute in such cases, unless the act is done by an official state
+         representative of any kind, as mentioned in 151.4 and 151.5.
+
+         North Macedonian Penal Code:
+           https://www.wipo.int/edocs/lexdocs/laws/mk/mk/mk018mk.pdf
+    -->
+    <country iso="mk" allowed="true" />
+
+    <!-- Enable recording for Malta:
+         Relevant laws and/or legal precedents:
+         Article 34 (1)(f) of the Maltese Constitution states that a person may be deprived of his
+         rights in the case of there being suspicion of said person having commited, or being in the
+         process of committing a crime. Effectively this means that recording your own calls is
+         legal when done so to report a crime. There is no mention of the act of recording one's own
+         calls in the Maltese Criminal Code, which means that even if it were to be illegal, it is
+         not a criminal offense. The Media and Defamation Act of 2018 handles all cases of
+         defamation, which may or may not include the act of publishing one's call recordings
+         without the knowledge or consent of the other concerned party.
+
+         Constitution: (nonencrypted link)
+           http://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8566&l=1
+
+         Criminal Code: (nonencrypted link)
+           https://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8574&l=1
+
+         Media and Defamation Act: (nonencrypted link)
+           http://justiceservices.gov.mt/DownloadDocument.aspx?app=lp&itemid=29045&l=1
+    -->
+    <country iso="mt" allowed="true" />
+
+    <!-- Enable recording for Netherlands, Bonaire, Sint Eustatius, Saba, Sint Maarten, Curaçao,
+                              Aruba:
+         Recording one's own conversations without the consent of the other party or parties is not
+         in itself punishable by law. Sharing recordings made without consent is punishable in the
+         form of a libel case. This in effect means that the government shall not prosecute anyone
+         for the recording of calls. Call recordings may be used as evidence in criminal and civil
+         cases.
+
+         Legal discussion:
+           https://blog.wetrecht.nl/telefoongesprekken-opnemen-als-bewijs-kan-dat
+    -->
+    <country iso="nl,bq,sx,cw,aw" allowed="true" />
+
+    <!-- Enable recording for Norway:
+         As a private citizen, one may record any call that they participate in. There is no
+         requirement to make other parties aware of the recording, but the use of said recording(s),
+         depending on the content, may be subject to various laws, such as data protection (privacy)
+         legislation, libel laws, laws governing trade and national secrets, non-disclosure
+         agreements and so on. It is, however, prohibited to record calls without the permission of
+         the other party or parties, if you are making the call on behalf of a company or
+         organization. All of the above is outlined in Article 205 of the Norwegian Penal Code.
+
+         Penal Code:
+           https://lovdata.no/dokument/NL/lov/2005-05-20-28/KAPITTEL_2-6#§205
+
+         Legal article:
+           https://www.datatilsynet.no/regelverk-og-verktoy/veiledere/lydopptak/
+    -->
+    <country iso="no" allowed="true" />
+
+    <!-- Enable recording for New Zealand:
+         According to the Crimes Act of 1961, Public Act 216B, Articles 1 and 2(a), anyone is liable
+         to imprisonment for a term not exceeding 2 years for intentionally intercepting any private
+         communication, unless he or she is a party to that private communication. Public Act 216C,
+         subsections (1) and (2) define the prohibition on disclosure of unlawfully intercepted
+         private communications. The recording of one's personal conversations and their publishing
+         or use as evidence without the other party's consent is not explicitly forbidden, nor is it
+         defined as a criminal offense.
+
+         Crimes Act of 1961, Part 9A, Crimes against personal privacy: (nonencrypted link)
+           http://www.legislation.govt.nz/act/public/1961/0043/latest/DLM327382.html#DLM329802
+    -->
+    <country iso="nz" allowed="true" />
+
+    <!-- Enable recording for Peru:
+         The Peruvian Constitution states that people own their own voice and images. If said images
+         or recordings are made for archival purposes, it is allowed. While wiretapping is illegal,
+         it has been used as legal evidence in a court of law. As long as one of the persons talking
+         agrees to the recording, said recording can be used in a court of law. There may be
+         exceptions if the communication contains information that may affect third parties, or if
+         it can be considered as information that should be blocked by medical or legal
+         confidentiality.
+
+         Wiretapping:
+           https://diariouno.pe/columna/chuponeo-prueba-prohibida-o-valida/
+
+         Legal article:
+           https://laley.pe/art/2679/una-grabacion-no-consentida-puede-ser-prueba-de-un-delito-
+
+         Legality of voice recordings and images:
+           https://commons.m.wikimedia.org/wiki/Special:MyLanguage/Commons:Country_specific_consent_requirements#Peru
+    -->
+    <country iso="pe" allowed="true" />
+
+    <!-- Enable recording for Poland:
+         Article 267 of the Polish Penal Code defines call recording as legal for private citizens,
+         when the recording is made by a party to the call.
+
+         Penal Code:
+           https://supertrans2014.files.wordpress.com/2014/06/the-criminal-code.pdf
+
+         Legal articles:
+           https://www.alfatronik.com.pl/info/nagrywanie-rozmow-legalne/
+           https://bezprawnik.pl/legalnosc-nagrywania-rozmowy/
+    -->
+    <country iso="pl" allowed="true" />
+
+    <!-- Disable recording for Puerto Rico:
+         Title Thirty-three of the Penal Code of 2004, Subtitle 5, Special Provisions, Part I:
+         Crimes Against the Person, Chapter 301: Crimes Against Civil Rights, Subchapter II: Crimes
+         Against the Right to Privacy, 33 L.P.R.A § 4809 defines the recording of a private personal
+         conversation, without the express authorization of all parties involved in it, as a
+         misdemeanor. This, in effect, means that recording a phone call as one of the two parties
+         is a criminal offense if done so without the explicit notification and consent of the other
+         party. This territory of the United States conforms with its State Laws. For further
+         information, check 'us'.
+
+         33 L.P.R.A. § 4809. Recording of communications by a participant:
+           https://bit.ly/2UqbrRC
+    -->
+    <country iso="pr" allowed="false" />
+
+    <!-- Disable recording for Portugal:
+         Privacy is a fundamental right in Portuguese law, as it is defined in Articles 26(1) and 34
+         of the Portuguese Constitution. Infringing on said rights constitutes a crime, as defined
+         in Articles 192(1), 194(2) and 199(1) of the Portuguese Penal Code. The punishment is
+         imprisonment for a period of up to one year or a fine equaling 240 days of pay, either of
+         which may be increased by a third, based on Article 197. Lower courts and Higher courts
+         have been ruling both for and against recording one's own calls, no matter the reason, and
+         there have been numerous cases of exceptions being made, despite what the law says. A
+         complaint has been lodged with the European Court of Human Rights, which may lead to a
+         reversal in the current laws and prohibitions. It is noteworthy that in one case, the
+         Supreme Court rendered a decision, which can be translated as such: "The protection of
+         speech that embodies criminal practices or the image that portrays them must yield to the
+         interest of protecting the victim and the efficiency of criminal justice: protection ends
+         when what is protected is a crime."
+
+         Constitution:
+           https://www.parlamento.pt/Legislacao/Paginas/ConstituicaoRepublicaPortuguesa.aspx
+
+         Penal Code: (nonencrypted link)
+           http://www.pgdlisboa.pt/leis/lei_mostra_articulado.php?artigo_id=109A0199&nid=109&tabela=leis&pagina=1&ficha=1&nversao=
+
+         Examples of privacy as a fundamental right: (nonencrypted link)
+           http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ab509203321d898d802579ea00576d95?OpenDocument
+           http://www.dgsi.pt/jtre.nsf/134973db04f39bf2802579bf005f080b/be3732dc1664576d8025836100514c19
+
+         Examples of exceptions: (nonencrypted links)
+           https://portal.oa.pt/comunicacao/imprensa/2017/11/12/tribunais-aprovam-videos-de-telemovel-apesar-da-legislacao/
+           http://www.dgsi.pt/jtrp.nsf/-/CC3190F093E769FC80257F69004D9E7B
+           http://www.dgsi.pt/jtrl.nsf/0/44ed8c6ca2d940d580256f250052bfd8
+
+         Complaint to ECHR:
+           https://hudoc.echr.coe.int/eng#{%22itemid%22:[%22001-184193%22]}
+
+         Quoted Supreme Court Case: (nonencrypted link)
+           http://www.dgsi.pt/jstj.nsf/954f0ce6ad9dd8b980256b5f003fa814/25cd7aa80cc3adb0802579260032dd4a?OpenDocument
+
+         Legal alternatives: (nonencrypted links)
+           http://www.dgsi.pt/jtrc.nsf/c3fb530030ea1c61802568d9005cd5bb/c5bb36d9a0470bdd80257b400048f9f2?OpenDocument
+           http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ff947b8a3fda778780257c0000478b5a
+    -->
+    <country iso="pt" allowed="false" />
+
+    <!-- Enable recording for Romania:
+         The Telecommunications Act (506/2004) states that the recording of a conversation by a
+         party to that conversation is permitted and not a criminal offense. Nevertheless, while
+         such recordings are legal, making use of them may fall subject to further civil or criminal
+         laws. Admissibility as evidence depends on how the recording was obtained.
+
+         Telecommunications Act: (nonencrypted link)
+           http://legislatie.just.ro/Public/DetaliiDocument/56973#id_artA88_ttl
+
+         Civil Procedure Code:
+           https://www.dreptonline.ro/legislatie/codul_procedura_civila_consolidat.php
+
+         Criminal Procedure Code:
+           https://www.dreptonline.ro/legislatie/codul_procedura_penala_2007.php
+
+         Legal article:
+           https://www.dsclex.ro/coduri/cciv2.htm
+    -->
+    <country iso="ro" allowed="true" />
+
+    <!-- Enable recording for Serbia:
+         Article 143 of the Serbian Penal Code covers unauthorized wiretapping and recordings. While
+         it is criminally punishable to share call recordings or wiretap them, the law specifically
+         states recording is only punishable when said recording is 'not meant for him/her', hence
+         it is legal to record your own calls, but not to share them with third parties.
+
+         Serbian Penal Code:
+           https://www.paragraf.rs/propisi/krivicni_zakonik.html
+    -->
+    <country iso="rs" allowed="true" />
+
+    <!-- Enable recording for Russia:
+         Recording a phone call when not one of the two parties participating in said call is
+         illegal and punishable by law. As a party to a phone call, one may record it without
+         notifying the other side, as is evident in the decision of the Supreme Court of Russia for
+         case 35-KG16-18, which was rendered on the 6th of December 2016. This concerns civil cases
+         between two private citizens. Whether this covers cases involving legal entities or people
+         holding a public position has not been researched. The key laws to consider are the Federal
+         Law of 27th July 2006, N 149-FZ, (amended on 18th March 2019) "Information, Information
+         Technologies and Information Security" - Article 9, Subarticle 8, as well as the Civil
+         Procedure Code of 2002, N 138-FZ (amended on the 27th December 2018), article 55. Citations
+         of other pertinent laws may be found in the linked decision, starting at internal document
+         page number 4.
+
+         Supreme Court of Russia, Decision on case number 35-KG16-18: (nonencrypted link)
+           http://www.supcourt.ru/stor_pdf.php?id=1502686
+
+         Federal Law of 27th July 2006, N 149-FZ:
+           https://www.consultant.ru/document/cons_doc_LAW_61798/35f4fb38534799919febebd589466c9838f571b2/
+
+         Civil Procedure Code of 2002, N 138-FZ:
+           https://www.consultant.ru/document/cons_doc_LAW_39570/b48406042a309ee368f395fb6f3be1d43c7cbfc2/
+    -->
+    <country iso="ru" allowed="true" />
+
+    <!-- Enable recording for Sweden:
+         According to the Swedish Penal Code (Brottsbalken), Chapter 4, 8–9 §§, it is illegal to
+         make unauthorized recordings of telephone conversations as a third party. A court can grant
+         permission for law enforcement agencies to tap telephone lines. Anyone participating in the
+         telephone call may record the conversation. A recording is always admissible as evidence in
+         a court of law, even when obtained in an illegal way.
+
+         Criminal Code: (nonencrypted link)
+           https://lagen.nu/begrepp/Olovlig_avlyssning
+           http://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/brottsbalk-1962700_sfs-1962-700
+    -->
+    <country iso="se" allowed="true" />
+
+    <!-- Enable recording for Singapore:
+         Singaporean law does not recognize privacy as a right that can be infringed upon. A party
+         can not be prosecuted or sued for recording a conversation he or she is a part of. The only
+         exception is when said recording contains confidential information, in which case the party
+         may or may not be liable for their actions, if said party makes use of said confidential
+         information in a way that clearly brings him or her gains of any sort, and/or harms the
+         other party in any perceivable way.
+
+         Legal discussion:
+           https://singaporelegaladvice.com/can-i-record-a-conversation-without-consent/
+
+         Personal Data Protection Act 2012:
+           https://sso.agc.gov.sg/Act/PDPA2012
+           https://www.pdpc.gov.sg/Legislation-and-Guidelines/Personal-Data-Protection-Act-Overview
+
+         Copyright Act, Revised Edition 2006:
+           https://sso.agc.gov.sg/Act/CA1987
+    -->
+    <country iso="sg" allowed="true" />
+
+    <!-- Enable recording for Slovenia:
+         Article 148 of the Slovenian Criminal Code covers the unlawful eavesdropping and sound
+         recording. Subarticle 1 defines a maximum punishment of no more than one year for the
+         unlawful eavesdropping or recording of a private conversation by use of special devices,
+         or directly transmitting said conversation to a third person. This also includes passing on
+         the gist of said conversation. Subarticle 2 states that recording another person's
+         statement with the intent to misuse it, without his or her consent, is punishable in the
+         way postulated in Subarticle 1. Prosecution is initiated by the aggrieved party for
+         Subarticle 1, while under Subarticle 2 it is initiated upon a private action. Based on
+         Article 148, it is legal to record one's own calls, when not done so with the intent to
+         misuse said recordings. Sharing said recordings in any way may be deemed a criminal or
+         civil offense.
+
+         Criminal Code:
+           https://www.wipo.int/edocs/lexdocs/laws/en/si/si046en.pdf
+    -->
+    <country iso="si" allowed="true" />
+
+    <!-- Enable recording for Slovakia:
+         Sections 376 and 377 of the Slovakian Criminal Code cover breach of confidentiality of
+         spoken utterance and other forms of personal expression and the breach of secrecy of all
+         types of instruments, recordings and documents. Section 376 states that the breach of
+         secrecy, by way of disclosing or making available to a third party and/or using it to cause
+         serious harm to another party, leads to an imprisonment of up to two years. Section 377
+         defines breach of confidentiality as the making of an unlawful recording accessible to a
+         third person or using it in any way that would hinder the other side's rights. This is
+         punishable with a term of imprisonment of up to two years.
+
+         Slovakian Criminal Code:
+           https://www.legislationline.org/documents/section/criminal-codes/country/4/Slovakia/show
+    -->
+    <country iso="sk" allowed="true" />
+
+    <!-- Enable recording for Turkey:
+         Article 132 of Law 5237, the Turkish Penal Code, sets the punishment for violating the
+         secrecy of communication as six months to two years of imprisonment. If the violation of
+         secrecy is done in the form of a recording, then the punishment is imprisonment of one to
+         three years. The act of unlawfully publishing the contents of a communication is
+         imprisonment of one to three years. Openly disclosing the content of a communication
+         between oneself and others, without the other party or parties' consent, is imprisonment of
+         six months to two years. If disclosure is done by way of the press or broadcast, the
+         punishment is increased by one half. One may listen and record conversations of other
+         parties, with the consent of at least one party. Doing so without consent is punishable
+         with imprisonment of two to six months. Article 135 of the Turkish Penal Code defines
+         punishment in the case of recording personal data and information. It stands to reason that
+         one may record one's own calls without the consent of the other party, but the act of
+         sharing those recordings in any way may result in persecution, either in the form of a
+         criminal or civil case against the party that has recorded his or hew own calls.
+
+         Turkish Penal Code:
+           https://www.wipo.int/edocs/lexdocs/laws/en/tr/tr171en.pdf
+    -->
+    <country iso="tr" allowed="true" />
+
+    <!-- Enable recording for Ukraine:
+         Sharing a call recording without consent is a punishable offense, and can not be used as
+         valid proof in a court of law. Call recordings may be handed over to the authorities, by
+         one of the two parties without the other party's consent, when a crime is mentioned in the
+         recording. In such cases the party that handed over the recording is not liable to fines or
+         punishment, as the authorities will use the recording to initiate an investigation, but not
+         as proof of a crime. The above information is covered in Articles 31 and 32 of the
+         Constitution, as well as Articles 163, 182 and 359 of the Criminal Code.
+
+         Constitution of Ukraine:
+           https://zakon.rada.gov.ua/cgi-bin/laws/main.cgi?nreg=254%EA%2F96%2D%E2%F0
+
+         Ukrainian Criminal Code:
+           https://www.legislationline.org/documents/section/criminal-codes/country/52
+
+         Radio Svoboda legal advice page:
+           https://www.radiosvoboda.org/a/details/28905674.html
+
+         Legal discussion:
+           https://sklaw.com.ua/ua/news/345_pro_naslidki_zapisu_telefonnoi_rozmovi_rozpovila_advokat_ao_spenser
+    -->
+    <country iso="ua" allowed="true" />
+
+    <!-- Disable recording for United States of America, Guam, the Northern Mariana Islands, the
+                               United States Virgin Islands, Puerto Rico:
+         Currently federal laws state that call recording is legal. On the other hand, each state
+         has its own laws which take priority. Most states allow call recording when one sides
+         agrees, but over 10 require both sides to agree. Since there are no ISO country codes per
+         state, there is no way to differentiate whether the state you are currently in allows call
+         recording or not. Due to this, call recording is set as disabled for the United States of
+         America and some of its territories. These include Guam, the Northern Mariana Islands, the
+         United States Virgin Islands and Puerto Rico. Until a method for properly differentiating
+         between states is created, or a law or precedent emerges which would allow call recording
+         to be legal in all of the USA and its territories, all aforementioned countries and
+         territories should be set as false. Even if such a method is to be found, there is still
+         the question of Native American Reservations, territories that have a cumulative size of
+         over 200 000 square kilometers and might enforce their own set of laws for call recording.
+
+         Two-party consent state laws:
+           https://recordinglaw.com/party-two-party-consent-states/
+    -->
+    <country iso="us,vi,gu" allowed="false" />
+
+    <!-- Enable recording for Kosovo:
+         Article 36 of the Constitution of Kosovo defines the Right to Privacy. Article 202 of the
+         Criminal Code of Kosovo covers the infringing of privacy in corresepondence and computer
+         databases, which can be best explained as the act of violating and/or sharing a private
+         document with another person. Article 203 covers the unauthorized disclosure of
+         confidential information, when the person disclosing said information is under legal duty
+         to maintain it as confidential. Said person is not liable when the disclosure of the
+         confidential information is done so in the interest of the public. Paragraph 4 describes
+         public interest as the welfare of the general public outweighing the individual interest.
+         It is also permissible to use this as a defense when the disclosed information involves
+         plans, preparation or the commission of crimes against the constitutional order or
+         territorial integrity of the Republic of Kosovo or other criminal offenses that will cause
+         great bodily injury or death to another person. Article 204 covers the unauthorized
+         interception of a conversation or statement. Article 205 covers the unauthorized
+         photographing or video recording of a person 'in his or her personal premises or in any
+         other place where a person has a reasonable expectation of privacy', with paragraph 4
+         offering an exception for liability when the act is done to discover a criminal offence or
+         the perpetrators of a criminal offence, or to present as evidence to the police,
+         prosecution or court, and if the photos or recordings are submitted to these authorities.
+         It is beyond doubt that none of these articles deal with recording personal conversation
+         between two parties, hence the act of doing so is not explicitly punishable and is not a
+         criminal offense that would warrant a criminal case.
+
+         Constitution of Kosovo:
+           https://kuvendikosoves.org/?cid=2,1058
+         Criminal Code of Kosovo:
+           https://assembly-kosova.org/common/docs/ligjet/Criminal%20Code.pdf
+    -->
+    <country iso="xk" allowed="true" />
+
+    <!-- Enable recording for South Africa:
+         Under the Regulation of Interception of Communications and Provision of
+         Communication-related Information Act of 2003, 4(1)(a)(b) as well as 16(5)(a)(b), it is
+         legal for a party of a conversation to record said conversation, when there are reasonable
+         grounds to believe that said act will prevent a crime, prevent bodily harm, is in the
+         interest of public safety or one of the other reasons stated in the previously noted
+         paragraphs.
+
+         Regulation of Interception of Communications Act of 2003:
+           https://www.gov.za/sites/default/files/gcis_document/201409/a70-02.pdf
+    -->
+    <country iso="za" allowed="true" />
+
+</call-record-allowed-flags>
diff --git a/java/com/android/dialer/callstats/AndroidManifest.xml b/java/com/android/dialer/callstats/AndroidManifest.xml
new file mode 100644
index 0000000..7f76f78
--- /dev/null
+++ b/java/com/android/dialer/callstats/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2018 The LineageOS 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.dialer">
+
+  <application>
+
+    <activity android:name=".callstats.CallStatsDetailActivity"
+      android:label="@string/call_stats_detail_title"
+      android:theme="@style/Dialer.ThemeBase.NoActionBar"
+      android:exported="true"
+      android:screenOrientation="portrait">
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+        <category android:name="android.intent.category.DEFAULT" />
+      </intent-filter>
+    </activity>
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/callstats/CallStatsAdapter.java b/java/com/android/dialer/callstats/CallStatsAdapter.java
new file mode 100644
index 0000000..1d673fc
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.dialer.R;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.contactinfo.NumberWithCountryIso;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.ExpirableCache;
+import com.android.dialer.util.PermissionsUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Adapter class to hold and handle call stat entries
+ */
+class CallStatsAdapter extends RecyclerView.Adapter {
+  private final Context mContext;
+  private final ContactInfoHelper mContactInfoHelper;
+  private final ContactInfoCache mContactInfoCache;
+  private final ContactDisplayPreferences mContactDisplayPreferences;
+
+  private ArrayList<CallStatsDetails> mAllItems;
+  private ArrayList<CallStatsDetails> mShownItems;
+  private CallStatsDetails mTotalItem;
+  private Map<CallStatsDetails, ContactInfo> mInfoLookup;
+
+  private int mType = -1;
+  private long mFilterFrom;
+  private long mFilterTo;
+  private boolean mSortByDuration;
+
+  /**
+   * Listener that is triggered to populate the context menu with actions to perform on the call's
+   * number, when the call log entry is long pressed.
+   */
+  private final View.OnCreateContextMenuListener mContextMenuListener = (menu, v, menuInfo) -> {
+    final CallStatsListItemViewHolder vh = (CallStatsListItemViewHolder) v.getTag();
+    if (TextUtils.isEmpty(vh.details.number)) {
+      return;
+    }
+
+    menu.setHeaderTitle(vh.details.number);
+
+    final MenuItem copyItem = menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard,
+        ContextMenu.NONE, R.string.action_copy_number_text);
+
+    copyItem.setOnMenuItemClickListener(item -> {
+      ClipboardUtils.copyText(CallStatsAdapter.this.mContext, null, vh.details.number, true);
+      return true;
+    });
+
+    // The edit number before call does not show up if any of the conditions apply:
+    // 1) Number cannot be called
+    // 2) Number is the voicemail number
+    // 3) Number is a SIP address
+
+    boolean canPlaceCallsTo = PhoneNumberHelper.canPlaceCallsTo(vh.details.number,
+        vh.details.numberPresentation);
+    if (!canPlaceCallsTo || PhoneNumberHelper.isSipNumber(vh.details.number)) {
+      return;
+    }
+
+    final MenuItem editItem = menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call,
+        ContextMenu.NONE, R.string.action_edit_number_before_call);
+
+    editItem.setOnMenuItemClickListener(item -> {
+      final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(vh.details.number));
+      DialerUtils.startActivityWithErrorToast(v.getContext(), intent);
+      return true;
+    });
+  };
+
+  private final Comparator<CallStatsDetails> mDurationComparator = (o1, o2) -> {
+    Long duration1 = o1.getRequestedDuration(mType);
+    Long duration2 = o2.getRequestedDuration(mType);
+    // sort descending
+    return duration2.compareTo(duration1);
+  };
+  private final Comparator<CallStatsDetails> mCountComparator = (o1, o2) -> {
+    Integer count1 = o1.getRequestedCount(mType);
+    Integer count2 = o2.getRequestedCount(mType);
+    // sort descending
+    return count2.compareTo(count1);
+  };
+
+  CallStatsAdapter(Context context, ContactDisplayPreferences prefs,
+      ExpirableCache<NumberWithCountryIso,ContactInfo> cache) {
+    mContext = context;
+    mContactDisplayPreferences = prefs;
+
+    final String currentCountryIso = GeoUtil.getCurrentCountryIso(mContext);
+    mContactInfoHelper = new ContactInfoHelper(mContext, currentCountryIso);
+
+    mAllItems = new ArrayList<CallStatsDetails>();
+    mShownItems = new ArrayList<CallStatsDetails>();
+    mTotalItem = new CallStatsDetails(null, 0, null, null, null, null, null, 0);
+    mInfoLookup = new ConcurrentHashMap<>();
+
+    mContactInfoCache = new ContactInfoCache(cache,
+        mContactInfoHelper, () -> notifyDataSetChanged());
+    if (!PermissionsUtil.hasContactsReadPermissions(context)) {
+      mContactInfoCache.disableRequestProcessing();
+    }
+  }
+
+  public void updateData(Map<ContactInfo, CallStatsDetails> calls, long from, long to) {
+    mInfoLookup.clear();
+    mFilterFrom = from;
+    mFilterTo = to;
+
+    mAllItems.clear();
+    mTotalItem.reset();
+
+    for (Map.Entry<ContactInfo, CallStatsDetails> entry : calls.entrySet()) {
+      final CallStatsDetails call = entry.getValue();
+      mAllItems.add(call);
+      mTotalItem.mergeWith(call);
+      mInfoLookup.put(call, entry.getKey());
+    }
+  }
+
+  public void updateDisplayedData(int type, boolean sortByDuration) {
+    mType = type;
+    mSortByDuration = sortByDuration;
+
+    mShownItems.clear();
+
+    for (CallStatsDetails call : mAllItems) {
+      if (sortByDuration && call.getRequestedDuration(type) > 0) {
+          mShownItems.add(call);
+      } else if (!sortByDuration && call.getRequestedCount(type) > 0) {
+          mShownItems.add(call);
+      }
+    }
+
+    Collections.sort(mShownItems, sortByDuration ? mDurationComparator : mCountComparator);
+    notifyDataSetChanged();
+  }
+
+  public void invalidateCache() {
+    mContactInfoCache.invalidate();
+  }
+
+  public void startCache() {
+    if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+      mContactInfoCache.start();
+    }
+  }
+
+  public void pauseCache() {
+    mContactInfoCache.stop();
+  }
+
+  @Override
+  public int getItemCount() {
+    return mShownItems.size();
+  }
+
+  @Override
+  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+    LayoutInflater inflater = LayoutInflater.from(mContext);
+    View view = inflater.inflate(R.layout.call_stats_list_item, parent, false);
+    CallStatsListItemViewHolder viewHolder = CallStatsListItemViewHolder.create(view,
+        mContactInfoHelper);
+
+    viewHolder.mPrimaryActionView.setOnCreateContextMenuListener(mContextMenuListener);
+    viewHolder.mPrimaryActionView.setTag(viewHolder);
+
+    return viewHolder;
+  }
+
+  @Override
+  public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+    CallStatsDetails details = mShownItems.get(position);
+    CallStatsDetails first = mShownItems.get(0);
+    CallStatsListItemViewHolder views = (CallStatsListItemViewHolder) viewHolder;
+
+    if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
+        && !details.isVoicemailNumber) {
+      ContactInfo info = mContactInfoCache.getValue(details.number + details.postDialDigits,
+          details.countryIso, mInfoLookup.get(details), false);
+      if (info != null) {
+        details.updateFromInfo(info);
+      }
+    }
+    views.setDetails(details, first, mTotalItem, mType,
+        mSortByDuration, mContactDisplayPreferences.getDisplayOrder());
+    views.clickIntent = getItemClickIntent(details);
+  }
+
+  private Intent getItemClickIntent(CallStatsDetails details) {
+    Intent intent = new Intent(mContext, CallStatsDetailActivity.class);
+    intent.putExtra(CallStatsDetailActivity.EXTRA_DETAILS, details);
+    intent.putExtra(CallStatsDetailActivity.EXTRA_TOTAL, mTotalItem);
+    intent.putExtra(CallStatsDetailActivity.EXTRA_FROM, mFilterFrom);
+    intent.putExtra(CallStatsDetailActivity.EXTRA_TO, mFilterTo);
+    return intent;
+  }
+
+  public String getTotalCallCountString() {
+    return CallStatsListItemViewHolder.getCallCountString(
+        mContext, mTotalItem.getRequestedCount(mType));
+  }
+
+  public String getFullDurationString(boolean withSeconds) {
+    final long duration = mTotalItem.getRequestedDuration(mType);
+    return CallStatsListItemViewHolder.getDurationString(
+        mContext, duration, withSeconds);
+  }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsDetailActivity.java b/java/com/android/dialer/callstats/CallStatsDetailActivity.java
new file mode 100644
index 0000000..d25e24b
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsDetailActivity.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.app.AccountSelectionActivity;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contacts.ContactsComponent;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.widget.LinearColorBar;
+
+/**
+ * Activity to display detailed information about a callstat item
+ */
+public class CallStatsDetailActivity extends AppCompatActivity implements
+    View.OnClickListener, View.OnLongClickListener {
+  private static final String TAG = "CallStatsDetailActivity";
+
+  public static final String EXTRA_DETAILS = "details";
+  public static final String EXTRA_TOTAL = "total";
+  public static final String EXTRA_FROM = "from";
+  public static final String EXTRA_TO = "to";
+
+  private ContactInfoHelper mContactInfoHelper;
+  private ContactDisplayPreferences mContactDisplayPreferences;
+  private Resources mResources;
+
+  private QuickContactBadge mQuickContactBadge;
+  private TextView mCallerName;
+  private TextView mCallerNumber;
+  private View mCallButton;
+  private View mSeparator;
+  private View mCopyButton;
+  private View mEditNumberButton;
+
+  private TextView mTotalDuration, mTotalCount;
+  private TextView mTotalTotalDuration, mTotalTotalCount;
+
+  private DetailLine mInDuration, mOutDuration;
+  private DetailLine mInCount, mOutCount;
+  private DetailLine mMissedCount, mBlockedCount;
+  private DetailLine mInAverage, mOutAverage;
+
+  private LinearColorBar mDurationBar, mCountBar;
+  private LinearColorBar mTotalDurationBar, mTotalCountBar;
+
+  private CallStatsDetails mData;
+  private CallStatsDetails mTotalData;
+  private String mNumber = null;
+
+  private class UpdateContactTask extends AsyncTask<String, Void, ContactInfo> {
+    @Override
+    protected ContactInfo doInBackground(String... strings) {
+      return mContactInfoHelper.lookupNumber(strings[0], strings[1]);
+    }
+
+    @Override
+    protected void onPostExecute(ContactInfo info) {
+      if (info != null) {
+        mData.updateFromInfo(info);
+        updateData();
+      }
+    }
+  }
+
+  @Override
+  protected void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    setContentView(R.layout.call_stats_detail);
+
+    Toolbar toolbar = findViewById(R.id.toolbar);
+    toolbar.setNavigationOnClickListener(v -> finish());
+    toolbar.setTitle(R.string.call_stats_detail_title);
+
+    mResources = getResources();
+    mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
+    mContactDisplayPreferences = ContactsComponent.get(this).contactDisplayPreferences();
+
+    mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
+    mQuickContactBadge.setOverlay(null);
+    mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+    mCallerName = (TextView) findViewById(R.id.caller_name);
+    mCallerNumber = (TextView) findViewById(R.id.caller_number);
+
+    mCallButton = findViewById(R.id.call_back_button);
+    mCallButton.setOnClickListener(this);
+    mCallButton.setOnLongClickListener(this);
+
+    mSeparator = findViewById(R.id.separator);
+    mEditNumberButton = findViewById(R.id.call_detail_action_edit_before_call);
+    mEditNumberButton.setOnClickListener(this);
+    mCopyButton = findViewById(R.id.call_detail_action_copy);
+    mCopyButton.setOnClickListener(this);
+
+    mDurationBar = (LinearColorBar) findViewById(R.id.duration_number_percent_bar);
+    mTotalDurationBar = (LinearColorBar) findViewById(R.id.duration_total_percent_bar);
+    mTotalDuration = (TextView) findViewById(R.id.total_duration_number);
+    mTotalTotalDuration = (TextView) findViewById(R.id.total_duration_total);
+    mInDuration = new DetailLine(R.id.in_duration,
+        R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+    mOutDuration = new DetailLine(R.id.out_duration,
+        R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+
+    mCountBar = (LinearColorBar) findViewById(R.id.count_number_percent_bar);
+    mTotalCountBar = (LinearColorBar) findViewById(R.id.count_total_percent_bar);
+    mTotalCount = (TextView) findViewById(R.id.total_count_number);
+    mTotalTotalCount = (TextView) findViewById(R.id.total_count_total);
+    mInCount = new DetailLine(R.id.in_count, R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+    mOutCount = new DetailLine(R.id.out_count, R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+    mMissedCount = new DetailLine(R.id.missed_count,
+        R.string.call_stats_missed, Calls.MISSED_TYPE);
+    mBlockedCount = new DetailLine(R.id.blocked_count,
+        R.string.call_stats_blocked, Calls.BLOCKED_TYPE);
+
+    mInAverage = new DetailLine(R.id.in_average,
+        R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+    mOutAverage = new DetailLine(R.id.out_average,
+        R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+
+    Intent launchIntent = getIntent();
+    mData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_DETAILS);
+    mTotalData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_TOTAL);
+    updateData();
+
+    TextView dateFilterView = (TextView) findViewById(R.id.date_filter);
+    long filterFrom = launchIntent.getLongExtra(EXTRA_FROM, -1);
+    if (filterFrom == -1) {
+      dateFilterView.setVisibility(View.GONE);
+    } else {
+      long filterTo = launchIntent.getLongExtra(EXTRA_TO, -1);
+      dateFilterView.setText(DateUtils.formatDateRange(this, filterFrom, filterTo, 0));
+    }
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    new UpdateContactTask().execute(mData.number.toString(), mData.countryIso);
+  }
+
+  private void updateData() {
+    mNumber = mData.number.toString();
+
+    // Cache the details about the phone number.
+    boolean canPlaceCallsTo = PhoneNumberHelper.canPlaceCallsTo(mNumber, mData.numberPresentation);
+    final CharSequence callLocationOrType = !TextUtils.isEmpty(mData.displayName)
+        ? Phone.getTypeLabel(mResources, mData.numberType, mData.numberLabel)
+        : mData.geocode;
+
+    mData.updateDisplayProperties(this, mContactDisplayPreferences.getDisplayOrder());
+
+    final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber);
+    boolean hasEditNumberBeforeCallOption =
+        canPlaceCallsTo && !isSipNumber && !mData.isVoicemailNumber;
+
+    if (!TextUtils.isEmpty(mData.displayName)) {
+      mCallerName.setText(mData.displayName);
+      mCallerNumber.setText(callLocationOrType + " " + mData.displayNumber);
+    } else {
+      mCallerName.setText(mData.displayNumber);
+      if (!TextUtils.isEmpty(callLocationOrType)) {
+        mCallerNumber.setText(callLocationOrType);
+        mCallerNumber.setVisibility(View.VISIBLE);
+      } else {
+        mCallerNumber.setVisibility(View.GONE);
+      }
+    }
+
+    mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+    mCopyButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+    mEditNumberButton.setVisibility(hasEditNumberBeforeCallOption ? View.VISIBLE : View.GONE);
+    mSeparator.setVisibility(canPlaceCallsTo || hasEditNumberBeforeCallOption
+        ? View.VISIBLE : View.GONE);
+
+    final boolean isBusiness = mContactInfoHelper.isBusiness(mData.sourceType);
+    final int contactType =
+        mData.isVoicemailNumber ? LetterTileDrawable.TYPE_VOICEMAIL :
+        isBusiness ? LetterTileDrawable.TYPE_BUSINESS :
+        LetterTileDrawable.TYPE_DEFAULT;
+    final String nameForDefaultImage = TextUtils.isEmpty(mData.name)
+        ? mData.displayNumber : mData.name;
+
+    ContactPhotoManager.getInstance(this).loadDialerThumbnailOrPhoto(mQuickContactBadge,
+        mData.contactUri, mData.photoId, mData.photoUri, nameForDefaultImage, contactType);
+
+    invalidateOptionsMenu();
+
+    long totalDuration = mData.getFullDuration();
+    mInDuration.updateFromDurations(mData.inDuration, totalDuration);
+    mOutDuration.updateFromDurations(mData.outDuration, totalDuration);
+    if (totalDuration != 0) {
+      mTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this,
+          totalDuration, true));
+      mTotalTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this,
+          mTotalData.getFullDuration(), true));
+      updateBar(mDurationBar, mData.inDuration, mData.outDuration, 0, 0, totalDuration);
+      updateBar(mTotalDurationBar, mData.inDuration, mData.outDuration,
+          0, 0, mTotalData.getFullDuration());
+      findViewById(R.id.duration_container).setVisibility(View.VISIBLE);
+    } else {
+      findViewById(R.id.duration_container).setVisibility(View.GONE);
+    }
+
+    mInAverage.updateAsAverage(mData.inDuration, mData.incomingCount);
+    mOutAverage.updateAsAverage(mData.outDuration, mData.outgoingCount);
+
+    int totalCount = mData.getTotalCount();
+    mTotalCount.setText(CallStatsListItemViewHolder.getCallCountString(this, totalCount));
+    mTotalTotalCount.setText(
+        CallStatsListItemViewHolder.getCallCountString(this, mTotalData.getTotalCount()));
+    mInCount.updateFromCounts(mData.incomingCount, totalCount);
+    mOutCount.updateFromCounts(mData.outgoingCount, totalCount);
+    mMissedCount.updateFromCounts(mData.missedCount, totalCount);
+    mBlockedCount.updateFromCounts(mData.blockedCount, totalCount);
+    updateBar(mCountBar, mData.incomingCount, mData.outgoingCount,
+        mData.missedCount, mData.blockedCount, totalCount);
+    updateBar(mTotalCountBar, mData.incomingCount, mData.outgoingCount,
+        mData.missedCount, mData.blockedCount, mTotalData.getTotalCount());
+  }
+
+  private void updateBar(LinearColorBar bar,
+      long value1, long value2, long value3, long value4, long total) {
+    bar.setRatios((float) value1 / total, (float) value2 / total,
+        (float) value3 / total, (float) value4 / total);
+  }
+
+  @Override
+  public void onClick(View view) {
+    if (view == mCallButton) {
+      Intent intent = new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_LOG).build();
+      startActivity(intent);
+    } else if (view == mCopyButton) {
+      ClipboardUtils.copyText(this, null, mNumber, true);
+    } else if (view == mEditNumberButton) {
+      startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber)));
+    }
+  }
+
+  @Override
+  public boolean onLongClick(View view) {
+    if (view == mCallButton) {
+      Intent intent = AccountSelectionActivity.createIntent(
+          CallStatsDetailActivity.this, mNumber, CallInitiationType.Type.CALL_LOG);
+      if (intent != null) {
+        startActivity(intent);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void onHomeSelected() {
+    Intent intent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
+    // This will open the call log even if the detail view has been opened directly.
+    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+    startActivity(intent);
+    finish();
+  }
+
+  private class DetailLine {
+    private int mValueTemplateResId;
+    private View mRootView;
+    private TextView mTextView;
+    private TextView mPercentView;
+
+    public DetailLine(int rootViewId, int valueTemplateResId, int iconType) {
+      mValueTemplateResId = valueTemplateResId;
+      mRootView = findViewById(rootViewId);
+      mTextView = (TextView) mRootView.findViewById(R.id.value);
+      mPercentView = (TextView) mRootView.findViewById(R.id.percent);
+
+      CallTypeIconsView icon = (CallTypeIconsView) mRootView.findViewById(R.id.icon);
+      icon.add(iconType);
+    }
+
+    public void updateFromCounts(int count, int totalCount) {
+      if (count == 0 && totalCount > 0) {
+        mRootView.setVisibility(View.GONE);
+        return;
+      }
+
+      mRootView.setVisibility(View.VISIBLE);
+      String value = CallStatsListItemViewHolder.getCallCountString(mTextView.getContext(), count);
+      mTextView.setText(getString(mValueTemplateResId, value));
+      updatePercent(count, totalCount);
+    }
+
+    public void updateFromDurations(long duration, long totalDuration) {
+      if (duration == 0 && totalDuration >= 0) {
+        mRootView.setVisibility(View.GONE);
+        return;
+      }
+
+      mRootView.setVisibility(View.VISIBLE);
+      String value = CallStatsListItemViewHolder.getDurationString(
+          mTextView.getContext(), duration, true);
+      mTextView.setText(getString(mValueTemplateResId, value));
+      updatePercent(duration, totalDuration);
+    }
+
+    public void updateAsAverage(long duration, int count) {
+      if (count == 0) {
+        mRootView.setVisibility(View.GONE);
+        return;
+      }
+
+      mRootView.setVisibility(View.VISIBLE);
+      mPercentView.setVisibility(View.GONE);
+
+      long averageDuration = (long) Math.round((float) duration / (float) count);
+      String value = CallStatsListItemViewHolder.getDurationString(
+          mTextView.getContext(), averageDuration, true);
+      mTextView.setText(getString(mValueTemplateResId, value));
+    }
+
+    private void updatePercent(long value, long total) {
+      int percent = (int) Math.round(100F * value / total);
+      mPercentView.setText(getString(R.string.call_stats_percent, percent));
+    }
+  }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsDetails.java b/java/com/android/dialer/callstats/CallStatsDetails.java
new file mode 100644
index 0000000..cfa6dfa
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsDetails.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccountHandle;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.DisplayOrder;
+import com.android.dialer.logging.ContactSource;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+/**
+ * Class to store statistical details for a given contact/number.
+ */
+public class CallStatsDetails implements Parcelable {
+  public final String number;
+  public final String postDialDigits;
+  public final int numberPresentation;
+  public String formattedNumber;
+  public final String countryIso;
+  public final String geocode;
+  public final long date;
+  public String name;
+  public String nameAlternative;
+  public int numberType;
+  public String numberLabel;
+  public Uri contactUri;
+  public Uri photoUri;
+  public long photoId;
+  public long inDuration;
+  public long outDuration;
+  public int incomingCount;
+  public int outgoingCount;
+  public int missedCount;
+  public int blockedCount;
+  public PhoneAccountHandle accountHandle;
+  public ContactSource.Type sourceType = ContactSource.Type.UNKNOWN_SOURCE_TYPE;
+
+  public boolean isVoicemailNumber;
+  public String displayNumber;
+  public String displayName;
+
+  public CallStatsDetails(CharSequence number, int numberPresentation,
+      String postDialDigits, PhoneAccountHandle accountHandle,
+      ContactInfo info, String countryIso, String geocode, long date) {
+    this.number = number != null ? number.toString() : null;
+    this.numberPresentation = numberPresentation;
+    this.postDialDigits = postDialDigits;
+    this.countryIso = countryIso;
+    this.geocode = geocode;
+    this.date = date;
+
+    reset();
+
+    if (info != null) {
+      updateFromInfo(info);
+    }
+  }
+
+  public void updateFromInfo(ContactInfo info) {
+    this.displayName = info.name;
+    this.nameAlternative = info.nameAlternative;
+    this.name = info.name;
+    this.numberType = info.type;
+    this.numberLabel = info.label;
+    this.photoId = info.photoId;
+    this.photoUri = info.photoUri;
+    this.formattedNumber = info.formattedNumber;
+    this.contactUri = info.lookupUri;
+    this.photoUri = info.photoUri;
+    this.photoId = info.photoId;
+    this.sourceType = info.sourceType;
+    this.displayNumber = null;
+  }
+
+
+  public void updateDisplayProperties(Context context, DisplayOrder nameDisplayOrder) {
+    if (nameDisplayOrder == DisplayOrder.PRIMARY || TextUtils.isEmpty(nameAlternative)) {
+      this.displayName = this.name;
+    } else {
+      this.displayName = this.nameAlternative;
+    }
+
+    if (displayNumber == null) {
+      isVoicemailNumber = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number);
+      final CharSequence displayNumber = PhoneNumberDisplayUtil.getDisplayNumber(context,
+          number, numberPresentation, formattedNumber, postDialDigits, isVoicemailNumber);
+      this.displayNumber = BidiFormatter.getInstance().unicodeWrap(
+          displayNumber.toString(), TextDirectionHeuristics.LTR);
+    }
+  }
+
+  public long getFullDuration() {
+    return inDuration + outDuration;
+  }
+
+  public int getTotalCount() {
+    return incomingCount + outgoingCount + missedCount + blockedCount;
+  }
+
+  public void addTimeOrMissed(int type, long time) {
+    switch (type) {
+      case Calls.INCOMING_TYPE:
+        incomingCount++;
+        inDuration += time;
+        break;
+      case Calls.OUTGOING_TYPE:
+        outgoingCount++;
+        outDuration += time;
+        break;
+      case Calls.MISSED_TYPE:
+        missedCount++;
+        break;
+      case Calls.BLOCKED_TYPE:
+        blockedCount++;
+        break;
+    }
+  }
+
+  public int getDurationPercentage(int type) {
+    long duration = getRequestedDuration(type);
+    return Math.round((float) duration * 100F / getFullDuration());
+  }
+
+  public int getCountPercentage(int type) {
+    int count = getRequestedCount(type);
+    return Math.round((float) count * 100F / getTotalCount());
+  }
+
+  public long getRequestedDuration(int type) {
+    switch (type) {
+      case Calls.INCOMING_TYPE:
+        return inDuration;
+      case Calls.OUTGOING_TYPE:
+        return outDuration;
+      case Calls.MISSED_TYPE:
+        return (long) missedCount;
+      case Calls.BLOCKED_TYPE:
+        return (long) blockedCount;
+      default:
+        return getFullDuration();
+    }
+  }
+
+  public int getRequestedCount(int type) {
+    switch (type) {
+      case Calls.INCOMING_TYPE:
+        return incomingCount;
+      case Calls.OUTGOING_TYPE:
+        return outgoingCount;
+      case Calls.MISSED_TYPE:
+        return missedCount;
+      case Calls.BLOCKED_TYPE:
+        return blockedCount;
+      default:
+        return getTotalCount();
+    }
+  }
+
+  public void mergeWith(CallStatsDetails other) {
+    this.inDuration += other.inDuration;
+    this.outDuration += other.outDuration;
+    this.incomingCount += other.incomingCount;
+    this.outgoingCount += other.outgoingCount;
+    this.missedCount += other.missedCount;
+    this.blockedCount += other.blockedCount;
+  }
+
+  public void reset() {
+    this.inDuration = this.outDuration = 0;
+    this.incomingCount = this.outgoingCount = this.missedCount = this.blockedCount = 0;
+  }
+
+  /* Parcelable interface */
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel out, int flags) {
+    out.writeString(number);
+    out.writeInt(numberPresentation);
+    out.writeString(postDialDigits);
+    out.writeString(formattedNumber);
+    out.writeString(countryIso);
+    out.writeString(geocode);
+    out.writeLong(date);
+    out.writeString(name);
+    out.writeInt(numberType);
+    out.writeString(numberLabel);
+    out.writeParcelable(contactUri, flags);
+    out.writeParcelable(photoUri, flags);
+    out.writeLong(photoId);
+    out.writeLong(inDuration);
+    out.writeLong(outDuration);
+    out.writeInt(incomingCount);
+    out.writeInt(outgoingCount);
+    out.writeInt(missedCount);
+    out.writeInt(blockedCount);
+  }
+
+  public static final Parcelable.Creator<CallStatsDetails> CREATOR =
+      new Parcelable.Creator<CallStatsDetails>() {
+    public CallStatsDetails createFromParcel(Parcel in) {
+      return new CallStatsDetails(in);
+    }
+
+    public CallStatsDetails[] newArray(int size) {
+      return new CallStatsDetails[size];
+    }
+  };
+
+  private CallStatsDetails (Parcel in) {
+    number = in.readString();
+    numberPresentation = in.readInt();
+    postDialDigits = in.readString();
+    formattedNumber = in.readString();
+    countryIso = in.readString();
+    geocode = in.readString();
+    date = in.readLong();
+    name = in.readString();
+    numberType = in.readInt();
+    numberLabel = in.readString();
+    contactUri = in.readParcelable(null);
+    photoUri = in.readParcelable(null);
+    photoId = in.readLong();
+    inDuration = in.readLong();
+    outDuration = in.readLong();
+    incomingCount = in.readInt();
+    outgoingCount = in.readInt();
+    missedCount = in.readInt();
+    blockedCount = in.readInt();
+  }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsFragment.java b/java/com/android/dialer/callstats/CallStatsFragment.java
new file mode 100644
index 0000000..3a90d93
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsFragment.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.telecom.PhoneAccountHandle;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment;
+import com.android.dialer.calllogutils.FilterSpinnerHelper;
+import com.android.dialer.contacts.ContactsComponent;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.widget.EmptyContentView;
+
+import java.util.Map;
+
+import static android.Manifest.permission.READ_CALL_LOG;
+
+public class CallStatsFragment extends Fragment implements
+    CallStatsQueryHandler.Listener, FilterSpinnerHelper.OnFilterChangedListener,
+    EmptyContentView.OnEmptyViewActionButtonClickedListener,
+    DoubleDatePickerDialog.OnDateSetListener {
+  private static final String TAG = "CallStatsFragment";
+
+  private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
+
+  private PhoneAccountHandle mAccountFilter = null;
+  private int mCallTypeFilter = -1;
+  private long mFilterFrom = -1;
+  private long mFilterTo = -1;
+  private boolean mSortByDuration = true;
+  private boolean mDataLoaded = false;
+
+  private RecyclerView mRecyclerView;
+  private EmptyContentView mEmptyListView;
+  private LinearLayoutManager mLayoutManager;
+  private CallStatsAdapter mAdapter;
+  private CallStatsQueryHandler mCallStatsQueryHandler;
+  private FilterSpinnerHelper mFilterHelper;
+
+  private TextView mSumHeaderView;
+  private TextView mDateFilterView;
+
+  private boolean mHasReadCallLogPermission = false;
+
+  private boolean mRefreshDataRequired = true;
+  private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+    @Override
+    public void onChange(boolean selfChange) {
+      mRefreshDataRequired = true;
+    }
+  };
+
+  @Override
+  public void onCreate(Bundle state) {
+    super.onCreate(state);
+
+    final ContentResolver cr = getActivity().getContentResolver();
+    mCallStatsQueryHandler = new CallStatsQueryHandler(cr, this);
+    cr.registerContentObserver(CallLog.CONTENT_URI, true, mObserver);
+    cr.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, mObserver);
+
+    setHasOptionsMenu(true);
+
+    ExpirableCacheHeadlessFragment cacheFragment =
+        ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity());
+    mAdapter = new CallStatsAdapter(getActivity(),
+        ContactsComponent.get(getActivity()).contactDisplayPreferences(),
+        cacheFragment.getRetainedCache());
+  }
+
+  @Override
+  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+    View view = inflater.inflate(R.layout.call_stats_fragment, container, false);
+
+    mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+    mRecyclerView.setHasFixedSize(true);
+    mLayoutManager = new LinearLayoutManager(getActivity());
+    mRecyclerView.setLayoutManager(mLayoutManager);
+    mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+    mEmptyListView.setImage(R.drawable.empty_call_log);
+    mEmptyListView.setActionClickedListener(this);
+
+    mSumHeaderView = (TextView) view.findViewById(R.id.sum_header);
+    mDateFilterView = (TextView) view.findViewById(R.id.date_filter);
+
+    return view;
+  }
+
+  @Override
+  public void onViewCreated(View view, Bundle savedInstanceState) {
+    super.onViewCreated(view, savedInstanceState);
+    mRecyclerView.setAdapter(mAdapter);
+    mFilterHelper = new FilterSpinnerHelper(view, false, this);
+    updateEmptyVisibilityAndMessage();
+  }
+
+  @Override
+  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+    if (getUserVisibleHint() && PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
+      inflater.inflate(R.menu.call_stats_options, menu);
+
+      final MenuItem resetItem = menu.findItem(R.id.reset_date_filter);
+      final MenuItem sortDurationItem = menu.findItem(R.id.sort_by_duration);
+      final MenuItem sortCountItem = menu.findItem(R.id.sort_by_count);
+
+      resetItem.setVisible(mFilterFrom != -1);
+      sortDurationItem.setVisible(!mSortByDuration);
+      sortCountItem.setVisible(mSortByDuration);
+    }
+
+    super.onCreateOptionsMenu(menu, inflater);
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    final int itemId = item.getItemId();
+    switch (itemId) {
+      case R.id.date_filter: {
+        final DoubleDatePickerDialog.Fragment fragment =
+            new DoubleDatePickerDialog.Fragment();
+        fragment.setArguments(
+            DoubleDatePickerDialog.Fragment.createArguments(mFilterFrom, mFilterTo));
+        fragment.show(getFragmentManager(), "filter");
+        break;
+      }
+      case R.id.reset_date_filter: {
+        mFilterFrom = -1;
+        mFilterTo = -1;
+        fetchCalls();
+        updateEmptyVisibilityAndMessage();
+        getActivity().invalidateOptionsMenu();
+        break;
+      }
+      case R.id.sort_by_duration:
+      case R.id.sort_by_count: {
+        mSortByDuration = itemId == R.id.sort_by_duration;
+        mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+        getActivity().invalidateOptionsMenu();
+        break;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public void onFilterChanged(PhoneAccountHandle account, int callType) {
+    if (account != mAccountFilter) {
+      mAccountFilter = account;
+      fetchCalls();
+    }
+    if (callType != mCallTypeFilter) {
+      mCallTypeFilter = callType;
+      mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+      if (mDataLoaded) {
+        updateHeader();
+        updateEmptyVisibilityAndMessage();
+      }
+    }
+  }
+
+  @Override
+  public void onEmptyViewActionButtonClicked() {
+    if (!PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
+      requestPermissions(new String[] { READ_CALL_LOG },
+          READ_CALL_LOG_PERMISSION_REQUEST_CODE);
+    }
+  }
+
+  @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;
+        getActivity().invalidateOptionsMenu();
+      }
+    }
+  }
+
+  @Override
+  public void onDateSet(long from, long to) {
+    mFilterFrom = from;
+    mFilterTo = to;
+    getActivity().invalidateOptionsMenu();
+    fetchCalls();
+    updateEmptyVisibilityAndMessage();
+  }
+
+  /**
+   * Called by the CallStatsQueryHandler when the list of calls has been
+   * fetched or updated.
+   */
+  @Override
+  public void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls) {
+    if (getActivity() == null || getActivity().isFinishing()) {
+      return;
+    }
+
+    mDataLoaded = true;
+    mAdapter.updateData(calls, mFilterFrom, mFilterTo);
+    mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+    updateHeader();
+    updateEmptyVisibilityAndMessage();
+  }
+
+  @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;
+      mDataLoaded = false;
+      updateEmptyVisibilityAndMessage();
+      getActivity().invalidateOptionsMenu();
+    }
+    mHasReadCallLogPermission = hasReadCallLogPermission;
+    refreshData();
+    mAdapter.startCache();
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    mAdapter.pauseCache();
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    mAdapter.pauseCache();
+    getActivity().getContentResolver().unregisterContentObserver(mObserver);
+  }
+
+  private void fetchCalls() {
+    mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo, mAccountFilter);
+  }
+
+  private void updateHeader() {
+    final String callCount = mAdapter.getTotalCallCountString();
+    final String duration = mAdapter.getFullDurationString(false);
+
+    if (duration != null) {
+      mSumHeaderView.setText(getString(R.string.call_stats_header_total, callCount, duration));
+    } else {
+      mSumHeaderView.setText(getString(R.string.call_stats_header_total_callsonly, callCount));
+    }
+    mSumHeaderView.setVisibility(isListEmpty() ? View.GONE : View.VISIBLE);
+
+    if (mFilterFrom == -1) {
+      mDateFilterView.setVisibility(View.GONE);
+    } else {
+      mDateFilterView.setText(
+          DateUtils.formatDateRange(getActivity(), mFilterFrom, mFilterTo, 0));
+      mDateFilterView.setVisibility(View.VISIBLE);
+    }
+  }
+
+  /** Requests updates to the data to be shown. */
+  private void refreshData() {
+    // Prevent unnecessary refresh.
+    if (mRefreshDataRequired) {
+      // Mark all entries in the contact info cache as out of date, so
+      // they will be looked up again once being shown.
+      mAdapter.invalidateCache();
+      fetchCalls();
+      mRefreshDataRequired = false;
+    }
+  }
+
+  private boolean isListEmpty() {
+    return mDataLoaded && mAdapter.getItemCount() == 0;
+  }
+
+  private void updateEmptyVisibilityAndMessage() {
+    final Context context = getActivity();
+    if (context == null) {
+      return;
+    }
+
+    boolean showListView = !isListEmpty();
+
+    if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
+      mEmptyListView.setDescription(R.string.permission_no_calllog);
+      mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+      showListView = false;
+    } else if (mFilterFrom > 0 || mFilterTo > 0) {
+      mEmptyListView.setDescription(R.string.recent_calls_no_items_in_range);
+      mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+    } else {
+      mEmptyListView.setDescription(R.string.call_log_all_empty);
+      mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+    }
+
+    mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
+    mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
+  }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java b/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java
new file mode 100644
index 0000000..bda6b3e
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2011 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.callstats;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.widget.LinearColorBar;
+
+/**
+ * This is an object containing references to views contained by the call log list item. This
+ * improves performance by reducing the frequency with which we need to find views by IDs.
+ *
+ * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter.
+ */
+public final class CallStatsListItemViewHolder extends RecyclerView.ViewHolder
+    implements View.OnClickListener {
+
+  public CallStatsDetails details;
+  public Intent clickIntent;
+
+  public final View mRootView;
+  public final QuickContactBadge mQuickContactView;
+  public final View mPrimaryActionView;
+  public final TextView mNameView;
+  public final TextView mNumberView;
+  public final TextView mLabelView;
+  public final TextView mPercentView;
+  public final LinearColorBar mBarView;
+
+  private Context mContext;
+  private ContactInfoHelper mContactInfoHelper;
+  private final int mPhotoSize;
+
+  private CallStatsListItemViewHolder(View rootView,
+      QuickContactBadge quickContactView,
+      View primaryActionView,
+      TextView nameView,
+      TextView numberView,
+      TextView labelView,
+      TextView percentView,
+      LinearColorBar barView,
+      ContactInfoHelper contactInfoHelper) {
+    super(rootView);
+
+    mRootView = rootView;
+    mQuickContactView = quickContactView;
+    mPrimaryActionView = primaryActionView;
+    mNameView = nameView;
+    mNumberView = numberView;
+    mLabelView = labelView;
+    mPercentView = percentView;
+    mBarView = barView;
+
+    mPrimaryActionView.setOnClickListener(this);
+
+    quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+
+    mContext = rootView.getContext();
+    mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+    mContactInfoHelper = contactInfoHelper;
+  }
+
+  public static CallStatsListItemViewHolder create(View view,
+      ContactInfoHelper contactInfoHelper) {
+    return new CallStatsListItemViewHolder(view,
+        (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+        view.findViewById(R.id.primary_action_view),
+        (TextView) view.findViewById(R.id.name),
+        (TextView) view.findViewById(R.id.number),
+        (TextView) view.findViewById(R.id.label),
+        (TextView) view.findViewById(R.id.percent),
+        (LinearColorBar) view.findViewById(R.id.percent_bar),
+        contactInfoHelper);
+  }
+
+  @Override
+  public void onClick(View v) {
+    if (clickIntent != null) {
+      DialerUtils.startActivityWithErrorToast(mContext, clickIntent);
+    }
+  }
+
+  public void setDetails(CallStatsDetails details, CallStatsDetails first,
+      CallStatsDetails total, int type, boolean byDuration,
+      ContactDisplayPreferences.DisplayOrder nameDisplayOrder) {
+    this.details = details;
+    details.updateDisplayProperties(mContext, nameDisplayOrder);
+
+    CharSequence numberFormattedLabel = null;
+    // Only show a label if the number is shown and it is not a SIP address.
+    if (!TextUtils.isEmpty(details.number)
+        && !PhoneNumberHelper.isUriNumber(details.number.toString())) {
+      numberFormattedLabel = Phone.getTypeLabel(mContext.getResources(),
+          details.numberType, details.numberLabel);
+    }
+
+    final CharSequence nameText;
+    final CharSequence numberText;
+    final CharSequence labelText;
+
+    if (TextUtils.isEmpty(details.displayName)) {
+      nameText = details.displayNumber;
+      if (TextUtils.isEmpty(details.geocode) || details.isVoicemailNumber) {
+          numberText = null;
+      } else {
+          numberText = details.geocode;
+      }
+      labelText = null;
+    } else {
+      nameText = details.displayName;
+      numberText = details.displayNumber;
+      labelText = numberFormattedLabel;
+    }
+
+    float in = 0, out = 0, missed = 0, blocked = 0;
+    float ratio = getDetailValue(details, type, byDuration) /
+                  getDetailValue(first, type, byDuration);
+
+    if (type == Calls.INCOMING_TYPE) {
+      in = ratio;
+    } else if (type == Calls.OUTGOING_TYPE) {
+      out = ratio;
+    } else if (type == Calls.MISSED_TYPE) {
+      missed = ratio;
+    } else if (type == Calls.BLOCKED_TYPE) {
+      blocked = ratio;
+    } else {
+      float full = getDetailValue(details, type, byDuration);
+      in = getDetailValue(details, Calls.INCOMING_TYPE, byDuration) * ratio / full;
+      out = getDetailValue(details, Calls.OUTGOING_TYPE, byDuration) * ratio / full;
+      if (!byDuration) {
+          missed = getDetailValue(details, Calls.MISSED_TYPE, byDuration) * ratio / full;
+          blocked = getDetailValue(details, Calls.BLOCKED_TYPE, byDuration) * ratio / full;
+      }
+    }
+
+    mBarView.setRatios(in, out, missed, blocked);
+    mNameView.setText(nameText);
+    mNumberView.setText(numberText);
+    mLabelView.setText(labelText);
+    mLabelView.setVisibility(TextUtils.isEmpty(labelText) ? View.GONE : View.VISIBLE);
+
+    if (byDuration && type == Calls.MISSED_TYPE) {
+      mPercentView.setText(getCallCountString(mContext, details.missedCount));
+    } else if (byDuration && type == Calls.BLOCKED_TYPE) {
+      mPercentView.setText(getCallCountString(mContext, details.blockedCount));
+    } else {
+      float percent = getDetailValue(details, type, byDuration) * 100F /
+                      getDetailValue(total, type, byDuration);
+      mPercentView.setText(String.format("%.1f%%", percent));
+    }
+
+    final String nameForDefaultImage = TextUtils.isEmpty(details.name)
+        ? details.displayNumber : details.name;
+
+    int contactType = LetterTileDrawable.TYPE_DEFAULT;
+    if (details.isVoicemailNumber) {
+      contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+    } else if (mContactInfoHelper.isBusiness(details.sourceType)) {
+      contactType = LetterTileDrawable.TYPE_BUSINESS;
+    }
+
+    ContactPhotoManager.getInstance(mContext).loadDialerThumbnailOrPhoto(mQuickContactView,
+        details.contactUri, details.photoId, details.photoUri, nameForDefaultImage, contactType);
+  }
+
+  private float getDetailValue(CallStatsDetails details, int type, boolean byDuration) {
+    if (byDuration) {
+        return (float) details.getRequestedDuration(type);
+    } else {
+        return (float) details.getRequestedCount(type);
+    }
+  }
+
+  public static String getCallCountString(Context context, long count) {
+    return context.getResources().getQuantityString(R.plurals.call, (int) count, (int) count);
+  }
+
+  public static String getDurationString(Context context, long duration, boolean includeSeconds) {
+    int hours, minutes, seconds;
+
+    hours = (int) (duration / 3600);
+    duration -= (long) hours * 3600;
+    minutes = (int) (duration / 60);
+    duration -= (long) minutes * 60;
+    seconds = (int) duration;
+
+    if (!includeSeconds) {
+      if (seconds >= 30) {
+        minutes++;
+      }
+      if (minutes >= 60) {
+        hours++;
+      }
+    }
+
+    boolean dispHours = hours > 0;
+    boolean dispMinutes = minutes > 0 || (!includeSeconds && hours == 0);
+    boolean dispSeconds = includeSeconds && (seconds > 0 || (hours == 0 && minutes == 0));
+
+    final Resources res = context.getResources();
+    final String hourString = dispHours ?
+        res.getQuantityString(R.plurals.hour, hours, hours) : null;
+    final String minuteString = dispMinutes ?
+        res.getQuantityString(R.plurals.minute, minutes, minutes) : null;
+    final String secondString = dispSeconds ?
+        res.getQuantityString(R.plurals.second, seconds, seconds) : null;
+
+    int index = ((dispHours ? 4 : 0) | (dispMinutes ? 2 : 0) | (dispSeconds ? 1 : 0)) - 1;
+    String[] formats = res.getStringArray(R.array.call_stats_duration);
+    return String.format(formats[index], hourString, minuteString, secondString);
+  }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsQuery.java b/java/com/android/dialer/callstats/CallStatsQuery.java
new file mode 100644
index 0000000..92bd9c7
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsQuery.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.provider.CallLog.Calls;
+
+public class CallStatsQuery {
+  public static final String[] _PROJECTION = new String[] {
+    Calls._ID, // 0
+    Calls.NUMBER, // 1
+    Calls.DATE, // 2
+    Calls.DURATION, // 3
+    Calls.TYPE, // 4
+    Calls.COUNTRY_ISO, // 5
+    Calls.GEOCODED_LOCATION, // 6
+    Calls.CACHED_NAME, // 7
+    Calls.CACHED_NUMBER_TYPE, // 8
+    Calls.CACHED_NUMBER_LABEL, // 9
+    Calls.CACHED_LOOKUP_URI, // 10
+    Calls.CACHED_MATCHED_NUMBER, // 11
+    Calls.CACHED_NORMALIZED_NUMBER, // 12
+    Calls.CACHED_PHOTO_ID, // 13
+    Calls.CACHED_FORMATTED_NUMBER, // 14
+    Calls.NUMBER_PRESENTATION, // 15
+    Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 16
+    Calls.PHONE_ACCOUNT_ID,             // 17
+    Calls.POST_DIAL_DIGITS, // 18
+  };
+
+  public static final int ID = 0;
+  public static final int NUMBER = 1;
+  public static final int DATE = 2;
+  public static final int DURATION = 3;
+  public static final int CALL_TYPE = 4;
+  public static final int COUNTRY_ISO = 5;
+  public static final int GEOCODED_LOCATION = 6;
+  public static final int CACHED_NAME = 7;
+  public static final int CACHED_NUMBER_TYPE = 8;
+  public static final int CACHED_NUMBER_LABEL = 9;
+  public static final int CACHED_LOOKUP_URI = 10;
+  public static final int CACHED_MATCHED_NUMBER = 11;
+  public static final int CACHED_NORMALIZED_NUMBER = 12;
+  public static final int CACHED_PHOTO_ID = 13;
+  public static final int CACHED_FORMATTED_NUMBER = 14;
+  public static final int NUMBER_PRESENTATION = 15;
+  public static final int ACCOUNT_COMPONENT_NAME = 16;
+  public static final int ACCOUNT_ID = 17;
+  public static final int POST_DIAL_DIGITS = 18;
+}
diff --git a/java/com/android/dialer/callstats/CallStatsQueryHandler.java b/java/com/android/dialer/callstats/CallStatsQueryHandler.java
new file mode 100644
index 0000000..3c93be0
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsQueryHandler.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.UriUtils;
+
+import com.google.common.collect.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class to handle call-log queries, optionally with a date-range filter
+ */
+public class CallStatsQueryHandler extends AsyncQueryHandler {
+  private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+  private static final int EVENT_PROCESS_DATA = 10;
+
+  private static final int QUERY_CALLS_TOKEN = 100;
+
+  private static final String TAG = "CallStatsQueryHandler";
+
+  private final WeakReference<Listener> mListener;
+  private Handler mWorkerThreadHandler;
+
+  /**
+   * Simple handler that wraps background calls to catch
+   * {@link SQLiteException}, such as when the disk is full.
+   */
+  protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+    public CatchingWorkerHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      if (msg.arg1 == EVENT_PROCESS_DATA) {
+        Cursor cursor = (Cursor) msg.obj;
+        Message reply = CallStatsQueryHandler.this.obtainMessage(msg.what);
+        reply.obj = processData(cursor);
+        reply.arg1 = msg.arg1;
+        reply.sendToTarget();
+        return;
+      }
+
+      try {
+        // Perform same query while catching any exceptions
+        super.handleMessage(msg);
+      } catch (SQLiteDiskIOException e) {
+        Log.w(TAG, "Exception on background worker thread", e);
+      } catch (SQLiteFullException e) {
+        Log.w(TAG, "Exception on background worker thread", e);
+      } catch (SQLiteDatabaseCorruptException e) {
+        Log.w(TAG, "Exception on background worker thread", e);
+      }
+    }
+  }
+
+  @Override
+  protected Handler createHandler(Looper looper) {
+    // Provide our special handler that catches exceptions
+    mWorkerThreadHandler = new CatchingWorkerHandler(looper);
+    return mWorkerThreadHandler;
+  }
+
+  public CallStatsQueryHandler(ContentResolver contentResolver, Listener listener) {
+    super(contentResolver);
+    mListener = new WeakReference<Listener>(listener);
+  }
+
+  public void fetchCalls(long from, long to, PhoneAccountHandle account) {
+    cancelOperation(QUERY_CALLS_TOKEN);
+
+    StringBuilder selection = new StringBuilder();
+    List<String> selectionArgs = Lists.newArrayList();
+
+    if (from != -1) {
+      selection.append(String.format("(%s > ?)", Calls.DATE));
+      selectionArgs.add(String.valueOf(from));
+    }
+    if (to != -1) {
+      if (selection.length() > 0) {
+        selection.append(" AND ");
+      }
+      selection.append(String.format("(%s < ?)", Calls.DATE));
+      selectionArgs.add(String.valueOf(to));
+    }
+    if (account != null) {
+      if (selection.length() > 0) {
+        selection.append(" AND ");
+      }
+      selection.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID));
+      selectionArgs.add(account.getId());
+    }
+
+    startQuery(QUERY_CALLS_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+        CallStatsQuery._PROJECTION, selection.toString(),
+        selectionArgs.toArray(EMPTY_STRING_ARRAY), Calls.NUMBER + " ASC");
+  }
+
+  @Override
+  protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+    if (token == QUERY_CALLS_TOKEN) {
+      Message msg = mWorkerThreadHandler.obtainMessage(token);
+      msg.arg1 = EVENT_PROCESS_DATA;
+      msg.obj = cursor;
+
+      mWorkerThreadHandler.sendMessage(msg);
+    }
+  }
+
+  @Override
+  public void handleMessage(Message msg) {
+    if (msg.arg1 == EVENT_PROCESS_DATA) {
+      final Map<ContactInfo, CallStatsDetails> calls =
+          (Map<ContactInfo, CallStatsDetails>) msg.obj;
+      final Listener listener = mListener.get();
+      if (listener != null) {
+        listener.onCallsFetched(calls);
+      }
+    } else {
+      super.handleMessage(msg);
+    }
+  }
+
+  private Map<ContactInfo, CallStatsDetails> processData(Cursor cursor) {
+    final Map<ContactInfo, CallStatsDetails> result = new HashMap<ContactInfo, CallStatsDetails>();
+    final ArrayList<ContactInfo> infos = new ArrayList<ContactInfo>();
+    final ArrayList<CallStatsDetails> calls = new ArrayList<CallStatsDetails>();
+    CallStatsDetails pending = null;
+
+    cursor.moveToFirst();
+
+    while (!cursor.isAfterLast()) {
+      final String number = cursor.getString(CallStatsQuery.NUMBER);
+      final long duration = cursor.getLong(CallStatsQuery.DURATION);
+      final int callType = cursor.getInt(CallStatsQuery.CALL_TYPE);
+
+      if (pending == null || !phoneNumbersEqual(pending.number.toString(), number)) {
+        final long date = cursor.getLong(CallStatsQuery.DATE);
+        final int numberPresentation = cursor.getInt(CallStatsQuery.NUMBER_PRESENTATION);
+        final String countryIso = cursor.getString(CallStatsQuery.COUNTRY_ISO);
+        final String geocode = cursor.getString(CallStatsQuery.GEOCODED_LOCATION);
+        final String postDialDigits = cursor.getString(CallStatsQuery.POST_DIAL_DIGITS);
+        final ContactInfo info = getContactInfoFromCallStats(cursor);
+        final PhoneAccountHandle accountHandle = TelecomUtil.composePhoneAccountHandle(
+            cursor.getString(CallStatsQuery.ACCOUNT_COMPONENT_NAME),
+            cursor.getString(CallStatsQuery.ACCOUNT_ID));
+
+        pending = new CallStatsDetails(number, numberPresentation, postDialDigits,
+            accountHandle, info, countryIso, geocode, date);
+        infos.add(info);
+        calls.add(pending);
+      }
+
+      pending.addTimeOrMissed(callType, duration);
+      cursor.moveToNext();
+    }
+
+    cursor.close();
+    mergeItemsByNumber(calls, infos);
+
+    for (int i = 0; i < calls.size(); i++) {
+      result.put(infos.get(i), calls.get(i));
+    }
+
+    return result;
+  }
+
+  private void mergeItemsByNumber(List<CallStatsDetails> calls, List<ContactInfo> infos) {
+    // temporarily store items marked for removal
+    final ArrayList<CallStatsDetails> callsToRemove = new ArrayList<CallStatsDetails>();
+    final ArrayList<ContactInfo> infosToRemove = new ArrayList<ContactInfo>();
+
+    for (int i = 0; i < calls.size(); i++) {
+      final CallStatsDetails outerItem = calls.get(i);
+      final String currentFormattedNumber = outerItem.number.toString();
+
+      for (int j = calls.size() - 1; j > i; j--) {
+        final CallStatsDetails innerItem = calls.get(j);
+        final String innerNumber = innerItem.number.toString();
+
+        if (phoneNumbersEqual(currentFormattedNumber, innerNumber)) {
+          outerItem.mergeWith(innerItem);
+          //make sure we're not counting twice in case we're dealing with
+          //multiple different formats
+          innerItem.reset();
+          callsToRemove.add(innerItem);
+          infosToRemove.add(infos.get(j));
+        }
+      }
+    }
+
+    for (CallStatsDetails call : callsToRemove) {
+      calls.remove(call);
+    }
+    for (ContactInfo info : infosToRemove) {
+      infos.remove(info);
+    }
+  }
+
+  private ContactInfo getContactInfoFromCallStats(Cursor c) {
+    ContactInfo info = new ContactInfo();
+    info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallStatsQuery.CACHED_LOOKUP_URI));
+    info.name = c.getString(CallStatsQuery.CACHED_NAME);
+    info.type = c.getInt(CallStatsQuery.CACHED_NUMBER_TYPE);
+    info.label = c.getString(CallStatsQuery.CACHED_NUMBER_LABEL);
+
+    final String matchedNumber = c.getString(CallStatsQuery.CACHED_MATCHED_NUMBER);
+    info.number = matchedNumber == null ? c.getString(CallStatsQuery.NUMBER) : matchedNumber;
+    info.normalizedNumber = c.getString(CallStatsQuery.CACHED_NORMALIZED_NUMBER);
+    info.formattedNumber = c.getString(CallStatsQuery.CACHED_FORMATTED_NUMBER);
+
+    info.photoId = c.getLong(CallStatsQuery.CACHED_PHOTO_ID);
+    info.photoUri = null; // We do not cache the photo URI.
+
+    return info;
+  }
+
+  private static boolean phoneNumbersEqual(String number1, String number2) {
+    if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) {
+      return sipAddressesEqual(number1, number2);
+    } else {
+      return PhoneNumberUtils.compare(number1, number2);
+    }
+  }
+
+  private static boolean sipAddressesEqual(String number1, String number2) {
+    if (number1 == null || number2 == null) {
+      return number1 == number2;
+    }
+
+    int index1 = number1.indexOf('@');
+    final String userinfo1;
+    final String rest1;
+    if (index1 != -1) {
+      userinfo1 = number1.substring(0, index1);
+      rest1 = number1.substring(index1);
+    } else {
+      userinfo1 = number1;
+      rest1 = "";
+    }
+
+    int index2 = number2.indexOf('@');
+    final String userinfo2;
+    final String rest2;
+    if (index2 != -1) {
+      userinfo2 = number2.substring(0, index2);
+      rest2 = number2.substring(index2);
+    } else {
+      userinfo2 = number2;
+      rest2 = "";
+    }
+
+    return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+  }
+
+  public interface Listener {
+    void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls);
+  }
+}
diff --git a/java/com/android/dialer/callstats/DoubleDatePickerDialog.java b/java/com/android/dialer/callstats/DoubleDatePickerDialog.java
new file mode 100644
index 0000000..5d11bec
--- /dev/null
+++ b/java/com/android/dialer/callstats/DoubleDatePickerDialog.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.callstats;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.DatePicker.OnDateChangedListener;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+import com.android.dialer.R;
+
+/**
+ * Alertdialog with two date pickers - one for a start and one for an end date.
+ * Used to filter the callstats query.
+ */
+public class DoubleDatePickerDialog extends AlertDialog
+    implements OnClickListener, OnDateChangedListener, OnItemSelectedListener {
+
+  private static final String TAG = "DoubleDatePickerDialog";
+
+  public interface OnDateSetListener {
+    void onDateSet(long from, long to);
+  }
+
+  public static class Fragment extends DialogFragment implements OnDateSetListener {
+    private DoubleDatePickerDialog mDialog;
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+      mDialog = new DoubleDatePickerDialog(getActivity(), this);
+      return mDialog;
+    }
+
+    @Override
+    public void onStart() {
+      final Bundle args = getArguments();
+      final long from = args.getLong("from", -1);
+      final long to = args.getLong("to", -1);
+
+      if (from != -1) {
+          mDialog.setValues(from, to);
+      } else {
+          mDialog.resetPickers();
+      }
+      super.onStart();
+    }
+
+    @Override
+    public void onDateSet(long from, long to) {
+      ((DoubleDatePickerDialog.OnDateSetListener) getActivity()).onDateSet(from, to);
+    }
+
+    public static Bundle createArguments(long from, long to) {
+      final Bundle args = new Bundle();
+      args.putLong("from", from);
+      args.putLong("to", to);
+      return args;
+    }
+  }
+
+  private interface QuickSelection {
+    void adjustStartDate(Calendar date);
+  }
+
+  private static final int[] QUICKSELECTION_ENTRIES = new int[] {
+    R.string.date_qs_currentmonth,
+    R.string.date_qs_currentquarter,
+    R.string.date_qs_currentyear,
+    R.string.date_qs_lastweek,
+    R.string.date_qs_lastmonth,
+    R.string.date_qs_lastquarter,
+    R.string.date_qs_lastyear
+  };
+
+  private static final QuickSelection[] QUICKSELECTIONS = new QuickSelection[] {
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+          date.set(Calendar.DAY_OF_MONTH, 1);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        final int currentMonth = date.get(Calendar.MONTH);
+        date.set(Calendar.MONTH, currentMonth - (currentMonth % 3));
+        date.set(Calendar.DAY_OF_MONTH, 1);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        date.set(Calendar.MONTH, 0);
+        date.set(Calendar.DAY_OF_MONTH, 1);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        date.add(Calendar.WEEK_OF_YEAR, -1);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        date.add(Calendar.MONTH, -1);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        date.add(Calendar.MONTH, -3);
+      }
+    },
+    new QuickSelection() {
+      @Override
+      public void adjustStartDate(Calendar date) {
+        date.add(Calendar.YEAR, -1);
+      }
+    },
+  };
+
+  private static final String YEAR = "year";
+  private static final String MONTH = "month";
+  private static final String DAY = "day";
+
+  private final Spinner mQuickSelSpinner;
+  private final DatePicker mDatePickerFrom;
+  private final DatePicker mDatePickerTo;
+  private final OnDateSetListener mCallBack;
+  private Button mOkButton;
+  private int mQuickSelSelection = -1;
+
+  public DoubleDatePickerDialog(final Context context, OnDateSetListener callBack) {
+    super(context);
+
+    mCallBack = callBack;
+
+    setTitle(R.string.call_stats_filter_picker_title);
+    setButton(BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this);
+    setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
+    setIcon(0);
+
+    LayoutInflater inflater =
+        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    View view = inflater.inflate(R.layout.double_date_picker_dialog, null);
+    setView(view);
+
+    mDatePickerFrom = (DatePicker) view.findViewById(R.id.date_picker_from);
+    mDatePickerTo = (DatePicker) view.findViewById(R.id.date_picker_to);
+
+    ArrayList<CharSequence> quickSelEntries = new ArrayList<CharSequence>();
+    for (int entryId : QUICKSELECTION_ENTRIES) {
+      quickSelEntries.add(context.getString(entryId));
+    }
+    ArrayAdapter<CharSequence> quickSelAdapter = new ArrayAdapter<CharSequence>(
+        context, android.R.layout.simple_spinner_item, android.R.id.text1, quickSelEntries) {
+      @Override
+      public View getView(int position, View convertView, android.view.ViewGroup parent) {
+        final TextView v = (TextView) super.getView(position, convertView, parent);
+        v.setText(context.getString(R.string.date_quick_selection));
+        return v;
+      }
+    };
+    quickSelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+    mQuickSelSpinner = (Spinner) view.findViewById(R.id.date_quick_selection);
+    mQuickSelSpinner.setOnItemSelectedListener(this);
+    mQuickSelSpinner.setAdapter(quickSelAdapter);
+
+    resetPickers();
+  }
+
+  @Override
+  protected void onStart() {
+    super.onStart();
+    mOkButton = getButton(DialogInterface.BUTTON_POSITIVE);
+    updateOkButtonState();
+  }
+
+  @Override
+  public void onClick(DialogInterface dialog, int which) {
+    switch (which) {
+      case BUTTON_POSITIVE:
+        tryNotifyDateSet();
+        break;
+      case BUTTON_NEGATIVE:
+        break;
+    }
+  }
+
+  @Override
+  public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+    if (mQuickSelSelection >= 0) {
+      QuickSelection sel = QUICKSELECTIONS[pos];
+      Calendar from = Calendar.getInstance();
+      long millisTo = from.getTimeInMillis();
+      sel.adjustStartDate(from);
+      long millisFrom = from.getTimeInMillis();
+
+      setValues(millisFrom, millisTo);
+    }
+    mQuickSelSelection = pos;
+  }
+
+  @Override
+  public void onNothingSelected(AdapterView<?> parent) {
+  }
+
+  public void onDateChanged(DatePicker view, int year, int month, int day) {
+    view.init(year, month, day, this);
+    updateOkButtonState();
+  }
+
+  public void setValues(long millisFrom, long millisTo) {
+    setPicker(mDatePickerFrom, millisFrom);
+    setPicker(mDatePickerTo, millisTo);
+    updateOkButtonState();
+  }
+
+  public void resetPickers() {
+    long millis = System.currentTimeMillis();
+    setPicker(mDatePickerFrom, millis);
+    setPicker(mDatePickerTo, millis);
+    updateOkButtonState();
+  }
+
+  private void setPicker(DatePicker picker, long millis) {
+    Calendar c = Calendar.getInstance();
+    c.setTimeInMillis(millis);
+
+    int year = c.get(Calendar.YEAR);
+    int month = c.get(Calendar.MONTH);
+    int day = c.get(Calendar.DAY_OF_MONTH);
+
+    picker.init(year, month, day, this);
+  }
+
+  private long getMillisForPicker(DatePicker picker, boolean endOfDay) {
+    Calendar c = Calendar.getInstance();
+    c.set(Calendar.YEAR, picker.getYear());
+    c.set(Calendar.MONTH, picker.getMonth());
+    c.set(Calendar.DAY_OF_MONTH, picker.getDayOfMonth());
+    c.set(Calendar.HOUR_OF_DAY, 0);
+    c.set(Calendar.MINUTE, 0);
+    c.set(Calendar.SECOND, 0);
+
+    long millis = c.getTimeInMillis();
+    if (endOfDay) {
+      millis += 24L * 60L * 60L * 1000L - 1L;
+    }
+
+    return millis;
+  }
+
+  private void updateOkButtonState() {
+    if (mOkButton != null) {
+      long millisFrom = getMillisForPicker(mDatePickerFrom, false);
+      long millisTo = getMillisForPicker(mDatePickerTo, true);
+      mOkButton.setEnabled(millisFrom < millisTo);
+    }
+  }
+
+  private void tryNotifyDateSet() {
+    if (mCallBack != null) {
+      mDatePickerFrom.clearFocus();
+      mDatePickerTo.clearFocus();
+
+      long millisFrom = getMillisForPicker(mDatePickerFrom, false);
+      long millisTo = getMillisForPicker(mDatePickerTo, true);
+
+      mCallBack.onDateSet(millisFrom, millisTo);
+    }
+  }
+
+  // users like to play with it, so save the state and don't reset each time
+  @Override
+  public Bundle onSaveInstanceState() {
+    Bundle state = super.onSaveInstanceState();
+    state.putInt("F_" + YEAR, mDatePickerFrom.getYear());
+    state.putInt("F_" + MONTH, mDatePickerFrom.getMonth());
+    state.putInt("F_" + DAY, mDatePickerFrom.getDayOfMonth());
+    state.putInt("T_" + YEAR, mDatePickerTo.getYear());
+    state.putInt("T_" + MONTH, mDatePickerTo.getMonth());
+    state.putInt("T_" + DAY, mDatePickerTo.getDayOfMonth());
+    return state;
+  }
+
+  @Override
+  public void onRestoreInstanceState(Bundle savedInstanceState) {
+    super.onRestoreInstanceState(savedInstanceState);
+    int fyear = savedInstanceState.getInt("F_" + YEAR);
+    int fmonth = savedInstanceState.getInt("F_" + MONTH);
+    int fday = savedInstanceState.getInt("F_" + DAY);
+    int tyear = savedInstanceState.getInt("T_" + YEAR);
+    int tmonth = savedInstanceState.getInt("T_" + MONTH);
+    int tday = savedInstanceState.getInt("T_" + DAY);
+    mDatePickerFrom.init(fyear, fmonth, fday, this);
+    mDatePickerTo.init(tyear, tmonth, tday, this);
+  }
+}
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml
new file mode 100644
index 0000000..c12dbfa
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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:id="@+id/call_stats_detail"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical"
+  android:layout_alignParentStart="true"
+  android:layout_alignParentTop="true"
+  android:background="?android:attr/colorBackground">
+
+  <com.android.dialer.widget.DialerToolbar
+    android:id="@+id/toolbar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"/>
+
+  <!-- Caller information "card" -->
+  <LinearLayout
+    android:id="@+id/caller_information"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="@dimen/call_details_top_margin"
+    android:paddingStart="@dimen/contact_container_padding_top_start"
+    android:paddingEnd="@dimen/contact_container_padding_bottom_end"
+    android:paddingTop="@dimen/contact_container_padding_top_start"
+    android:paddingBottom="@dimen/contact_container_padding_bottom_end"
+    android:baselineAligned="false"
+    android:orientation="horizontal"
+    android:focusable="true">
+
+    <QuickContactBadge
+      android:id="@+id/quick_contact_photo"
+      android:layout_width="@dimen/contact_photo_size"
+      android:layout_height="@dimen/contact_photo_size"
+      android:layout_gravity="center_vertical"
+      android:focusable="true" />
+
+    <LinearLayout
+      android:layout_width="0dp"
+      android:layout_height="wrap_content"
+      android:layout_weight="1"
+      android:layout_gravity="center_vertical"
+      android:orientation="vertical"
+      android:layout_marginStart="@dimen/photo_text_margin">
+
+      <TextView
+        android:id="@+id/caller_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/Dialer.TextAppearance.Primary" />
+
+      <TextView
+        android:id="@+id/caller_number"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/Dialer.TextAppearance.Secondary" />
+
+    </LinearLayout>
+
+    <ImageView
+      android:id="@+id/call_back_button"
+      android:layout_width="@dimen/call_back_button_size"
+      android:layout_height="@dimen/call_back_button_size"
+      android:background="?android:attr/selectableItemBackgroundBorderless"
+      android:src="@drawable/quantum_ic_call_white_24"
+      android:scaleType="center"
+      android:tint="?android:attr/textColorSecondary"
+      android:contentDescription="@string/call"
+      android:visibility="gone" />
+
+  </LinearLayout>
+
+  <ScrollView
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:orientation="vertical">
+
+      <!-- The actual details -->
+      <include layout="@layout/call_stats_detail_info" />
+
+      <View
+        android:id="@+id/separator"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_below="@id/out_average"
+        android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+        android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+        android:background="#12000000" />
+
+      <TextView
+        android:id="@+id/call_detail_action_copy"
+        style="@style/CallDetailsActionItemStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:drawableStart="@drawable/quantum_ic_content_copy_grey600_24"
+        android:text="@string/call_details_copy_number"/>
+
+      <TextView
+        android:id="@+id/call_detail_action_edit_before_call"
+        style="@style/CallDetailsActionItemStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:drawableStart="@drawable/quantum_ic_edit_grey600_24"
+        android:text="@string/call_details_edit_number"/>
+
+    </LinearLayout>
+
+  </ScrollView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml
new file mode 100644
index 0000000..4f4e59f
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+  android:id="@+id/call_stats_info"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:orientation="vertical"
+  android:paddingStart="@dimen/call_log_list_item_info_margin_start"
+  android:paddingEnd="@dimen/call_log_list_item_info_margin_start"
+  android:paddingTop="@dimen/call_log_outer_margin"
+  android:paddingBottom="@dimen/call_log_outer_margin">
+
+  <TextView
+    android:id="@+id/date_filter"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="@style/Dialer.TextAppearance.Secondary" />
+
+  <RelativeLayout
+    android:id="@+id/duration_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/date_filter">
+
+    <TextView
+      android:id="@+id/durations_header"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:text="@string/call_stats_title_durations"
+      android:layout_marginBottom="@dimen/call_log_vertical_padding"
+      android:textColor="?android:attr/colorAccent"
+      style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+    <TextView
+      android:id="@+id/total_duration_total"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentEnd="true"
+      android:layout_below="@id/durations_header"
+      style="@style/Dialer.TextAppearance.Primary" />
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentStart="true"
+      android:layout_toStartOf="@id/total_duration_total"
+      android:layout_alignBaseline="@id/total_duration_total"
+      android:text="@string/call_stats_title_of_total"
+      style="@style/Dialer.TextAppearance.Primary" />
+
+    <com.android.dialer.widget.LinearColorBar
+      android:id="@+id/duration_total_percent_bar"
+      android:layout_width="match_parent"
+      android:layout_below="@id/total_duration_total"
+      style="@style/CallStatsBarStyle" />
+
+    <TextView
+      android:id="@+id/total_duration_number"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentEnd="true"
+      android:layout_below="@id/duration_total_percent_bar"
+      style="@style/Dialer.TextAppearance.Primary" />
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_alignParentStart="true"
+      android:layout_toStartOf="@id/total_duration_number"
+      android:layout_alignBaseline="@id/total_duration_number"
+      android:text="@string/call_stats_title_for_number"
+      style="@style/Dialer.TextAppearance.Primary" />
+
+    <com.android.dialer.widget.LinearColorBar
+      android:id="@+id/duration_number_percent_bar"
+      android:layout_width="match_parent"
+      android:layout_below="@id/total_duration_number"
+      style="@style/CallStatsBarStyle" />
+
+    <include
+      android:id="@+id/in_duration"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_below="@id/duration_number_percent_bar"
+      android:layout_marginBottom="@dimen/call_log_icon_margin"
+      layout="@layout/call_stats_detail_line" />
+
+    <include
+      android:id="@+id/out_duration"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_below="@id/in_duration"
+      android:layout_marginBottom="@dimen/call_log_icon_margin"
+      layout="@layout/call_stats_detail_line" />
+
+  </RelativeLayout>
+
+  <TextView
+    android:id="@+id/count_header"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/duration_container"
+    android:layout_marginTop="16dp"
+    android:layout_marginBottom="@dimen/call_log_vertical_padding"
+    android:text="@string/call_stats_title_count"
+    android:textColor="?android:attr/colorAccent"
+    style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+  <TextView
+    android:id="@+id/total_count_total"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentEnd="true"
+    android:layout_below="@id/count_header"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <TextView
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentStart="true"
+    android:layout_toStartOf="@id/total_count_total"
+    android:layout_alignBaseline="@id/total_count_total"
+    android:text="@string/call_stats_title_of_total"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <com.android.dialer.widget.LinearColorBar
+    android:id="@+id/count_total_percent_bar"
+    android:layout_width="match_parent"
+    android:layout_below="@id/total_count_total"
+    style="@style/CallStatsBarStyle" />
+
+  <TextView
+    android:id="@+id/total_count_number"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentEnd="true"
+    android:layout_below="@id/count_total_percent_bar"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <TextView
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentStart="true"
+    android:layout_toStartOf="@id/total_count_number"
+    android:layout_alignBaseline="@id/total_count_number"
+    android:text="@string/call_stats_title_for_number"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <com.android.dialer.widget.LinearColorBar
+    android:id="@+id/count_number_percent_bar"
+    android:layout_width="match_parent"
+    android:layout_below="@id/total_count_number"
+    style="@style/CallStatsBarStyle" />
+
+  <include
+    android:id="@+id/in_count"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/count_number_percent_bar"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+  <include
+    android:id="@+id/out_count"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/in_count"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+  <include
+    android:id="@+id/missed_count"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/out_count"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+  <include
+    android:id="@+id/blocked_count"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/missed_count"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+  <TextView
+    android:id="@+id/average_header"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/blocked_count"
+    android:layout_marginTop="16dp"
+    android:layout_marginBottom="@dimen/call_log_vertical_padding"
+    android:text="@string/call_stats_title_average_duration"
+    android:textColor="?android:attr/colorAccent"
+    style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+  <include
+    android:id="@+id/in_average"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/average_header"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+  <include
+    android:id="@+id/out_average"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/in_average"
+    android:layout_marginBottom="@dimen/call_log_icon_margin"
+    layout="@layout/call_stats_detail_line" />
+
+</RelativeLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml
new file mode 100644
index 0000000..5219f07
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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">
+
+  <com.android.dialer.calllogutils.CallTypeIconsView
+    android:id="@+id/icon"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_vertical" />
+
+  <TextView
+    android:id="@+id/value"
+    android:layout_width="0dp"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:layout_marginStart="@dimen/call_log_icon_margin"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <TextView
+    android:id="@+id/percent"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginStart="@dimen/call_log_icon_margin"
+    style="@style/Dialer.TextAppearance.Secondary" />
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml b/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml
new file mode 100644
index 0000000..017c3a4
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<android.support.design.widget.CoordinatorLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:design="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  design:statusBarBackground="@null" >
+
+  <android.support.design.widget.AppBarLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:stateListAnimator="@null"
+    android:background="?android:attr/colorBackground"
+    android:elevation="2dp">
+
+    <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:orientation="vertical"
+      design:layout_scrollFlags="scroll|enterAlways">
+
+      <include layout="@layout/call_log_filter_spinners" />
+
+      <TextView
+        android:id="@+id/date_filter"
+        style="@style/ContactListSeparatorTextViewStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/call_log_outer_margin"
+        android:layout_marginEnd="@dimen/call_log_outer_margin"
+        android:visibility="gone" />
+
+      <TextView
+        android:id="@+id/sum_header"
+        style="@style/ContactListSeparatorTextViewStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/call_log_outer_margin"
+        android:layout_marginEnd="@dimen/call_log_outer_margin"
+        android:paddingBottom="@dimen/call_log_outer_margin"
+        android:visibility="gone" />
+    </LinearLayout>
+
+  </android.support.design.widget.AppBarLayout>
+
+  <android.support.v7.widget.RecyclerView
+    android:id="@+id/recycler_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingStart="@dimen/call_log_horizontal_margin"
+    android:paddingEnd="@dimen/call_log_horizontal_margin"
+    android:background="?android:attr/colorBackground"
+    design:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+  <com.android.dialer.widget.EmptyContentView
+    android:id="@+id/empty_list_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_vertical"
+    android:background="?android:attr/colorBackground"
+    android:visibility="gone" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml b/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml
new file mode 100644
index 0000000..18600f4
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2007 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.
+-->
+
+<RelativeLayout
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+  android:id="@+id/primary_action_view"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:paddingStart="@dimen/call_log_start_margin"
+  android:paddingEnd="@dimen/call_log_outer_margin"
+  android:paddingTop="@dimen/call_log_vertical_padding"
+  android:paddingBottom="@dimen/call_log_vertical_padding"
+  android:background="?android:attr/selectableItemBackground">
+
+  <QuickContactBadge
+    android:id="@+id/quick_contact_photo"
+    android:layout_width="@dimen/contact_photo_size"
+    android:layout_height="@dimen/contact_photo_size"
+    android:layout_marginEnd="@dimen/call_log_start_margin"
+    android:paddingTop="2dp" />
+
+  <TextView
+    android:id="@+id/name"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_toEndOf="@id/quick_contact_photo"
+    android:ellipsize="marquee"
+    android:singleLine="true"
+    style="@style/Dialer.TextAppearance.Primary" />
+
+  <TextView
+    android:id="@+id/percent"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_toEndOf="@id/name"
+    android:layout_alignBaseline="@id/name"
+    android:layout_alignParentEnd="true"
+    android:gravity="end"
+      style="@style/Dialer.TextAppearance.Secondary" />
+
+  <com.android.dialer.widget.LinearColorBar
+    android:id="@+id/percent_bar"
+    android:layout_width="wrap_content"
+    android:layout_below="@id/name"
+    android:layout_toEndOf="@id/quick_contact_photo"
+    android:layout_alignParentEnd="true"
+    style="@style/CallStatsBarStyle" />
+
+  <TextView
+    android:id="@+id/label"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/percent_bar"
+    android:layout_toEndOf="@id/quick_contact_photo"
+    android:layout_marginEnd="8dp"
+    android:singleLine="true"
+    style="@style/Dialer.TextAppearance.Secondary" />
+
+  <TextView
+    android:id="@+id/number"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_below="@id/percent_bar"
+    android:layout_toEndOf="@id/label"
+    android:layout_alignBaseline="@id/label"
+    android:singleLine="true"
+    android:textSize="12sp"
+    style="@style/Dialer.TextAppearance.Secondary" />
+
+</RelativeLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml b/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml
new file mode 100644
index 0000000..ec4a1f2
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2007, 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="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical" >
+
+    <Spinner
+        android:id="@+id/date_quick_selection"
+        android:layout_marginTop="3dp"
+        android:layout_marginBottom="3dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="-12dp"
+        android:layout_marginRight="3dp"
+        android:layout_marginTop="3dp"
+        android:text="@string/call_stats_filter_from"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <DatePicker
+        android:id="@+id/date_picker_from"
+        android:datePickerMode="spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:calendarViewShown="false"
+        android:spinnersShown="true" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="-12dp"
+        android:layout_marginRight="4dp"
+        android:text="@string/call_stats_filter_to"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <DatePicker
+        android:id="@+id/date_picker_to"
+        android:datePickerMode="spinner"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:calendarViewShown="false"
+        android:spinnersShown="true" />
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/menu/call_stats_options.xml b/java/com/android/dialer/callstats/res/menu/call_stats_options.xml
new file mode 100644
index 0000000..ae4b7eb
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/menu/call_stats_options.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+  <item
+    android:id="@+id/date_filter"
+    android:showAsAction="never"
+    android:title="@string/call_stats_date_filter" />
+
+  <item
+    android:id="@+id/reset_date_filter"
+    android:showAsAction="never"
+    android:visible="false"
+    android:title="@string/call_stats_reset_filter" />
+
+  <item
+    android:id="@+id/sort_by_duration"
+    android:showAsAction="never"
+    android:visible="false"
+    android:title="@string/call_stats_sort_by_duration" />
+
+  <item
+    android:id="@+id/sort_by_count"
+    android:showAsAction="never"
+    android:title="@string/call_stats_sort_by_count" />
+
+</menu>
diff --git a/java/com/android/dialer/callstats/res/values/cm_arrays.xml b/java/com/android/dialer/callstats/res/values/cm_arrays.xml
new file mode 100644
index 0000000..ee49865
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_arrays.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- 0:           sec
+         1:      min
+         2:      min  sec
+         3: hour
+         4: hour      sec
+         5: hour min
+         6: hour min  sec -->
+
+    <string-array name="call_stats_duration">
+        <item><xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+        <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item>
+        <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+        <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g></item>
+        <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+        <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item>
+        <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+    </string-array>
+
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/cm_plurals.xml b/java/com/android/dialer/callstats/res/values/cm_plurals.xml
new file mode 100644
index 0000000..536e3f8
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_plurals.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="hour">
+        <item quantity="one">1 hr</item>
+        <item quantity="other">%d hrs</item>
+    </plurals>
+    <plurals name="minute">
+        <item quantity="one">1 min</item>
+        <item quantity="other">%d mins</item>
+    </plurals>
+    <plurals name="second">
+        <item quantity="one">1 sec</item>
+        <item quantity="other">%d secs</item>
+    </plurals>
+
+    <plurals name="call">
+        <item quantity="one">1 call</item>
+        <item quantity="other">%d calls</item>
+    </plurals>
+</resources>
+
diff --git a/java/com/android/dialer/callstats/res/values/cm_strings.xml b/java/com/android/dialer/callstats/res/values/cm_strings.xml
new file mode 100644
index 0000000..0e3fd91
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_strings.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod Project
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="call_stats_detail_title">Contact statistics details</string>
+
+    <string name="call_stats_incoming">Incoming: <xliff:g id="value">%s</xliff:g></string>
+    <string name="call_stats_outgoing">Outgoing: <xliff:g id="value">%s</xliff:g></string>
+    <string name="call_stats_missed">Missed: <xliff:g id="value">%s</xliff:g></string>
+    <string name="call_stats_blocked">Blocked: <xliff:g id="value">%s</xliff:g></string>
+    <string name="call_stats_percent"><xliff:g id="percent">%d</xliff:g>%%</string>
+    <string name="call_stats_header_total">Total: <xliff:g id="call_count">%1$s</xliff:g>, <xliff:g id="duration">%2$s</xliff:g></string>
+    <string name="call_stats_header_total_callsonly">Total: <xliff:g id="call_count">%s</xliff:g></string>
+    <string name="call_stats_filter_from">Start date</string>
+    <string name="call_stats_filter_to">End date</string>
+    <string name="call_stats_filter_picker_title">Filter range</string>
+
+    <string name="date_quick_selection">Quick selection</string>
+    <string name="date_qs_currentmonth">Current month</string>
+    <string name="date_qs_currentquarter">Current quarter</string>
+    <string name="date_qs_currentyear">Current year</string>
+    <string name="date_qs_lastweek">Last week</string>
+    <string name="date_qs_lastmonth">Last month</string>
+    <string name="date_qs_lastquarter">Last quarter</string>
+    <string name="date_qs_lastyear">Last year</string>
+
+    <string name="call_stats_date_filter">Adjust time range</string>
+    <string name="call_stats_reset_filter">Reset time range</string>
+    <string name="call_stats_sort_by_duration">Sort by call duration</string>
+    <string name="call_stats_sort_by_count">Sort by call count</string>
+
+    <string name="call_stats_title_for_number">This number</string>
+    <string name="call_stats_title_of_total">Of total</string>
+    <string name="call_stats_title_durations">Call durations</string>
+    <string name="call_stats_title_count">Call count</string>
+    <string name="call_stats_title_average_duration">Average call duration</string>
+
+    <!-- Text displayed when there are no call log entries in the selected time range. -->
+    <string name="recent_calls_no_items_in_range">Your call log does not contain any calls in the selected time range.</string>
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/colors.xml b/java/com/android/dialer/callstats/res/values/colors.xml
new file mode 100644
index 0000000..40472cf
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/colors.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2018 The LineageOS 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
+-->
+
+<resources>
+  <!-- Colors for incoming and outgoing calls in the call statistics -->
+  <color name="call_stats_bar_background">#88888888</color>
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/styles.xml b/java/com/android/dialer/callstats/res/values/styles.xml
new file mode 100644
index 0000000..3aae797
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/styles.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright (C) 2018 The LineageOS 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
+  -->
+
+<resources>
+  <style name="CallStatsBarStyle">
+    <item name="android:layout_height">4dp</item>
+    <item name="android:layout_marginTop">6dp</item>
+    <item name="android:layout_marginBottom">6dp</item>
+    <item name="android:orientation">horizontal</item>
+    <item name="backgroundColor">@color/call_stats_bar_background</item>
+    <item name="blueColor">@color/answered_incoming_call</item>
+    <item name="greenColor">@color/answered_outgoing_call</item>
+    <item name="redColor">@color/missed_call</item>
+    <item name="orangeColor">@color/blocked_call</item>
+  </style>
+
+  <style name="TextAppearance.CallStatsCategoryTitle" parent="TextAppearance.AppCompat.Body2">
+    <item name="android:textAllCaps">true</item>
+    <item name="android:textSize">11sp</item>
+    <!-- 0.8 Spacing, 0.8/11 = 0.072727273 -->
+    <item name="android:letterSpacing">0.072727273</item>
+  </style>
+</resources>
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
index 719492e..9d26398 100644
--- a/java/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -280,6 +280,34 @@
     db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
   }
 
+  @Override
+  public void onDowngrade(SQLiteDatabase db, int oldNumber, int newNumber) {
+    // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
+    // our own from the database.
+
+    int oldVersion;
+
+    oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
+
+    if (oldVersion == 0) {
+      LogUtil.e(
+          "DialerDatabaseHelper.onDowngrade", "malformed database version..recreating database");
+      setupTables(db);
+      return;
+    }
+
+    if (oldVersion == 70011) {
+      oldVersion = 10;
+    }
+
+    if (oldVersion != DATABASE_VERSION) {
+      throw new IllegalStateException(
+          "error downgrading the database to version " + DATABASE_VERSION);
+    }
+
+    setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+  }
+
   /** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */
   public void setProperty(String key, String value) {
     setProperty(getWritableDatabase(), key, value);
diff --git a/java/com/android/dialer/dialpadview/DialpadFragment.java b/java/com/android/dialer/dialpadview/DialpadFragment.java
index 12c82c9..234457e 100644
--- a/java/com/android/dialer/dialpadview/DialpadFragment.java
+++ b/java/com/android/dialer/dialpadview/DialpadFragment.java
@@ -50,6 +50,7 @@
 import android.support.design.widget.FloatingActionButton;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.ServiceState;
@@ -64,6 +65,7 @@
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
@@ -204,6 +206,8 @@
   private boolean isLayoutRtl;
   private boolean isLandscape;
 
+  private PhoneAccountHandle selectedAccount;
+
   private DialerExecutor<String> initPhoneNumberFormattingTextWatcherExecutor;
   private boolean isDialpadSlideUp;
 
@@ -1021,6 +1025,37 @@
                 item.setVisible(CallUtil.isCallWithSubjectSupported(getContext()));
               }
             }
+
+            final MenuItem callWithItem = menu.findItem(R.id.call_with);
+            List<PhoneAccount> accounts =
+                CallUtil.getCallCapablePhoneAccounts(getContext(), PhoneAccount.SCHEME_TEL);
+            if (accounts != null && accounts.size() > 1) {
+              final PhoneAccountHandle selected;
+              if (selectedAccount != null) {
+                selected = selectedAccount;
+              } else {
+                selected = TelecomUtil.getDefaultOutgoingPhoneAccount(getContext(),
+                    PhoneAccount.SCHEME_TEL);
+              }
+
+              SubMenu callWithMenu = callWithItem.getSubMenu();
+              callWithMenu.clear();
+
+              for (PhoneAccount account : accounts) {
+                final PhoneAccountHandle handle = account.getAccountHandle();
+                final Intent intent = new Intent()
+                    .putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle);
+
+                callWithMenu.add(Menu.FIRST, Menu.NONE, Menu.NONE, account.getLabel())
+                    .setIntent(intent)
+                    .setChecked(handle.equals(selected));
+              }
+              callWithMenu.setGroupCheckable(Menu.FIRST, true, true);
+              callWithItem.setVisible(callWithMenu.hasVisibleItems());
+            } else {
+              callWithItem.setVisible(false);
+            }
+
             super.show();
           }
         };
@@ -1186,7 +1221,9 @@
         // Clear the digits just in case.
         clearDialpad();
       } else {
-        PreCall.start(getContext(), new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD));
+        CallIntentBuilder builder = new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD)
+            .setPhoneAccountHandle(selectedAccount);
+        PreCall.start(getContext(), builder);
         hideAndClearDialpad();
       }
     }
@@ -1196,6 +1233,7 @@
     if (digits != null) {
       digits.getText().clear();
     }
+    selectedAccount = null;
   }
 
   private void handleDialButtonClickWithEmptyDigits() {
@@ -1420,6 +1458,11 @@
 
   @Override
   public boolean onMenuItemClick(MenuItem item) {
+    if (item.getGroupId() == Menu.FIRST) {
+      Intent intent = item.getIntent();
+      selectedAccount = intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+      return true;
+    }
     int resId = item.getItemId();
     if (resId == R.id.menu_2s_pause) {
       updateDialString(PAUSE);
diff --git a/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml b/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
index 2921ea3..760a004 100644
--- a/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
+++ b/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
@@ -14,7 +14,11 @@
      limitations under the License.
 -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
-
+  <item
+    android:id="@+id/call_with"
+    android:title="@string/call_via">
+    <menu />
+  </item>
   <item
     android:id="@+id/menu_2s_pause"
     android:showAsAction="withText"
diff --git a/java/com/android/dialer/lookup/AndroidManifest.xml b/java/com/android/dialer/lookup/AndroidManifest.xml
new file mode 100644
index 0000000..0a278db
--- /dev/null
+++ b/java/com/android/dialer/lookup/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.dialer.lookup">
+
+  <uses-sdk android:minSdkVersion="23"/>
+  <uses-permission android:name="lineageos.permission.WRITE_SETTINGS"/>
+
+  <application>
+    <provider android:name="com.android.dialer.lookup.LookupProvider"
+      android:authorities="com.android.dialer.lookup"
+      android:exported="false"
+      android:multiprocess="false" />
+
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/lookup/ContactBuilder.java b/java/com/android/dialer/lookup/ContactBuilder.java
new file mode 100644
index 0000000..e88f956
--- /dev/null
+++ b/java/com/android/dialer/lookup/ContactBuilder.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.w3c.dom.Text;
+
+import java.sql.Struct;
+import java.util.ArrayList;
+
+public class ContactBuilder {
+  private static final String TAG = ContactBuilder.class.getSimpleName();
+
+  private static final boolean DEBUG = false;
+
+  /** Default photo for businesses if no other image is found */
+  public static final String PHOTO_URI_BUSINESS = new Uri.Builder()
+      .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+      .authority("com.android.dialer")
+      .appendPath(String.valueOf(R.drawable.ic_places_picture_180_holo_light))
+      .build()
+      .toString();
+
+  private final ArrayList<Address> addresses = new ArrayList<>();
+  private final ArrayList<PhoneNumber> phoneNumbers = new ArrayList<>();
+  private final ArrayList<WebsiteUrl> websites = new ArrayList<>();
+
+  private final long directoryId;
+  private Name name;
+  private final String normalizedNumber;
+  private final String formattedNumber;
+  private Uri photoUri;
+
+  public static ContactBuilder forForwardLookup(String number) {
+      return new ContactBuilder(DirectoryId.NEARBY, null, number);
+  }
+
+  public static ContactBuilder forPeopleLookup(String number) {
+      return new ContactBuilder(DirectoryId.PEOPLE, null, number);
+  }
+
+  public static ContactBuilder forReverseLookup(String normalizedNumber, String formattedNumber) {
+      return new ContactBuilder(DirectoryId.NULL, normalizedNumber, formattedNumber);
+  }
+
+  private ContactBuilder(long directoryId, String normalizedNumber, String formattedNumber) {
+    this.directoryId = directoryId;
+    this.normalizedNumber = normalizedNumber;
+    this.formattedNumber = formattedNumber;
+  }
+
+  public ContactBuilder(Uri encodedContactUri) throws JSONException {
+    String jsonData = encodedContactUri.getEncodedFragment();
+    String directoryIdStr = encodedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+    long directoryId = DirectoryId.DEFAULT;
+
+    if (!TextUtils.isEmpty(directoryIdStr)) {
+      try {
+        directoryId = Long.parseLong(directoryIdStr);
+      } catch (NumberFormatException e) {
+        Log.e(TAG, "Error parsing directory id of uri " + encodedContactUri, e);
+      }
+    }
+
+    this.directoryId = directoryId;
+    this.formattedNumber = null;
+    this.normalizedNumber = null;
+
+    try {
+      // name
+      JSONObject json = new JSONObject(jsonData);
+      JSONObject contact = json.optJSONObject(Contacts.CONTENT_ITEM_TYPE);
+      JSONObject nameObj = contact.optJSONObject(StructuredName.CONTENT_ITEM_TYPE);
+      name = new Name(nameObj);
+
+      if (contact != null) {
+        // numbers
+        if (contact.has(Phone.CONTENT_ITEM_TYPE)) {
+          String phoneData = contact.getString(Phone.CONTENT_ITEM_TYPE);
+          Object phoneObject = new JSONTokener(phoneData).nextValue();
+          JSONArray phoneNumbersJson;
+          if (phoneObject instanceof JSONObject) {
+            phoneNumbersJson = new JSONArray();
+            phoneNumbersJson.put(phoneObject);
+          } else {
+            phoneNumbersJson = contact.getJSONArray(Phone.CONTENT_ITEM_TYPE);
+          }
+          for (int i = 0; i < phoneNumbersJson.length(); ++i) {
+            JSONObject phoneObj = phoneNumbersJson.getJSONObject(i);
+            phoneNumbers.add(new PhoneNumber(phoneObj));
+          }
+        }
+
+        // address
+        if (contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) {
+          JSONArray addressesJson = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE);
+          for (int i = 0; i < addressesJson.length(); ++i) {
+            JSONObject addrObj = addressesJson.getJSONObject(i);
+            addresses.add(new Address(addrObj));
+          }
+        }
+
+        // websites
+        if (contact.has(Website.CONTENT_ITEM_TYPE)) {
+          JSONArray websitesJson = contact.getJSONArray(Website.CONTENT_ITEM_TYPE);
+          for (int i = 0; i < websitesJson.length(); ++i) {
+            JSONObject websiteObj = websitesJson.getJSONObject(i);
+            final WebsiteUrl websiteUrl = new WebsiteUrl(websiteObj);
+            if (!TextUtils.isEmpty(websiteUrl.url)) {
+              websites.add(new WebsiteUrl(websiteObj));
+            }
+          }
+        }
+      }
+    } catch(JSONException e) {
+      Log.e(TAG, "Error parsing encoded fragment of uri " + encodedContactUri, e);
+      throw e;
+    }
+  }
+
+  public ContactBuilder addAddress(Address address) {
+    if (DEBUG) Log.d(TAG, "Adding address");
+    if (address != null) {
+      addresses.add(address);
+    }
+    return this;
+  }
+
+  public ContactBuilder addPhoneNumber(PhoneNumber phoneNumber) {
+    if (DEBUG) Log.d(TAG, "Adding phone number");
+    if (phoneNumber != null) {
+      phoneNumbers.add(phoneNumber);
+    }
+    return this;
+  }
+
+  public ContactBuilder addWebsite(WebsiteUrl website) {
+    if (DEBUG) Log.d(TAG, "Adding website");
+    if (website != null) {
+      websites.add(website);
+    }
+    return this;
+  }
+
+  public ContactBuilder setName(Name name) {
+    if (DEBUG) Log.d(TAG, "Setting name");
+    if (name != null) {
+      this.name = name;
+    }
+    return this;
+  }
+
+  public ContactBuilder setPhotoUri(String photoUri) {
+    if (photoUri != null) {
+      setPhotoUri(Uri.parse(photoUri));
+    }
+    return this;
+  }
+
+  public ContactBuilder setPhotoUri(Uri photoUri) {
+    if (DEBUG) Log.d(TAG, "Setting photo URI");
+    this.photoUri = photoUri;
+    return this;
+  }
+
+  public ContactInfo build() {
+    if (name == null) {
+      throw new IllegalStateException("Name has not been set");
+    }
+
+    // Use the incoming call's phone number if no other phone number
+    // is specified. The reverse lookup source could present the phone
+    // number differently (eg. without the area code).
+    if (phoneNumbers.isEmpty()) {
+      PhoneNumber pn = new PhoneNumber();
+      // Use the formatted number where possible
+      pn.number = formattedNumber != null
+              ? formattedNumber : normalizedNumber;
+      pn.type = Phone.TYPE_MAIN;
+      addPhoneNumber(pn);
+    }
+
+    try {
+      JSONObject contact = new JSONObject();
+
+      // Insert the name
+      contact.put(StructuredName.CONTENT_ITEM_TYPE, name.getJsonObject());
+
+      // Insert phone numbers
+      JSONArray phoneNumbersJson = new JSONArray();
+      for (PhoneNumber number : phoneNumbers) {
+        phoneNumbersJson.put(number.getJsonObject());
+      }
+      contact.put(Phone.CONTENT_ITEM_TYPE, phoneNumbersJson);
+
+      // Insert addresses if there are any
+      if (!addresses.isEmpty()) {
+        JSONArray addressesJson = new JSONArray();
+        for (Address address : addresses) {
+          addressesJson.put(address.getJsonObject());
+        }
+        contact.put(StructuredPostal.CONTENT_ITEM_TYPE, addressesJson);
+      }
+
+      // Insert websites if there are any
+      if (!websites.isEmpty()) {
+        JSONArray websitesJson = new JSONArray();
+        for (WebsiteUrl site : websites) {
+          websitesJson.put(site.getJsonObject());
+        }
+        contact.put(Website.CONTENT_ITEM_TYPE, websitesJson);
+      }
+
+      ContactInfo info = new ContactInfo();
+      info.name = name.displayName;
+      info.normalizedNumber = normalizedNumber;
+      info.number = phoneNumbers.get(0).number;
+      info.type = phoneNumbers.get(0).type;
+      info.label = phoneNumbers.get(0).label;
+      info.photoUri = photoUri;
+
+      String json = new JSONObject()
+          .put(Contacts.DISPLAY_NAME, name.displayName)
+          .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.ORGANIZATION)
+          .put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT)
+          .put(Contacts.CONTENT_ITEM_TYPE, contact)
+          .toString();
+
+      if (json != null) {
+        info.lookupUri = Contacts.CONTENT_LOOKUP_URI
+            .buildUpon()
+            .appendPath(Constants.LOOKUP_URI_ENCODED)
+            .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                String.valueOf(directoryId))
+            .encodedFragment(json)
+            .build();
+      }
+
+      return info;
+    } catch (JSONException e) {
+      Log.e(TAG, "Failed to build contact", e);
+      return null;
+    }
+  }
+
+  // android.provider.ContactsContract.CommonDataKinds.StructuredPostal
+  public static class Address {
+    public String formattedAddress;
+    public int type;
+    public String label;
+    public String street;
+    public String poBox;
+    public String neighborhood;
+    public String city;
+    public String region;
+    public String postCode;
+    public String country;
+
+    public static Address createFormattedHome(String address) {
+      if (address == null) {
+        return null;
+      }
+      Address a = new Address();
+      a.formattedAddress = address;
+      a.type = StructuredPostal.TYPE_HOME;
+      return a;
+    }
+
+    public JSONObject getJsonObject() throws JSONException {
+      JSONObject json = new JSONObject();
+      json.putOpt(StructuredPostal.FORMATTED_ADDRESS, formattedAddress);
+      json.put(StructuredPostal.TYPE, type);
+      json.putOpt(StructuredPostal.LABEL, label);
+      json.putOpt(StructuredPostal.STREET, street);
+      json.putOpt(StructuredPostal.POBOX, poBox);
+      json.putOpt(StructuredPostal.NEIGHBORHOOD, neighborhood);
+      json.putOpt(StructuredPostal.CITY, city);
+      json.putOpt(StructuredPostal.REGION, region);
+      json.putOpt(StructuredPostal.POSTCODE, postCode);
+      json.putOpt(StructuredPostal.COUNTRY, country);
+      return json;
+    }
+
+    public Address() {}
+
+    public Address(JSONObject json) throws JSONException {
+      if (json.has(StructuredPostal.FORMATTED_ADDRESS)) {
+        formattedAddress = json.getString(StructuredPostal.FORMATTED_ADDRESS);
+      }
+    }
+
+    public String toString() {
+      return "formattedAddress: " + formattedAddress + "; " +
+          "type: " + type + "; " +
+          "label: " + label + "; " +
+          "street: " + street + "; " +
+          "poBox: " + poBox + "; " +
+          "neighborhood: " + neighborhood + "; " +
+          "city: " + city + "; " +
+          "region: " + region + "; " +
+          "postCode: " + postCode + "; " +
+          "country: " + country;
+    }
+  }
+
+  // android.provider.ContactsContract.CommonDataKinds.StructuredName
+  public static class Name {
+    public String displayName;
+    public String givenName;
+    public String familyName;
+    public String prefix;
+    public String middleName;
+    public String suffix;
+    public String phoneticGivenName;
+    public String phoneticMiddleName;
+    public String phoneticFamilyName;
+
+    public static Name createDisplayName(String displayName) {
+      Name name = new Name();
+      name.displayName = displayName;
+      return name;
+    }
+
+    public JSONObject getJsonObject() throws JSONException {
+      JSONObject json = new JSONObject();
+      json.putOpt(StructuredName.DISPLAY_NAME, displayName);
+      json.putOpt(StructuredName.GIVEN_NAME, givenName);
+      json.putOpt(StructuredName.FAMILY_NAME, familyName);
+      json.putOpt(StructuredName.PREFIX, prefix);
+      json.putOpt(StructuredName.MIDDLE_NAME, middleName);
+      json.putOpt(StructuredName.SUFFIX, suffix);
+      json.putOpt(StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName);
+      json.putOpt(StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName);
+      json.putOpt(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+      return json;
+    }
+
+    public Name(JSONObject json) throws JSONException {
+      if (json != null) {
+        displayName = json.optString(StructuredName.DISPLAY_NAME, null);
+      }
+    }
+
+    public Name() {}
+
+    public String toString() {
+      return "displayName: " + displayName + "; " +
+          "givenName: " + givenName + "; " +
+          "familyName: " + familyName + "; " +
+          "prefix: " + prefix + "; " +
+          "middleName: " + middleName + "; " +
+          "suffix: " + suffix + "; " +
+          "phoneticGivenName: " + phoneticGivenName + "; " +
+          "phoneticMiddleName: " + phoneticMiddleName + "; " +
+          "phoneticFamilyName: " + phoneticFamilyName;
+    }
+  }
+
+  // android.provider.ContactsContract.CommonDataKinds.Phone
+  public static class PhoneNumber {
+    public String number;
+    public int type;
+    public String label;
+
+    public static PhoneNumber createMainNumber(String number) {
+      PhoneNumber n = new PhoneNumber();
+      n.number = number;
+      n.type = Phone.TYPE_MAIN;
+      return n;
+    }
+
+    public JSONObject getJsonObject() throws JSONException {
+      JSONObject json = new JSONObject();
+      json.put(Phone.NUMBER, number);
+      json.put(Phone.TYPE, type);
+      json.putOpt(Phone.LABEL, label);
+      return json;
+    }
+
+    public PhoneNumber(JSONObject json) throws JSONException {
+      number = json.getString(Phone.NUMBER);
+      type = json.getInt(Phone.TYPE);
+      if (json.has(Phone.LABEL)) {
+        label = json.getString(Phone.LABEL);
+      }
+    }
+
+    public PhoneNumber() {}
+
+    public String toString() {
+      return "number: " + number + "; " +
+          "type: " + type + "; " +
+          "label: " + label;
+    }
+  }
+
+  // android.provider.ContactsContract.CommonDataKinds.Website
+  public static class WebsiteUrl {
+    public String url;
+    public int type;
+    public String label;
+
+    public static WebsiteUrl createProfile(String url) {
+      if (url == null) {
+        return null;
+      }
+      WebsiteUrl u = new WebsiteUrl();
+      u.url = url;
+      u.type = Website.TYPE_PROFILE;
+      return u;
+    }
+
+    public JSONObject getJsonObject() throws JSONException {
+      JSONObject json = new JSONObject();
+      json.put(Website.URL, url);
+      json.put(Website.TYPE, type);
+      json.putOpt(Website.LABEL, label);
+      return json;
+    }
+
+    public WebsiteUrl() {}
+
+    public WebsiteUrl(JSONObject json) throws JSONException {
+      if (json.has(Website.URL)) {
+        url = json.getString(Website.URL);
+      }
+      if (json.has(Website.TYPE)) {
+        type = json.getInt(Website.TYPE);
+      }
+      if (json.has(Website.LABEL)) {
+        label = json.getString(Website.LABEL);
+      }
+    }
+
+    public String toString() {
+      return "url: " + url + "; " +
+          "type: " + type + "; " +
+          "label: " + label;
+    }
+  }
+}
diff --git a/java/com/android/dialer/lookup/DirectoryId.java b/java/com/android/dialer/lookup/DirectoryId.java
new file mode 100644
index 0000000..023585c
--- /dev/null
+++ b/java/com/android/dialer/lookup/DirectoryId.java
@@ -0,0 +1,33 @@
+package com.android.dialer.lookup;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+public class DirectoryId {
+  // default contacts directory
+  public static final long DEFAULT = ContactsContract.Directory.DEFAULT;
+
+  // id for a non existant directory
+  public static final long NULL = Long.MAX_VALUE;
+
+  // id for nearby forward lookup results (not a real directory)
+  public static final long NEARBY = NULL - 1;
+
+  // id for people forward lookup results (not a real directory)
+  public static final long PEOPLE = NULL - 2;
+
+  public static boolean isFakeDirectory(long directory) {
+    return directory == NULL || directory == NEARBY || directory == PEOPLE;
+  }
+
+  public static long fromUri(Uri lookupUri) {
+    long directory = DirectoryId.DEFAULT;
+    if (lookupUri != null) {
+      String dqp = lookupUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+      if (dqp != null) {
+        directory = Long.valueOf(dqp);
+      }
+    }
+    return directory;
+  }
+}
diff --git a/java/com/android/dialer/lookup/ForwardLookup.java b/java/com/android/dialer/lookup/ForwardLookup.java
new file mode 100644
index 0000000..2f59aeb
--- /dev/null
+++ b/java/com/android/dialer/lookup/ForwardLookup.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.Context;
+import android.location.Location;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.google.GoogleForwardLookup;
+import com.android.dialer.lookup.openstreetmap.OpenStreetMapForwardLookup;
+
+import java.util.List;
+
+public abstract class ForwardLookup {
+  private static final String TAG = ForwardLookup.class.getSimpleName();
+
+  private static ForwardLookup INSTANCE = null;
+
+  public static ForwardLookup getInstance(Context context) {
+    String provider = LookupSettings.getForwardLookupProvider(context);
+
+    if (INSTANCE == null || !isInstance(provider)) {
+      Log.d(TAG, "Chosen forward lookup provider: " + provider);
+
+      if (provider.equals(LookupSettings.FLP_GOOGLE)) {
+        INSTANCE = new GoogleForwardLookup(context);
+      } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP)) {
+        INSTANCE = new OpenStreetMapForwardLookup(context);
+      }
+    }
+
+    return INSTANCE;
+  }
+
+  private static boolean isInstance(String provider) {
+    if (provider.equals(LookupSettings.FLP_GOOGLE)
+        && INSTANCE instanceof GoogleForwardLookup) {
+      return true;
+    } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP)
+        && INSTANCE instanceof OpenStreetMapForwardLookup) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  public abstract List<ContactInfo> lookup(Context context, String filter, Location lastLocation);
+}
diff --git a/java/com/android/dialer/lookup/LookupCache.java b/java/com/android/dialer/lookup/LookupCache.java
new file mode 100644
index 0000000..6fe3a9f
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupCache.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chenxiaolong@cxl.epac.to>
+ *
+ * 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.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.DialerUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LookupCache {
+  private static final String TAG = LookupCache.class.getSimpleName();
+
+  public static final String NAME = "Name";
+  public static final String TYPE = "Type";
+  public static final String LABEL = "Label";
+  public static final String NUMBER = "Number";
+  public static final String FORMATTED_NUMBER = "FormattedNumber";
+  public static final String NORMALIZED_NUMBER = "NormalizedNumber";
+  public static final String PHOTO_ID = "PhotoID";
+  public static final String LOOKUP_URI = "LookupURI";
+
+  public static boolean hasCachedContact(Context context, String number) {
+    String normalizedNumber = formatE164(context, number);
+    if (normalizedNumber == null) {
+        return false;
+    }
+
+    File file = getFilePath(context, normalizedNumber);
+    return file.exists();
+  }
+
+  public static void cacheContact(Context context, ContactInfo info) {
+    File file = getFilePath(context, info.normalizedNumber);
+
+    if (file.exists()) {
+      file.delete();
+    }
+
+    FileOutputStream out = null;
+    JsonWriter writer = null;
+
+    try {
+      out = new FileOutputStream(file);
+      writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+      writer.setIndent("  ");
+      List messages = new ArrayList();
+
+      writer.beginObject();
+      if (info.name != null) {
+        writer.name(NAME).value(info.name);
+      }
+      writer.name(TYPE).value(info.type);
+      if (info.label != null) {
+        writer.name(LABEL).value(info.label);
+      }
+      if (info.number != null) {
+        writer.name(NUMBER).value(info.number);
+      }
+      if (info.formattedNumber != null) {
+          writer.name(FORMATTED_NUMBER).value(info.formattedNumber);
+      }
+      if (info.normalizedNumber != null) {
+          writer.name(NORMALIZED_NUMBER).value(info.normalizedNumber);
+      }
+      writer.name(PHOTO_ID).value(info.photoId);
+
+      if (info.lookupUri != null) {
+          writer.name(LOOKUP_URI).value(info.lookupUri.toString());
+      }
+
+      // We do not save the photo URI. If there's a cached image, that
+      // will be used when the contact is retrieved. Otherwise, photoUri
+      // will be set to null.
+
+      writer.endObject();
+    } catch (IOException e) {
+      e.printStackTrace();
+    } finally {
+      DialerUtils.closeQuietly(writer);
+      DialerUtils.closeQuietly(out);
+    }
+  }
+
+  public static ContactInfo getCachedContact(Context context, String number) {
+    String normalizedNumber = formatE164(context, number);
+    if (normalizedNumber == null) {
+      return null;
+    }
+
+    File file = getFilePath(context, normalizedNumber);
+    if (!file.exists()) {
+      // Whatever is calling this should probably check anyway
+      return null;
+    }
+
+    ContactInfo info = new ContactInfo();
+
+    FileInputStream in = null;
+    JsonReader reader = null;
+
+    try {
+      in = new FileInputStream(file);
+      reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        String name = reader.nextName();
+
+        if (NAME.equals(name)) {
+          info.name = reader.nextString();
+        } else if (TYPE.equals(name)) {
+          info.type = reader.nextInt();
+        } else if (LABEL.equals(name)) {
+          info.label = reader.nextString();
+        } else if (NUMBER.equals(name)) {
+          info.number = reader.nextString();
+        } else if (FORMATTED_NUMBER.equals(name)) {
+          info.formattedNumber = reader.nextString();
+        } else if (NORMALIZED_NUMBER.equals(name)) {
+          info.normalizedNumber = reader.nextString();
+        } else if (PHOTO_ID.equals(name)) {
+          info.photoId = reader.nextInt();
+        } else if (LOOKUP_URI.equals(name)) {
+          Uri lookupUri = Uri.parse(reader.nextString());
+
+          if (hasCachedImage(context, normalizedNumber)) {
+            // Insert cached photo URI
+            Uri image = Uri.withAppendedPath(LookupProvider.IMAGE_CACHE_URI,
+                Uri.encode(normalizedNumber));
+
+            String json = lookupUri.getEncodedFragment();
+            if (json != null) {
+              try {
+                JSONObject jsonObj = new JSONObject(json);
+                jsonObj.putOpt(Contacts.PHOTO_URI, image.toString());
+                lookupUri = lookupUri.buildUpon()
+                    .encodedFragment(jsonObj.toString())
+                    .build();
+              } catch (JSONException e) {
+                Log.e(TAG, "Failed to add image URI to json", e);
+              }
+            }
+
+            info.photoUri = image;
+          }
+
+          info.lookupUri = lookupUri;
+        }
+      }
+      reader.endObject();
+    } catch (IOException e) {
+      e.printStackTrace();
+    } finally {
+      DialerUtils.closeQuietly(reader);
+      DialerUtils.closeQuietly(in);
+    }
+
+    return info;
+  }
+
+  public static void deleteCachedContacts(Context context) {
+    File dir = new File(context.getCacheDir(), "lookup");
+    if (!dir.exists()) {
+      Log.v(TAG, "Lookup cache directory does not exist. Not clearing it.");
+      return;
+    }
+
+    if (!dir.isDirectory()) {
+      Log.e(TAG, "Path " + dir + " is not a directory");
+      return;
+    }
+
+    File[] files = dir.listFiles();
+    if (files != null) {
+      for (File file : files) {
+        if (file.isFile()) {
+          file.delete();
+        }
+      }
+    }
+  }
+
+  public static void deleteCachedContact(Context context, String normalizedNumber) {
+    File f = getFilePath(context, normalizedNumber);
+    if (f.exists()) {
+      f.delete();
+    }
+
+    f = getImagePath(context, normalizedNumber);
+    if (f.exists()) {
+      f.delete();
+    }
+  }
+
+  public static boolean hasCachedImage(Context context, String number) {
+    String normalizedNumber = formatE164(context, number);
+    if (normalizedNumber == null) {
+      return false;
+    }
+
+    File file = getImagePath(context, normalizedNumber);
+    return file.exists();
+  }
+
+  public static Uri cacheImage(Context context, String normalizedNumber, Bitmap bmp) {
+    // Compress the cached images to save space
+    if (bmp == null) {
+      Log.e(TAG, "Failed to cache image");
+      return null;
+    }
+
+    File image = getImagePath(context, normalizedNumber);
+    FileOutputStream out = null;
+
+    try {
+      out = new FileOutputStream(image);
+      bmp.compress(Bitmap.CompressFormat.WEBP, 100, out);
+      return Uri.fromFile(image);
+    } catch (Exception e) {
+      e.printStackTrace();
+    } finally {
+      DialerUtils.closeQuietly(out);
+    }
+    return null;
+  }
+
+  public static Bitmap getCachedImage(Context context, String normalizedNumber) {
+    File image = getImagePath(context, normalizedNumber);
+    if (!image.exists()) {
+      return null;
+    }
+
+    BitmapFactory.Options options = new BitmapFactory.Options();
+    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+    return BitmapFactory.decodeFile(image.getPath(), options);
+  }
+
+  private static String formatE164(Context context, String number) {
+    TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+    String countryIso = tm.getSimCountryIso().toUpperCase();
+    return PhoneNumberUtils.formatNumberToE164(number, countryIso);
+  }
+
+  private static File getFilePath(Context context, String normalizedNumber) {
+    File dir = new File(context.getCacheDir(), "lookup");
+    if (!dir.exists()) {
+      dir.mkdirs();
+    }
+
+    return new File(dir, normalizedNumber + ".json");
+  }
+
+  public static File getImagePath(Context context, String normalizedNumber) {
+    File dir = new File(context.getCacheDir(), "lookup");
+    if (!dir.exists()) {
+      dir.mkdirs();
+    }
+
+    return new File(dir, normalizedNumber + ".webp");
+  }
+}
diff --git a/java/com/android/dialer/lookup/LookupCacheService.java b/java/com/android/dialer/lookup/LookupCacheService.java
new file mode 100644
index 0000000..43dc9f0
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupCacheService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+
+import com.android.dialer.logging.ContactSource;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.ContactInfo;
+
+import java.io.InputStream;
+
+public class LookupCacheService implements CachedNumberLookupService {
+  @Override
+  public CachedContactInfo buildCachedContactInfo(ContactInfo info) {
+    return new LookupCachedContactInfo(info);
+  }
+
+  @Override
+  public void addContact(Context context, CachedContactInfo cachedInfo) {
+    LookupCache.cacheContact(context, cachedInfo.getContactInfo());
+  }
+
+  @Override
+  public CachedContactInfo lookupCachedContactFromNumber(Context context, String number) {
+    ContactInfo info = LookupCache.getCachedContact(context, number);
+    return info != null ? new LookupCachedContactInfo(info) : null;
+  }
+
+  @Override
+  public void clearAllCacheEntries(Context context) {
+    LookupCache.deleteCachedContacts(context);
+  }
+
+  @Override
+  public boolean isBusiness(ContactSource.Type sourceType) {
+    // We don't store source type, so assume false
+    return false;
+  }
+
+  @Override
+  public boolean canReportAsInvalid(ContactSource.Type sourceType, String objectId) {
+    return false;
+  }
+
+  @Override
+  public boolean reportAsInvalid(Context context, CachedContactInfo cachedContactInfo) {
+    return false;
+  }
+
+  @Override
+  public @Nullable Uri addPhoto(Context context, String number, InputStream in) {
+    TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+    String countryIso = tm.getSimCountryIso().toUpperCase();
+    String normalized = number != null
+        ? PhoneNumberUtils.formatNumberToE164(number, countryIso) : null;
+    if (normalized != null) {
+      Bitmap bitmap = BitmapFactory.decodeStream(in, null, null);
+      if (bitmap != null) {
+        return LookupCache.cacheImage(context, normalized, bitmap);
+      }
+    }
+    return null;
+  }
+
+  private static class LookupCachedContactInfo implements CachedContactInfo {
+    private final ContactInfo info;
+
+    private LookupCachedContactInfo(ContactInfo info) {
+      this.info = info;
+    }
+
+    @Override
+    @NonNull public ContactInfo getContactInfo() {
+      return info;
+    }
+
+    @Override
+    public void setSource(ContactSource.Type sourceType, String name, long directoryId) {
+    }
+
+    @Override
+    public void setDirectorySource(String name, long directoryId) {
+    }
+
+    @Override
+    public void setLookupKey(String lookupKey) {
+    }
+  }
+}
diff --git a/java/com/android/dialer/lookup/LookupProvider.java b/java/com/android/dialer/lookup/LookupProvider.java
new file mode 100644
index 0000000..5e8fc1d
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupProvider.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.dialer.searchfragment.common.Projections;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class LookupProvider extends ContentProvider {
+  private static final String TAG = LookupProvider.class.getSimpleName();
+
+  private static final boolean DEBUG = false;
+
+  public static final String AUTHORITY = "com.android.dialer.lookup";
+  public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+  public static final Uri NEARBY_LOOKUP_URI =
+      Uri.withAppendedPath(AUTHORITY_URI, "nearby");
+  public static final Uri PEOPLE_LOOKUP_URI =
+      Uri.withAppendedPath(AUTHORITY_URI, "people");
+  public static final Uri NEARBY_AND_PEOPLE_LOOKUP_URI =
+      Uri.withAppendedPath(AUTHORITY_URI, "nearby_and_people");
+  public static final Uri IMAGE_CACHE_URI =
+      Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+  private static final UriMatcher uriMatcher = new UriMatcher(-1);
+  private final LinkedList<FutureTask> activeTasks = new LinkedList<>();
+
+  private static final int NEARBY = 0;
+  private static final int PEOPLE = 1;
+  private static final int NEARBY_AND_PEOPLE = 2;
+  private static final int IMAGE = 3;
+
+  static {
+    uriMatcher.addURI(AUTHORITY, "nearby/*", NEARBY);
+    uriMatcher.addURI(AUTHORITY, "people/*", PEOPLE);
+    uriMatcher.addURI(AUTHORITY, "nearby_and_people/*", NEARBY_AND_PEOPLE);
+    uriMatcher.addURI(AUTHORITY, "images/*", IMAGE);
+  }
+
+  private class FutureCallable<T> implements Callable<T> {
+    private final Callable<T> callable;
+    private volatile FutureTask<T> future;
+
+    public FutureCallable(Callable<T> callable) {
+      future = null;
+      this.callable = callable;
+    }
+
+    public T call() throws Exception {
+      Log.v(TAG, "Future called for " + Thread.currentThread().getName());
+
+      T result = callable.call();
+      if (future == null) {
+        return result;
+      }
+
+      synchronized (activeTasks) {
+        activeTasks.remove(future);
+      }
+
+      future = null;
+      return result;
+    }
+
+    public void setFuture(FutureTask<T> future) {
+      this.future = future;
+    }
+  }
+
+  @Override
+  public boolean onCreate() {
+    return true;
+  }
+
+  @Override
+  public Cursor query(Uri uri, final String[] projection, String selection,
+      String[] selectionArgs, String sortOrder) {
+    if (DEBUG) Log.v(TAG, "query: " + uri);
+
+    Location lastLocation = null;
+    final int match = uriMatcher.match(uri);
+
+    switch (match) {
+      case NEARBY:
+      case NEARBY_AND_PEOPLE:
+        if (!PermissionsUtil.hasLocationPermissions(getContext())) {
+          Log.v(TAG, "Location permission is missing, can not determine location.");
+        } else if (!isLocationEnabled()) {
+          Log.v(TAG, "Location settings is disabled, can no determine location.");
+        } else {
+          lastLocation = getLastLocation();
+        }
+        if (match == NEARBY && lastLocation == null) {
+          Log.v(TAG, "No location available, ignoring query.");
+          return null;
+        }
+        // fall through to the actual query
+
+      case PEOPLE:
+        final String filter = Uri.encode(uri.getLastPathSegment());
+        String limit = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+
+        int maxResults = -1;
+
+        try {
+          if (limit != null) {
+            maxResults = Integer.parseInt(limit);
+          }
+        } catch (NumberFormatException e) {
+          Log.e(TAG, "query: invalid limit parameter: '" + limit + "'");
+        }
+
+        final Location finalLastLocation = lastLocation;
+        final int finalMaxResults = maxResults;
+
+        return execute(new Callable<Cursor>() {
+          @Override
+          public Cursor call() {
+            return handleFilter(match, projection, filter, finalMaxResults, finalLastLocation);
+          }
+        }, "FilterThread");
+    }
+
+    return null;
+  }
+
+  @Override
+  public Uri insert(Uri uri, ContentValues values) {
+    throw new UnsupportedOperationException("insert() not supported");
+  }
+
+  @Override
+  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+    throw new UnsupportedOperationException("update() not supported");
+  }
+
+  @Override
+  public int delete(Uri uri, String selection, String[] selectionArgs) {
+    throw new UnsupportedOperationException("delete() not supported");
+  }
+
+  @Override
+  public String getType(Uri uri) {
+    int match = uriMatcher.match(uri);
+
+    switch (match) {
+      case NEARBY:
+      case PEOPLE:
+      case NEARBY_AND_PEOPLE:
+        return Contacts.CONTENT_ITEM_TYPE;
+
+      default:
+        return null;
+    }
+  }
+
+  @Override
+  public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+    switch (uriMatcher.match(uri)) {
+      case IMAGE:
+        String number = uri.getLastPathSegment();
+        File image = LookupCache.getImagePath(getContext(), number);
+
+        if (mode.equals("r")) {
+          if (image == null || !image.exists() || !image.isFile()) {
+            throw new FileNotFoundException("Cached image does not exist");
+          }
+
+          return ParcelFileDescriptor.open(image, ParcelFileDescriptor.MODE_READ_ONLY);
+        } else {
+          throw new FileNotFoundException("The URI is read only");
+        }
+
+      default:
+        throw new FileNotFoundException("Invalid URI: " + uri);
+    }
+  }
+
+  /**
+   * Check if the location services is on.
+   *
+   * @return Whether location services are enabled
+   */
+  private boolean isLocationEnabled() {
+    try {
+      int mode = Settings.Secure.getInt(getContext().getContentResolver(),
+          Settings.Secure.LOCATION_MODE);
+
+      return mode != Settings.Secure.LOCATION_MODE_OFF;
+    } catch (Settings.SettingNotFoundException e) {
+      Log.e(TAG, "Failed to get location mode", e);
+      return false;
+    }
+  }
+
+  /**
+   * Get location from last location query.
+   *
+   * @return The last location
+   */
+  private Location getLastLocation() {
+    LocationManager locationManager = getContext().getSystemService(LocationManager.class);
+
+    try {
+      locationManager.requestSingleUpdate(new Criteria(), new LocationListener() {
+        @Override
+        public void onLocationChanged(Location location) {
+        }
+
+        @Override
+        public void onProviderDisabled(String provider) {
+        }
+
+        @Override
+        public void onProviderEnabled(String provider) {
+        }
+
+        @Override
+        public void onStatusChanged(String provider, int status, Bundle extras) {
+        }
+      }, Looper.getMainLooper());
+
+      return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Process filter/query and perform the lookup.
+   *
+   * @param projection Columns to include in query
+   * @param filter String to lookup
+   * @param maxResults Maximum number of results
+   * @param lastLocation Coordinates of last location query
+   * @return Cursor for the results
+   */
+  private Cursor handleFilter(int type, String[] projection, String filter,
+      int maxResults, Location lastLocation) {
+    if (DEBUG) Log.v(TAG, "handleFilter(" + filter + ")");
+
+    if (filter == null) {
+      return null;
+    }
+
+    try {
+      filter = URLDecoder.decode(filter, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+    }
+
+    ArrayList<ContactInfo> results = null;
+    if ((type == NEARBY || type == NEARBY_AND_PEOPLE) && lastLocation != null) {
+      ForwardLookup fl = ForwardLookup.getInstance(getContext());
+      List<ContactInfo> nearby = fl.lookup(getContext(), filter, lastLocation);
+      if (nearby != null) {
+        results.addAll(nearby);
+      }
+    }
+    if (type == PEOPLE || type == NEARBY_AND_PEOPLE) {
+      PeopleLookup pl = PeopleLookup.getInstance(getContext());
+      List<ContactInfo> people = pl.lookup(getContext(), filter);
+      if (people != null) {
+        results.addAll(people);
+      }
+    }
+
+    if (results.isEmpty()) {
+      if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): No results");
+      return null;
+    }
+
+    Cursor cursor = null;
+    try {
+      cursor = buildResultCursor(projection, results, maxResults);
+      if (DEBUG) {
+        Log.v(TAG, "handleFilter(" + filter + "): " + cursor.getCount() + " matches");
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON failure", e);
+    }
+
+    return cursor;
+  }
+
+  /**
+   * Query results.
+   *
+   * @param projection Columns to include in query
+   * @param results Results for the forward lookup
+   * @param maxResults Maximum number of rows/results to add to cursor
+   * @return Cursor for forward lookup query results
+   */
+  private Cursor buildResultCursor(String[] projection, List<ContactInfo> results, int maxResults)
+      throws JSONException {
+    // Extended directories always use this projection
+    MatrixCursor cursor = new MatrixCursor(Projections.DATA_PROJECTION);
+
+    int id = 1;
+    for (ContactInfo result : results) {
+      Object[] row = new Object[Projections.DATA_PROJECTION.length];
+
+      row[Projections.ID] = id;
+      row[Projections.PHONE_TYPE] = result.type;
+      row[Projections.PHONE_LABEL] = getAddress(result);
+      row[Projections.PHONE_NUMBER] = result.number;
+      row[Projections.DISPLAY_NAME] = result.name;
+      row[Projections.PHOTO_ID] = 0;
+      row[Projections.PHOTO_URI] = result.photoUri;
+      row[Projections.LOOKUP_KEY] = result.lookupUri.getEncodedFragment();
+      row[Projections.CONTACT_ID] = id;
+
+      cursor.addRow(row);
+
+      if (maxResults != -1 && cursor.getCount() >= maxResults) {
+        break;
+      }
+
+      id++;
+    }
+
+    return cursor;
+  }
+
+  private String getAddress(ContactInfo info) {
+    // Hack: Show city or address for phone label, so they appear in the results list
+
+    String city = null;
+    String address = null;
+
+    try {
+      String jsonString = info.lookupUri.getEncodedFragment();
+      JSONObject json = new JSONObject(jsonString);
+      JSONObject contact = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+
+      if (!contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) {
+        return null;
+      }
+
+      JSONArray addresses = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE);
+      if (addresses.length() == 0) {
+        return null;
+      }
+
+      JSONObject addressEntry = addresses.getJSONObject(0);
+      if (addressEntry.has(StructuredPostal.CITY)) {
+        city = addressEntry.getString(StructuredPostal.CITY);
+      }
+      if (addressEntry.has(StructuredPostal.FORMATTED_ADDRESS)) {
+        address = addressEntry.getString(StructuredPostal.FORMATTED_ADDRESS);
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "Failed to get address", e);
+    }
+
+    if (city != null) {
+      return city;
+    } else if (address != null) {
+      return address;
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Execute thread that is killed after a specified amount of time.
+   *
+   * @param callable The thread
+   * @param name Name of the thread
+   * @return Instance of the thread
+   */
+  private <T> T execute(Callable<T> callable, String name) {
+    FutureCallable<T> futureCallable = new FutureCallable<T>(callable);
+    FutureTask<T> future = new FutureTask<T>(futureCallable);
+    futureCallable.setFuture(future);
+
+    synchronized (activeTasks) {
+      activeTasks.addLast(future);
+      Log.v(TAG, "Currently running tasks: " + activeTasks.size());
+
+      while (activeTasks.size() > 8) {
+        Log.w(TAG, "Too many tasks, canceling one");
+        activeTasks.removeFirst().cancel(true);
+      }
+    }
+
+    Log.v(TAG, "Starting task " + name);
+
+    new Thread(future, name).start();
+
+    try {
+      Log.v(TAG, "Getting future " + name);
+      return future.get(10000, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      Log.w(TAG, "Task was interrupted: " + name);
+      Thread.currentThread().interrupt();
+    } catch (ExecutionException e) {
+      Log.w(TAG, "Task threw an exception: " + name, e);
+    } catch (TimeoutException e) {
+      Log.w(TAG, "Task timed out: " + name);
+      future.cancel(true);
+    } catch (CancellationException e) {
+      Log.w(TAG, "Task was cancelled: " + name);
+    }
+
+    return null;
+  }
+}
diff --git a/java/com/android/dialer/lookup/LookupSettings.java b/java/com/android/dialer/lookup/LookupSettings.java
new file mode 100644
index 0000000..2686f68
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettings.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+
+import java.util.List;
+
+public final class LookupSettings {
+  private static final String TAG = LookupSettings.class.getSimpleName();
+
+  /** Forward lookup providers */
+  public static final String FLP_GOOGLE = "Google";
+  public static final String FLP_OPENSTREETMAP = "OpenStreetMap";
+  public static final String FLP_DEFAULT = FLP_GOOGLE;
+
+  /** People lookup providers */
+  public static final String PLP_AUSKUNFT = "Auskunft";
+  public static final String PLP_DEFAULT = PLP_AUSKUNFT;
+
+  /** Reverse lookup providers */
+  public static final String RLP_OPENCNAM = "OpenCnam";
+  public static final String RLP_YELLOWPAGES = "YellowPages";
+  public static final String RLP_YELLOWPAGES_CA = "YellowPages_CA";
+  public static final String RLP_ZABASEARCH = "ZabaSearch";
+  public static final String RLP_CYNGN_CHINESE = "CyngnChinese";
+  public static final String RLP_DASTELEFONBUCH = "DasTelefonbuch";
+  public static final String RLP_AUSKUNFT = "Auskunft";
+  public static final String RLP_DEFAULT = RLP_OPENCNAM;
+
+  /** Preferences */
+  private static final String SHARED_PREFERENCES_NAME = "lookup_settings";
+  private static final String ENABLE_FORWARD_LOOKUP = "enable_forward_lookup";
+  private static final String ENABLE_PEOPLE_LOOKUP = "enable_people_lookup";
+  private static final String ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup";
+  private static final String FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider";
+  private static final String PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider";
+  private static final String REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider";
+  private static final String OPENCNAM_ACCOUNT_SID = "opencnam_account_sid";
+  private static final String OPENCNAM_AUTH_TOKEN = "opencnam_auth_token";
+
+  private LookupSettings() {
+  }
+
+  private static SharedPreferences getSharedPreferences(Context context) {
+    return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+  }
+
+  public static boolean isForwardLookupEnabled(Context context) {
+    return getSharedPreferences(context).getBoolean(ENABLE_FORWARD_LOOKUP, false);
+  }
+
+  public static void setForwardLookupEnabled(Context context, boolean value) {
+    getSharedPreferences(context).edit().putBoolean(ENABLE_FORWARD_LOOKUP, value).commit();
+  }
+
+  public static boolean isPeopleLookupEnabled(Context context) {
+    return getSharedPreferences(context).getBoolean(ENABLE_PEOPLE_LOOKUP, false);
+  }
+
+  public static void setPeopleLookupEnabled(Context context, boolean value) {
+    getSharedPreferences(context).edit().putBoolean(ENABLE_PEOPLE_LOOKUP, value).commit();
+  }
+
+  public static boolean isReverseLookupEnabled(Context context) {
+    return getSharedPreferences(context).getBoolean(ENABLE_REVERSE_LOOKUP, false);
+  }
+
+  public static void setReverseLookupEnabled(Context context, boolean value) {
+    getSharedPreferences(context).edit().putBoolean(ENABLE_REVERSE_LOOKUP, value).commit();
+  }
+
+  public static String getForwardLookupProvider(Context context) {
+    return getSharedPreferences(context).getString(FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT);
+  }
+
+  public static void setForwardLookupProvider(Context context, String value) {
+    getSharedPreferences(context).edit().putString(FORWARD_LOOKUP_PROVIDER, value).commit();
+  }
+
+  public static String getPeopleLookupProvider(Context context) {
+    return getSharedPreferences(context).getString(PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT);
+  }
+
+  public static void setPeopleLookupProvider(Context context, String value) {
+    getSharedPreferences(context).edit().putString(PEOPLE_LOOKUP_PROVIDER, value).commit();
+  }
+
+  public static String getReverseLookupProvider(Context context) {
+    return getSharedPreferences(context).getString(REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT);
+  }
+
+  public static void setReverseLookupProvider(Context context, String value) {
+    getSharedPreferences(context).edit().putString(REVERSE_LOOKUP_PROVIDER, value).commit();
+  }
+
+  public static String getOpenCnamAccountSid(Context context) {
+    return getSharedPreferences(context).getString(OPENCNAM_ACCOUNT_SID, null);
+  }
+
+  public static void setOpenCnamAccountSid(Context context, String value) {
+    getSharedPreferences(context).edit().putString(OPENCNAM_ACCOUNT_SID, value).commit();
+  }
+
+  public static String getOpenCnamAuthToken(Context context) {
+    return getSharedPreferences(context).getString(OPENCNAM_AUTH_TOKEN, null);
+  }
+
+  public static void setOpenCnamAuthToken(Context context, String value) {
+    getSharedPreferences(context).edit().putString(OPENCNAM_AUTH_TOKEN, value).commit();
+  }
+}
diff --git a/java/com/android/dialer/lookup/LookupSettingsFragment.java b/java/com/android/dialer/lookup/LookupSettingsFragment.java
new file mode 100644
index 0000000..bca92f9
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettingsFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.lookup;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.ListPreference;
+import android.preference.PreferenceFragment;
+import android.preference.SwitchPreference;
+
+import com.android.dialer.R;
+
+import java.util.Arrays;
+
+public class LookupSettingsFragment extends PreferenceFragment
+    implements Preference.OnPreferenceChangeListener {
+
+  private static final String KEY_ENABLE_FORWARD_LOOKUP = "enable_forward_lookup";
+  private static final String KEY_ENABLE_PEOPLE_LOOKUP = "enable_people_lookup";
+  private static final String KEY_ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup";
+  private static final String KEY_FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider";
+  private static final String KEY_PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider";
+  private static final String KEY_REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider";
+
+  private SwitchPreference enableForwardLookup;
+  private SwitchPreference enablePeopleLookup;
+  private SwitchPreference enableReverseLookup;
+  private ListPreference forwardLookupProvider;
+  private ListPreference peopleLookupProvider;
+  private ListPreference reverseLookupProvider;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    addPreferencesFromResource(R.xml.lookup_settings);
+
+    enableForwardLookup = (SwitchPreference) findPreference(KEY_ENABLE_FORWARD_LOOKUP);
+    enablePeopleLookup = (SwitchPreference) findPreference(KEY_ENABLE_PEOPLE_LOOKUP);
+    enableReverseLookup = (SwitchPreference) findPreference(KEY_ENABLE_REVERSE_LOOKUP);
+
+    enableForwardLookup.setOnPreferenceChangeListener(this);
+    enablePeopleLookup.setOnPreferenceChangeListener(this);
+    enableReverseLookup.setOnPreferenceChangeListener(this);
+
+    forwardLookupProvider = (ListPreference) findPreference(KEY_FORWARD_LOOKUP_PROVIDER);
+    peopleLookupProvider = (ListPreference) findPreference(KEY_PEOPLE_LOOKUP_PROVIDER);
+    reverseLookupProvider = (ListPreference) findPreference(KEY_REVERSE_LOOKUP_PROVIDER);
+
+    forwardLookupProvider.setOnPreferenceChangeListener(this);
+    peopleLookupProvider.setOnPreferenceChangeListener(this);
+    reverseLookupProvider.setOnPreferenceChangeListener(this);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    restoreLookupProviderSwitches();
+    restoreLookupProviders();
+  }
+
+  @Override
+  public boolean onPreferenceChange(Preference preference, Object newValue) {
+    Context context = getContext();
+
+    if (preference == enableForwardLookup) {
+      LookupSettings.setForwardLookupEnabled(context, (Boolean) newValue);
+    } else if (preference == enablePeopleLookup) {
+      LookupSettings.setPeopleLookupEnabled(context, (Boolean) newValue);
+    } else if (preference == enableReverseLookup) {
+      LookupSettings.setReverseLookupEnabled(context, (Boolean) newValue);
+    } else if (preference == forwardLookupProvider) {
+      LookupSettings.setForwardLookupProvider(context, (String) newValue);
+    } else if (preference == peopleLookupProvider) {
+      LookupSettings.setPeopleLookupProvider(context, (String) newValue);
+    } else if (preference == reverseLookupProvider) {
+      LookupSettings.setReverseLookupProvider(context, (String) newValue);
+    }
+
+    return true;
+  }
+
+  private void restoreLookupProviderSwitches() {
+    Context context = getContext();
+
+    enableForwardLookup.setChecked(LookupSettings.isForwardLookupEnabled(context));
+    enablePeopleLookup.setChecked(LookupSettings.isPeopleLookupEnabled(context));
+    enableReverseLookup.setChecked(LookupSettings.isReverseLookupEnabled(context));
+  }
+
+  private void restoreLookupProviders() {
+    Context context = getContext();
+
+    restoreLookupProvider(forwardLookupProvider, LookupSettings.getForwardLookupProvider(context));
+    restoreLookupProvider(peopleLookupProvider, LookupSettings.getPeopleLookupProvider(context));
+    restoreLookupProvider(reverseLookupProvider, LookupSettings.getReverseLookupProvider(context));
+  }
+
+  private void restoreLookupProvider(ListPreference pref, String provider) {
+    Context context = getContext();
+
+    if (pref.getEntries().length < 1) {
+      pref.setEnabled(false);
+      return;
+    }
+
+    if (provider == null) {
+      pref.setValueIndex(0);
+
+      if (pref == forwardLookupProvider) {
+        LookupSettings.setForwardLookupProvider(context, pref.getValue());
+      } else if (pref == peopleLookupProvider) {
+        LookupSettings.setPeopleLookupProvider(context, pref.getValue());
+      } else if (pref == reverseLookupProvider) {
+        LookupSettings.setReverseLookupProvider(context, pref.getValue());
+      }
+    } else {
+      pref.setValue(provider);
+    }
+  }
+}
diff --git a/java/com/android/dialer/lookup/LookupUtils.java b/java/com/android/dialer/lookup/LookupUtils.java
new file mode 100644
index 0000000..b6e4533
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupUtils.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.lookup;
+
+import android.text.Html;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LookupUtils {
+  private static final String USER_AGENT =
+      "Mozilla/5.0 (X11; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0";
+
+  private static HttpURLConnection prepareHttpConnection(String url, Map<String, String> headers)
+      throws IOException {
+    // open connection
+    HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
+    // set user agent (default value is null)
+    urlConnection.setRequestProperty("User-Agent", USER_AGENT);
+    // set all other headers if not null
+    if (headers != null) {
+      for (Map.Entry<String, String> header : headers.entrySet()) {
+        urlConnection.setRequestProperty(header.getKey(), header.getValue());
+      }
+    }
+
+    return urlConnection;
+  }
+
+  private static byte[] httpFetch(HttpURLConnection urlConnection) throws IOException {
+    // query url, read and return buffered response body
+    // we want to make sure that the connection gets closed here
+    InputStream is = new BufferedInputStream(urlConnection.getInputStream());
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    byte[] result = null;
+    try {
+      byte[] partial = new byte[4096];
+      int read;
+      while ((read = is.read(partial, 0, 4096)) != -1) {
+        baos.write(partial, 0, read);
+      }
+      result = baos.toByteArray();
+    } finally {
+      is.close();
+      baos.close();
+    }
+    return result;
+  }
+
+  private static Charset determineCharset(HttpURLConnection connection) {
+    String contentType = connection.getContentType();
+    if (contentType != null) {
+      String[] split = contentType.split(";");
+      for (int i = 0; i < split.length; i++) {
+        String trimmed = split[i].trim();
+        if (trimmed.startsWith("charset=")) {
+          try {
+            return Charset.forName(trimmed.substring(8));
+          } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+            // we don't know about this charset -> ignore
+          }
+        }
+      }
+    }
+    return Charset.defaultCharset();
+  }
+
+  public static String httpGet(String url, Map<String, String> headers) throws IOException {
+    HttpURLConnection connection = prepareHttpConnection(url, headers);
+    try {
+      byte[] response = httpFetch(connection);
+      return new String(response, determineCharset(connection));
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  public static byte[] httpGetBytes(String url, Map<String, String> headers) throws IOException {
+    HttpURLConnection connection = prepareHttpConnection(url, headers);
+    try {
+      return httpFetch(connection);
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  public static String httpPost(String url, Map<String, String> headers, String postData)
+      throws IOException {
+    HttpURLConnection connection = prepareHttpConnection(url, headers);
+
+    try {
+      // write postData to buffered output stream
+      if (postData != null) {
+        connection.setDoOutput(true);
+        BufferedWriter bw = new BufferedWriter(
+            new OutputStreamWriter(connection.getOutputStream()));
+        try {
+          bw.write(postData, 0, postData.length());
+          // close connection and re-throw exception
+        } finally {
+          bw.close();
+        }
+      }
+      byte[] response = httpFetch(connection);
+      return new String(response, determineCharset(connection));
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  public static List<String> allRegexResults(String input, String regex, boolean dotall) {
+    if (input == null) {
+      return null;
+    }
+    Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0);
+    Matcher matcher = pattern.matcher(input);
+
+    List<String> regexResults = new ArrayList<String>();
+    while (matcher.find()) {
+      regexResults.add(matcher.group(1).trim());
+    }
+    return regexResults;
+  }
+
+  public static String firstRegexResult(String input, String regex, boolean dotall) {
+    if (input == null) {
+      return null;
+    }
+    Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0);
+    Matcher m = pattern.matcher(input);
+    return m.find() ? m.group(1).trim() : null;
+  }
+
+  public static String fromHtml(String input) {
+    if (input == null) {
+      return null;
+    }
+    return Html.fromHtml(input).toString().trim();
+  }
+}
diff --git a/java/com/android/dialer/lookup/PeopleLookup.java b/java/com/android/dialer/lookup/PeopleLookup.java
new file mode 100644
index 0000000..c7e53df
--- /dev/null
+++ b/java/com/android/dialer/lookup/PeopleLookup.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.auskunft.AuskunftPeopleLookup;
+
+import java.util.List;
+
+public abstract class PeopleLookup {
+  private static final String TAG = PeopleLookup.class.getSimpleName();
+
+  private static PeopleLookup INSTANCE = null;
+
+  public static PeopleLookup getInstance(Context context) {
+    String provider = LookupSettings.getPeopleLookupProvider(context);
+
+    if (INSTANCE == null || !isInstance(provider)) {
+      Log.d(TAG, "Chosen people lookup provider: " + provider);
+
+      if (provider.equals(LookupSettings.PLP_AUSKUNFT)) {
+        INSTANCE = new AuskunftPeopleLookup(context);
+      }
+    }
+
+    return INSTANCE;
+  }
+
+  private static boolean isInstance(String provider) {
+    if (provider.equals(LookupSettings.PLP_AUSKUNFT)
+        && INSTANCE instanceof AuskunftPeopleLookup) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  public abstract List<ContactInfo> lookup(Context context, String filter);
+}
diff --git a/java/com/android/dialer/lookup/ReverseLookup.java b/java/com/android/dialer/lookup/ReverseLookup.java
new file mode 100644
index 0000000..a2cc896
--- /dev/null
+++ b/java/com/android/dialer/lookup/ReverseLookup.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.auskunft.AuskunftReverseLookup;
+import com.android.dialer.lookup.dastelefonbuch.TelefonbuchReverseLookup;
+import com.android.dialer.lookup.opencnam.OpenCnamReverseLookup;
+import com.android.dialer.lookup.yellowpages.YellowPagesReverseLookup;
+import com.android.dialer.lookup.zabasearch.ZabaSearchReverseLookup;
+
+import java.io.IOException;
+
+public abstract class ReverseLookup {
+  private static final String TAG = ReverseLookup.class.getSimpleName();
+
+  private static ReverseLookup INSTANCE = null;
+
+  public static ReverseLookup getInstance(Context context) {
+    String provider = LookupSettings.getReverseLookupProvider(context);
+
+    if (INSTANCE == null || !isInstance(provider)) {
+      Log.d(TAG, "Chosen reverse lookup provider: " + provider);
+
+      if (provider.equals(LookupSettings.RLP_OPENCNAM)) {
+        INSTANCE = new OpenCnamReverseLookup(context);
+      } else if (provider.equals(LookupSettings.RLP_YELLOWPAGES)
+          || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) {
+        INSTANCE = new YellowPagesReverseLookup(context, provider);
+      } else if (provider.equals(LookupSettings.RLP_ZABASEARCH)) {
+        INSTANCE = new ZabaSearchReverseLookup(context);
+      } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)) {
+        INSTANCE = new TelefonbuchReverseLookup(context);
+      } else if (provider.equals(LookupSettings.RLP_AUSKUNFT)) {
+        INSTANCE = new AuskunftReverseLookup(context);
+      }
+    }
+
+    return INSTANCE;
+  }
+
+  private static boolean isInstance(String provider) {
+    if (provider.equals(LookupSettings.RLP_OPENCNAM)
+        && INSTANCE instanceof OpenCnamReverseLookup) {
+      return true;
+    } else if ((provider.equals(LookupSettings.RLP_YELLOWPAGES)
+        || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA))
+        && INSTANCE instanceof YellowPagesReverseLookup) {
+      return true;
+    } else if (provider.equals(LookupSettings.RLP_ZABASEARCH)
+        && INSTANCE instanceof ZabaSearchReverseLookup) {
+      return true;
+    } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)
+        && INSTANCE instanceof TelefonbuchReverseLookup) {
+      return true;
+    } else if (provider.equals(LookupSettings.RLP_AUSKUNFT)
+        && INSTANCE instanceof AuskunftReverseLookup) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Lookup image
+   *
+   * @param context The application context
+   * @param uri The image URI
+   */
+  public Bitmap lookupImage(Context context, Uri uri) {
+    return null;
+  }
+
+  /**
+   * Perform phone number lookup.
+   *
+   * @param context The application context
+   * @param normalizedNumber The normalized phone number
+   * @param formattedNumber The formatted phone number
+   * @return The phone number info object
+   */
+  public abstract ContactInfo lookupNumber(Context context,
+      String normalizedNumber, String formattedNumber) throws IOException;
+}
diff --git a/java/com/android/dialer/lookup/ReverseLookupService.java b/java/com/android/dialer/lookup/ReverseLookupService.java
new file mode 100644
index 0000000..02e873b
--- /dev/null
+++ b/java/com/android/dialer/lookup/ReverseLookupService.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.logging.ContactLookupResult;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.incallui.bindings.PhoneNumberService;
+
+import java.io.IOException;
+
+public class ReverseLookupService implements PhoneNumberService, Handler.Callback {
+  private final HandlerThread backgroundThread;
+  private final Handler backgroundHandler;
+  private final Handler handler;
+  private final Context context;
+  private final TelephonyManager telephonyManager;
+
+  private static final int MSG_LOOKUP = 1;
+  private static final int MSG_NOTIFY_NUMBER = 2;
+
+  public ReverseLookupService(Context context) {
+    this.context = context;
+    telephonyManager = context.getSystemService(TelephonyManager.class);
+
+    // TODO: stop after a while?
+    backgroundThread = new HandlerThread("ReverseLookup");
+    backgroundThread.start();
+
+    backgroundHandler = new Handler(backgroundThread.getLooper(), this);
+    handler = new Handler(this);
+  }
+
+  @Override
+  public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener numberListener) {
+    if (!LookupSettings.isReverseLookupEnabled(context)) {
+      LookupCache.deleteCachedContacts(context);
+      return;
+    }
+
+    String countryIso = telephonyManager.getSimCountryIso().toUpperCase();
+    String normalizedNumber = phoneNumber != null
+        ? PhoneNumberUtils.formatNumberToE164(phoneNumber, countryIso) : null;
+
+    // Can't do reverse lookup without a number
+    if (normalizedNumber == null) {
+      return;
+    }
+
+    LookupRequest request = new LookupRequest();
+    request.normalizedNumber = normalizedNumber;
+    request.formattedNumber = PhoneNumberUtils.formatNumber(phoneNumber,
+        request.normalizedNumber, GeoUtil.getCurrentCountryIso(context));
+    request.numberListener = numberListener;
+
+    backgroundHandler.obtainMessage(MSG_LOOKUP, request).sendToTarget();
+  }
+
+  @Override
+  public boolean handleMessage(Message msg) {
+    switch (msg.what) {
+      case MSG_LOOKUP: {
+        // background thread
+        LookupRequest request = (LookupRequest) msg.obj;
+        request.contactInfo = doLookup(request);
+        if (request.contactInfo != null) {
+          handler.obtainMessage(MSG_NOTIFY_NUMBER, request).sendToTarget();
+        }
+        break;
+      }
+      case MSG_NOTIFY_NUMBER: {
+        // main thread
+        LookupRequest request = (LookupRequest) msg.obj;
+        if (request.numberListener != null) {
+          LookupNumberInfo info = new LookupNumberInfo(request.contactInfo);
+          request.numberListener.onPhoneNumberInfoComplete(info);
+        }
+        break;
+      }
+    }
+
+    return true;
+  }
+
+  private ContactInfo doLookup(LookupRequest request) {
+    final String number = request.normalizedNumber;
+
+    if (LookupCache.hasCachedContact(context, number)) {
+      ContactInfo info = LookupCache.getCachedContact(context, number);
+      if (!ContactInfo.EMPTY.equals(info)) {
+        return info;
+      } else if (info != null) {
+        // If we have an empty cached contact, remove it and redo lookup
+        LookupCache.deleteCachedContact(context, number);
+      }
+    }
+
+    try {
+      ReverseLookup inst = ReverseLookup.getInstance(context);
+      ContactInfo info = inst.lookupNumber(context, number, request.formattedNumber);
+      if (info != null && !info.equals(ContactInfo.EMPTY)) {
+        LookupCache.cacheContact(context, info);
+        return info;
+      }
+    } catch (IOException e) {
+      // ignored
+    }
+
+    return null;
+  }
+
+  private Bitmap fetchImage(LookupRequest request, Uri uri) {
+    if (!LookupCache.hasCachedImage(context, request.normalizedNumber)) {
+      Bitmap bmp = ReverseLookup.getInstance(context).lookupImage(context, uri);
+      if (bmp != null) {
+        LookupCache.cacheImage(context, request.normalizedNumber, bmp);
+      }
+    }
+
+    return LookupCache.getCachedImage(context, request.normalizedNumber);
+  }
+
+  private static class LookupRequest {
+    String normalizedNumber;
+    String formattedNumber;
+    NumberLookupListener numberListener;
+    ContactInfo contactInfo;
+  }
+
+  private static class LookupNumberInfo implements PhoneNumberInfo {
+    private final ContactInfo info;
+    private LookupNumberInfo(ContactInfo info) {
+      this.info = info;
+    }
+
+    @Override
+    public String getDisplayName() {
+      return info.name;
+    }
+    @Override
+    public String getNumber() {
+      return info.number;
+    }
+    @Override
+    public int getPhoneType() {
+      return info.type;
+    }
+    @Override
+    public String getPhoneLabel() {
+      return info.label;
+    }
+    @Override
+    public String getNormalizedNumber() {
+      return info.normalizedNumber;
+    }
+    @Override
+    public String getImageUrl() {
+      return info.photoUri != null ? info.photoUri.toString() : null;
+    }
+    @Override
+    public boolean isBusiness() {
+      // FIXME
+      return false;
+    }
+    @Override
+    public String getLookupKey() {
+      return info.lookupKey;
+    }
+    @Override
+    public ContactLookupResult.Type getLookupSource() {
+      return ContactLookupResult.Type.REMOTE;
+    }
+  }
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftApi.java b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java
new file mode 100644
index 0000000..5b6b251
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.lookup.auskunft;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class AuskunftApi {
+  private static final String TAG = AuskunftApi.class.getSimpleName();
+
+  private static final String PEOPLE_LOOKUP_URL = "https://auskunft.at/suche";
+
+  private static final String SEARCH_RESULTS_REGEX =
+      "(?i)<section[\\s]+class=[\"']?search-entry(.*?)?</section";
+  private static final String NAME_REGEX =
+      "(?i)<h1[\\s]+itemprop=[\"']?name[\"']?>(.*?)</h1";
+  private static final String NUMBER_REGEX =
+      "(?i)phone[\"'][\\s]+?href=[\"']{1}tel:(.*?)[\"']{1}";
+  private static final String ADDRESS_REGEX =
+      "(?i)<span[\\s]+itemprop=[\"']?streetAddress[\"']?>(.*?)</a";
+
+  private static final String BUSINESS_IDENTIFIER = "(Firma)";
+
+  private AuskunftApi() {
+  }
+
+  public static List<ContactInfo> query(String filter) throws IOException {
+    // build URI
+    Uri uri = Uri.parse(PEOPLE_LOOKUP_URL)
+        .buildUpon()
+        .appendQueryParameter("query", filter)
+        .build();
+
+    // get all search entry sections
+    List<String> entries = LookupUtils.allRegexResults(
+        LookupUtils.httpGet(uri.toString(), null), SEARCH_RESULTS_REGEX, true);
+
+    // abort lookup if nothing found
+    if (entries == null || entries.isEmpty()) {
+      Log.w(TAG, "nothing found");
+      return null;
+    }
+
+    // build response by iterating through the search entries and parsing their HTML data
+    List<ContactInfo> infos = new ArrayList<ContactInfo>();
+    for (String entry : entries) {
+      // parse wanted data and replace null values
+      String name = replaceNullResult(LookupUtils.firstRegexResult(entry, NAME_REGEX, true));
+      String address = replaceNullResult(LookupUtils.firstRegexResult(entry, ADDRESS_REGEX, true));
+      String number = replaceNullResult(LookupUtils.firstRegexResult(entry, NUMBER_REGEX, true));
+      // ignore entry if name or number is empty (should not occur)
+      // missing addresses won't be a problem (but do occur)
+      if (name.isEmpty() || number.isEmpty()) {
+        continue;
+      }
+
+      ContactInfo info = new ContactInfo();
+      info.name = cleanupResult(name);
+      info.number = cleanupResult(number);
+      info.address = cleanupResult(address);
+      info.url = uri.toString();
+
+      infos.add(info);
+    }
+    return infos;
+  }
+
+  private static String cleanupResult(String result) {
+    // get displayable text
+    result = LookupUtils.fromHtml(result);
+    // replace newlines with spaces
+    result = result.replaceAll("\\r|\\n", " ");
+    // replace multiple spaces with one
+    result = result.replaceAll("\\s+", " ");
+    // remove business identifier that is originally not part of the name
+    result = result.replace(BUSINESS_IDENTIFIER, "");
+    // final trimming
+    result = result.trim();
+
+    return result;
+  }
+
+  private static String replaceNullResult(String result) {
+    return (result == null) ? "" : result;
+  }
+
+  static class ContactInfo {
+    String name;
+    String number;
+    String url;
+    String address;
+  };
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java
new file mode 100644
index 0000000..6feb1a5
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.lookup.auskunft;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.PeopleLookup;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuskunftPeopleLookup extends PeopleLookup {
+  private static final String TAG = AuskunftPeopleLookup.class.getSimpleName();
+
+  public AuskunftPeopleLookup(Context context) {
+  }
+
+  @Override
+  public List<ContactInfo> lookup(Context context, String filter) {
+    try {
+      List<AuskunftApi.ContactInfo> infos = AuskunftApi.query(filter);
+      if (infos != null) {
+          List<ContactInfo> result = new ArrayList<>();
+          for (AuskunftApi.ContactInfo info : infos) {
+            result.add(ContactBuilder.forPeopleLookup(info.number)
+                .setName(ContactBuilder.Name.createDisplayName(info.name))
+                .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number))
+                .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url))
+                .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+                .build());
+          }
+          return result;
+      }
+    } catch (IOException e) {
+      Log.e(TAG, "People lookup failed", e);
+    }
+    return null;
+  }
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java
new file mode 100644
index 0000000..6b6f415
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.lookup.auskunft;
+
+import android.content.Context;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+import java.util.List;
+
+public class AuskunftReverseLookup extends ReverseLookup {
+  public AuskunftReverseLookup(Context context) {
+  }
+
+  @Override
+  public ContactInfo lookupNumber(Context context, String normalizedNumber,
+      String formattedNumber) throws IOException {
+    // only Austrian numbers are supported
+    if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+43")) {
+      return null;
+    }
+
+    // query the API and return null if nothing found or general error
+    List<AuskunftApi.ContactInfo> infos = AuskunftApi.query(normalizedNumber);
+    AuskunftApi.ContactInfo info = infos != null && !infos.isEmpty() ? infos.get(0) : null;
+    if (info == null) {
+      return null;
+    }
+
+    return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+        .setName(ContactBuilder.Name.createDisplayName(info.name))
+        .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number))
+        .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url))
+        .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+        .build();
+  }
+}
diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java
new file mode 100644
index 0000000..ab1eb40
--- /dev/null
+++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de>
+ *
+ * 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.lookup.dastelefonbuch;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+
+public class TelefonbuchApi {
+  private static final String TAG = TelefonbuchApi.class.getSimpleName();
+
+  private static final String REVERSE_LOOKUP_URL =
+      "https://www.dastelefonbuch.de/?s=a20000" +
+      "&cmd=search&sort_ok=0&sp=55&vert_ok=0&aktion=23";
+
+  private static String NAME_REGEX ="<a id=\"name0.*?>\\s*\n?(.*?)\n?\\s*</a>";
+  private static String NUMBER_REGEX = "<span\\s+class=\"ico fon.*>.*<span>(.*?)</span><br/>";
+  private static String ADDRESS_REGEX = "<address.*?>\n?(.*?)</address>";
+
+  private TelefonbuchApi() {
+  }
+
+  public static ContactInfo reverseLookup(Context context, String number) throws IOException {
+    Uri uri = Uri.parse(REVERSE_LOOKUP_URL)
+        .buildUpon()
+        .appendQueryParameter("kw", number)
+        .build();
+    // Cut out everything we're not interested in (scripts etc.) to
+    // speed up the subsequent matching.
+    String output = LookupUtils.firstRegexResult(
+        LookupUtils.httpGet(uri.toString(), null), ": Treffer(.*)Ende Treffer", true);
+
+    String name = parseValue(output, NAME_REGEX, true, false);
+    if (name == null) {
+      return null;
+    }
+
+    String phoneNumber = parseValue(output, NUMBER_REGEX, false, true);
+    String address = parseValue(output, ADDRESS_REGEX, true, true);
+
+    ContactInfo info = new ContactInfo();
+    info.name = name;
+    info.address = address;
+    info.formattedNumber = phoneNumber != null ? phoneNumber : number;
+    info.website = uri.toString();
+
+    return info;
+  }
+
+  private static String parseValue(String output, String regex,
+      boolean dotall, boolean removeSpans) {
+    String result = LookupUtils.firstRegexResult(output, regex, dotall);
+    if (result != null && removeSpans) {
+      // completely remove hidden spans (including contents) ...
+      result = result.replaceAll("<span class=\"hide\".*?\\/span>", "");
+      // ... and remove span wrappers around data content
+      result = result.replaceAll("</?span.*?>", "");
+    }
+    return LookupUtils.fromHtml(result);
+  }
+
+  public static class ContactInfo {
+    String name;
+    String address;
+    String formattedNumber;
+    String website;
+  }
+}
diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java
new file mode 100644
index 0000000..cd89499
--- /dev/null
+++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de>
+ *
+ * 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.lookup.dastelefonbuch;
+
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class TelefonbuchReverseLookup extends ReverseLookup {
+  private static final String TAG = TelefonbuchReverseLookup.class.getSimpleName();
+
+  public TelefonbuchReverseLookup(Context context) {
+  }
+
+  /**
+   * Perform phone number lookup.
+   *
+   * @param context The application context
+   * @param normalizedNumber The normalized phone number
+   * @param formattedNumber The formatted phone number
+   * @return The phone number info object
+   */
+  @Override
+  public ContactInfo lookupNumber(Context context,
+      String normalizedNumber, String formattedNumber) throws IOException {
+    if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+49")) {
+      // Das Telefonbuch only supports German numbers
+      return null;
+    }
+
+    TelefonbuchApi.ContactInfo info = TelefonbuchApi.reverseLookup(context, normalizedNumber);
+    if (info == null) {
+      return null;
+    }
+
+    return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+        .setName(ContactBuilder.Name.createDisplayName(info.name))
+        .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+        .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website))
+        .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+        .build();
+  }
+}
diff --git a/java/com/android/dialer/lookup/google/GoogleForwardLookup.java b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java
new file mode 100644
index 0000000..bae11c4
--- /dev/null
+++ b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.google;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.Html;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ForwardLookup;
+import com.android.dialer.lookup.LookupUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GoogleForwardLookup extends ForwardLookup {
+  private static final String TAG = GoogleForwardLookup.class.getSimpleName();
+
+  private static final boolean DEBUG = false;
+
+  private static final String QUERY_FILTER = "q";
+  private static final String QUERY_LANGUAGE = "hl";
+  private static final String QUERY_LOCATION = "sll";
+  private static final String QUERY_RADIUS = "radius";
+  private static final String QUERY_RANDOM = "gs_gbg";
+
+  private static final String RESULT_ADDRESS = "a";
+  private static final String RESULT_NUMBER = "b";
+  private static final String RESULT_DISTANCE = "c";
+  private static final String RESULT_PHOTO_URI = "d";
+  private static final String RESULT_WEBSITE = "f";
+  private static final String RESULT_CITY = "g";
+
+  /** Base for the query URL */
+  private static final String LOOKUP_URL = "https://www.google.com/complete/search?gs_ri=dialer";
+
+  /** Minimum query length
+   * (default for dialer_nearby_places_min_query_len) */
+  private static final int MIN_QUERY_LEN = 2;
+
+  /** Maximum query length
+   * (default for dialer_nearby_places_max_query_len) */
+  private static final int MAX_QUERY_LEN = 50;
+
+  /** Radius (in miles)
+   * (default for dialer_nearby_places_directory_radius_meters) */
+  private static final int RADIUS = 1000;
+
+  /** User agent string */
+  private final String userAgent;
+
+  public GoogleForwardLookup(Context context) {
+    StringBuilder sb = new StringBuilder("GoogleDialer ");
+    try {
+      sb.append(context.getPackageManager().getPackageInfo(
+          context.getPackageName(), 0).versionName);
+      sb.append(" ");
+      sb.append(Build.FINGERPRINT);
+    } catch (PackageManager.NameNotFoundException e) {
+      sb.setLength(0);
+    }
+    userAgent = sb.toString();
+  }
+
+  @Override
+  public List<ContactInfo> lookup(Context context, String filter, Location lastLocation) {
+    int length = filter.length();
+
+    if (length >= MIN_QUERY_LEN) {
+      if (length > MAX_QUERY_LEN) {
+        filter = filter.substring(0, MAX_QUERY_LEN);
+      }
+
+      try {
+        Uri.Builder builder = Uri.parse(LOOKUP_URL).buildUpon();
+
+        // Query string
+        builder.appendQueryParameter(QUERY_FILTER, filter);
+
+        // Language
+        builder.appendQueryParameter(QUERY_LANGUAGE,
+            context.getResources().getConfiguration().locale.getLanguage());
+
+        // Location (latitude and longitude)
+        builder.appendQueryParameter(QUERY_LOCATION,
+            String.format("%f,%f", lastLocation.getLatitude(), lastLocation.getLongitude()));
+
+        // Radius distance
+        builder.appendQueryParameter(QUERY_RADIUS, Integer.toString(RADIUS));
+
+        // Random string (not really required)
+        builder.appendQueryParameter(QUERY_RANDOM, getRandomNoiseString());
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("User-Agent", userAgent);
+        JSONArray results = new JSONArray(
+            LookupUtils.httpGet(builder.build().toString(), headers));
+
+        if (DEBUG) Log.v(TAG, "Results: " + results);
+
+        return getEntries(results);
+      } catch (IOException e) {
+        Log.e(TAG, "Failed to execute query", e);
+      } catch (JSONException e) {
+        Log.e(TAG, "JSON error", e);
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Parse JSON results and return them as an array of ContactInfo
+   *
+   * @param results The JSON results returned from the server
+   * @return Array of ContactInfo containing the result information
+   */
+  private List<ContactInfo> getEntries(JSONArray results) throws JSONException {
+    ArrayList<ContactInfo> details = new ArrayList<>();
+    JSONArray entries = results.getJSONArray(1);
+
+    for (int i = 0; i < entries.length(); i++) {
+      try {
+        JSONArray entry = entries.getJSONArray(i);
+
+        String displayName = decodeHtml(entry.getString(0));
+
+        JSONObject params = entry.getJSONObject(3);
+
+        String phoneNumber = decodeHtml(params.getString(RESULT_NUMBER));
+
+        String address = decodeHtml(params.getString(RESULT_ADDRESS));
+        String city = decodeHtml(params.getString(RESULT_CITY));
+
+        String profileUrl = params.optString(RESULT_WEBSITE, null);
+        String photoUri = params.optString(RESULT_PHOTO_URI, null);
+
+        ContactBuilder.Address a = new ContactBuilder.Address();
+        a.formattedAddress = address;
+        a.city = city;
+        a.type = StructuredPostal.TYPE_WORK;
+
+        details.add(ContactBuilder.forForwardLookup(phoneNumber)
+            .setName(ContactBuilder.Name.createDisplayName(displayName))
+            .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber))
+            .addWebsite(ContactBuilder.WebsiteUrl.createProfile(profileUrl))
+            .addAddress(a)
+            .setPhotoUri(photoUri != null ? photoUri : ContactBuilder.PHOTO_URI_BUSINESS)
+            .build());
+      } catch (JSONException e) {
+        Log.e(TAG, "Skipping the suggestions at index " + i, e);
+      }
+    }
+
+    return details;
+  }
+
+  /**
+   * Generate a random string of alphanumeric characters of length [4, 36)
+   *
+   * @return Random alphanumeric string
+   */
+  private String getRandomNoiseString() {
+    StringBuilder garbage = new StringBuilder();
+    int length = getRandomInteger(32) + 4;
+
+    for (int i = 0; i < length; i++) {
+      int asciiCode;
+
+      if (Math.random() >= 0.3) {
+        if (Math.random() <= 0.5) {
+          // Lowercase letters
+          asciiCode = getRandomInteger(26) + 97;
+        } else {
+          // Uppercase letters
+          asciiCode = getRandomInteger(26) + 65;
+        }
+      } else {
+        // Numbers
+        asciiCode = getRandomInteger(10) + 48;
+      }
+
+      garbage.append(Character.toString((char) asciiCode));
+    }
+
+    return garbage.toString();
+  }
+
+  /**
+   * Generate number in the range [0, max).
+   *
+   * @param max Upper limit (non-inclusive)
+   * @return Random number inside [0, max)
+   */
+  private int getRandomInteger(int max) {
+    return (int) Math.floor(Math.random() * max);
+  }
+
+  /**
+   * Convert HTML to unformatted plain text.
+   *
+   * @param s HTML content
+   * @return Unformatted plain text
+   */
+  private String decodeHtml(String s) {
+    return Html.fromHtml(s).toString();
+  }
+}
diff --git a/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java
new file mode 100644
index 0000000..458dbc1
--- /dev/null
+++ b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.opencnam;
+
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.LookupUtils;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class OpenCnamReverseLookup extends ReverseLookup {
+  private static final String TAG = OpenCnamReverseLookup.class.getSimpleName();
+
+  private static final boolean DEBUG = false;
+
+  private static final String LOOKUP_URL = "https://api.opencnam.com/v2/phone/";
+
+  /** Query parameters for paid accounts */
+  private static final String ACCOUNT_SID = "account_sid";
+  private static final String AUTH_TOKEN = "auth_token";
+
+  public OpenCnamReverseLookup(Context context) {
+  }
+
+  /**
+   * Perform phone number lookup.
+   *
+   * @param context The application context
+   * @param normalizedNumber The normalized phone number
+   * @param formattedNumber The formatted phone number
+   * @return The phone number info object
+   */
+  @Override
+  public ContactInfo lookupNumber(Context context,
+      String normalizedNumber, String formattedNumber) throws IOException {
+    if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+1")) {
+      // Any non-US number will return "We currently accept only US numbers"
+      return null;
+    }
+
+    String displayName = httpGetRequest(context, normalizedNumber);
+    if (DEBUG) Log.d(TAG, "Reverse lookup returned name: " + displayName);
+
+    // Check displayName. The free tier of the service will return the
+    // following for some numbers:
+    // "CNAM for phone "NORMALIZED" is currently unavailable for Hobbyist Tier users."
+
+    if (displayName.contains("Hobbyist Tier")) {
+      return null;
+    }
+
+    String number = formattedNumber != null ? formattedNumber : normalizedNumber;
+
+    return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+        .setName(ContactBuilder.Name.createDisplayName(displayName))
+        .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(number))
+        .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS)
+        .build();
+  }
+
+  private String httpGetRequest(Context context, String number) throws IOException {
+    Uri.Builder builder = Uri.parse(LOOKUP_URL + number).buildUpon();
+
+    // Paid account
+    String accountSid = LookupSettings.getOpenCnamAccountSid(context);
+    String authToken = LookupSettings.getOpenCnamAuthToken(context);
+
+    if (!TextUtils.isEmpty(accountSid) && !TextUtils.isEmpty(authToken)) {
+      Log.d(TAG, "Using paid account");
+
+      builder.appendQueryParameter(ACCOUNT_SID, accountSid);
+      builder.appendQueryParameter(AUTH_TOKEN, authToken);
+    }
+
+    return LookupUtils.httpGet(builder.build().toString(), null);
+  }
+}
diff --git a/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java
new file mode 100644
index 0000000..4482e60
--- /dev/null
+++ b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2014 The OmniROM Project
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.
+ */
+
+// Partially based on OmniROM's implementation
+
+package com.android.dialer.lookup.openstreetmap;
+
+import android.content.Context;
+import android.location.Location;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ForwardLookup;
+import com.android.dialer.lookup.LookupUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class OpenStreetMapForwardLookup extends ForwardLookup {
+  private static final String TAG = OpenStreetMapForwardLookup.class.getSimpleName();
+
+  /** Search within radius (meters) */
+  private static final int RADIUS = 30000;
+
+  /** Query URL */
+  private static final String LOOKUP_URL = "https://overpass-api.de/api/interpreter";
+  private static final String LOOKUP_QUERY =
+      "[out:json];node[name~\"%s\"][phone](around:%d,%f,%f);out body;";
+
+  private static final String RESULT_ELEMENTS = "elements";
+  private static final String RESULT_TAGS = "tags";
+  private static final String TAG_NAME = "name";
+  private static final String TAG_PHONE = "phone";
+  private static final String TAG_HOUSENUMBER = "addr:housenumber";
+  private static final String TAG_STREET = "addr:street";
+  private static final String TAG_CITY = "addr:city";
+  private static final String TAG_POSTCODE = "addr:postcode";
+  private static final String TAG_WEBSITE = "website";
+
+  public OpenStreetMapForwardLookup(Context context) {
+  }
+
+  @Override
+  public List<ContactInfo> lookup(Context context, String filter, Location lastLocation) {
+    // The OSM API doesn't support case-insentive searches, but does
+    // support regular expressions.
+    String regex = "";
+    for (int i = 0; i < filter.length(); i++) {
+      char c = filter.charAt(i);
+      regex += "[" + Character.toUpperCase(c) + Character.toLowerCase(c) + "]";
+    }
+
+    String request = String.format(Locale.ENGLISH, LOOKUP_QUERY, regex,
+        RADIUS, lastLocation.getLatitude(), lastLocation.getLongitude());
+
+    try {
+      return getEntries(new JSONObject(LookupUtils.httpPost(LOOKUP_URL, null, request)));
+    } catch (IOException e) {
+      Log.e(TAG, "Failed to execute query", e);
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON error", e);
+    }
+
+    return null;
+  }
+
+  private List<ContactInfo> getEntries(JSONObject results) throws JSONException {
+    ArrayList<ContactInfo> details = new ArrayList<>();
+    JSONArray elements = results.getJSONArray(RESULT_ELEMENTS);
+
+    for (int i = 0; i < elements.length(); i++) {
+      try {
+        JSONObject element = elements.getJSONObject(i);
+        JSONObject tags = element.getJSONObject(RESULT_TAGS);
+
+        String displayName = tags.getString(TAG_NAME);
+        String phoneNumber = tags.getString(TAG_PHONE);
+
+        // Take the first number if there are multiple
+        if (phoneNumber.contains(";")) {
+          phoneNumber = phoneNumber.split(";")[0];
+          phoneNumber = phoneNumber.trim();
+        }
+
+        // The address is split
+        String addressHouseNumber = tags.optString(TAG_HOUSENUMBER, null);
+        String addressStreet = tags.optString(TAG_STREET, null);
+        String addressCity = tags.optString(TAG_CITY, null);
+        String addressPostCode = tags.optString(TAG_POSTCODE, null);
+
+        String address = String.format("%s %s, %s %s",
+            addressHouseNumber != null ? addressHouseNumber : "",
+            addressStreet != null ? addressStreet : "",
+            addressCity != null ? addressCity : "",
+            addressPostCode != null ? addressPostCode : "");
+
+        address = address.trim().replaceAll("\\s+", " ");
+        if (address.isEmpty()) {
+            address = null;
+        }
+
+        ContactBuilder builder = ContactBuilder.forForwardLookup(phoneNumber)
+            .setName(ContactBuilder.Name.createDisplayName(displayName))
+            .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber))
+            .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS);
+
+        if (address != null) {
+            ContactBuilder.Address a = new ContactBuilder.Address();
+            a.formattedAddress = address;
+            a.city = addressCity;
+            a.street = addressStreet;
+            a.postCode = addressPostCode;
+            a.type = StructuredPostal.TYPE_WORK;
+            builder.addAddress(a);
+        }
+
+        String website = tags.optString(TAG_WEBSITE, null);
+        if (website != null) {
+            ContactBuilder.WebsiteUrl w = new ContactBuilder.WebsiteUrl();
+            w.url = website;
+            w.type = Website.TYPE_HOMEPAGE;
+            builder.addWebsite(w);
+        }
+
+        details.add(builder.build());
+      } catch (JSONException e) {
+        Log.e(TAG, "Skipping the suggestions at index " + i, e);
+      }
+    }
+
+    return details;
+  }
+}
diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..f0bbe73
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..f70e8e7
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..6409ab1
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..7c92a60
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..97b9822
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..43029bd
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/values/cm_arrays.xml b/java/com/android/dialer/lookup/res/values/cm_arrays.xml
new file mode 100644
index 0000000..a566727
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/values/cm_arrays.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string-array name="forward_lookup_providers" translatable="false">
+        <item>Google</item>
+        <item>OpenStreetMap</item>
+    </string-array>
+
+    <string-array name="forward_lookup_provider_names" translatable="false">
+        <item>Google</item>
+        <item>OpenStreetMap</item>
+    </string-array>
+
+    <string-array name="people_lookup_providers" translatable="false">
+        <item>Auskunft</item>
+    </string-array>
+
+    <string-array name="people_lookup_provider_names" translatable="false">
+        <item>Auskunft (AT)</item>
+    </string-array>
+
+    <string-array name="reverse_lookup_providers" translatable="false">
+        <item>Auskunft</item>
+        <item>DasTelefonbuch</item>
+        <item>OpenCnam</item>
+        <item>YellowPages</item>
+        <item>YellowPages_CA</item>
+        <item>ZabaSearch</item>
+    </string-array>
+
+    <string-array name="reverse_lookup_provider_names" translatable="false">
+        <item>Auskunft (AT)</item>
+        <item>Das Telefonbuch (DE)</item>
+        <item>OpenCnam (US)</item>
+        <item>YellowPages (US)</item>
+        <item>YellowPages (CA)</item>
+        <item>ZabaSearch (US)</item>
+    </string-array>
+</resources>
diff --git a/java/com/android/dialer/lookup/res/values/cm_strings.xml b/java/com/android/dialer/lookup/res/values/cm_strings.xml
new file mode 100644
index 0000000..fed7c00
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/values/cm_strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Forward lookup -->
+    <string name="nearby_places">Nearby places</string>
+    <string name="people">People</string>
+
+    <!-- Number lookup -->
+    <string name="lookup_settings_label">Phone number lookup</string>
+    <string name="enable_forward_lookup_title">Forward lookup</string>
+    <string name="enable_forward_lookup_summary">Show nearby places when searching in the dialer</string>
+    <string name="enable_people_lookup_title">People lookup</string>
+    <string name="enable_people_lookup_summary">Show online results for people when searching in the dialer</string>
+    <string name="enable_reverse_lookup_title">Reverse lookup</string>
+    <string name="enable_reverse_lookup_summary">Look up information about the person or place for unknown numbers on incoming calls</string>
+    <string name="forward_lookup_provider_title">Forward lookup provider</string>
+    <string name="people_lookup_provider_title">People lookup provider</string>
+    <string name="reverse_lookup_provider_title">Reverse lookup provider</string>
+
+    <!-- Disclaimer -->
+    <string name="lookup_disclaimer">Lookups may send queries over a secure protocol (https) to remote websites to gather information. The query may include the other party\'s phone number or the search query</string>
+</resources>
diff --git a/java/com/android/dialer/lookup/res/xml/lookup_settings.xml b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml
new file mode 100644
index 0000000..006345f
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2014 The CyanogenMod 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
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <SwitchPreference
+        android:key="enable_forward_lookup"
+        android:title="@string/enable_forward_lookup_title"
+        android:summary="@string/enable_forward_lookup_summary"
+        android:defaultValue="false"
+        android:persistent="false" />
+
+    <ListPreference
+        android:key="forward_lookup_provider"
+        android:title="@string/forward_lookup_provider_title"
+        android:entries="@array/forward_lookup_provider_names"
+        android:entryValues="@array/forward_lookup_providers"
+        android:dependency="enable_forward_lookup"
+        android:summary="%s"
+        android:persistent="false" />
+
+    <SwitchPreference
+        android:key="enable_people_lookup"
+        android:title="@string/enable_people_lookup_title"
+        android:summary="@string/enable_people_lookup_summary"
+        android:defaultValue="false"
+        android:persistent="false" />
+
+    <ListPreference
+        android:key="people_lookup_provider"
+        android:title="@string/people_lookup_provider_title"
+        android:entries="@array/people_lookup_provider_names"
+        android:entryValues="@array/people_lookup_providers"
+        android:summary="%s"
+        android:dependency="enable_people_lookup"
+        android:persistent="false" />
+
+    <SwitchPreference
+        android:key="enable_reverse_lookup"
+        android:title="@string/enable_reverse_lookup_title"
+        android:summary="@string/enable_reverse_lookup_summary"
+        android:defaultValue="false"
+        android:persistent="false" />
+
+    <ListPreference
+        android:key="reverse_lookup_provider"
+        android:title="@string/reverse_lookup_provider_title"
+        android:entries="@array/reverse_lookup_provider_names"
+        android:entryValues="@array/reverse_lookup_providers"
+        android:dependency="enable_reverse_lookup"
+        android:summary="%s"
+        android:persistent="false" />
+
+    <Preference android:summary="@string/lookup_disclaimer" />
+</PreferenceScreen>
+
diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java
new file mode 100644
index 0000000..30d5aaf
--- /dev/null
+++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.yellowpages;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class YellowPagesApi {
+  private static final String TAG = YellowPagesApi.class.getSimpleName();
+
+  static final String LOOKUP_URL_UNITED_STATES =
+      "https://www.yellowpages.com/phone?phone_search_terms=";
+  static final String LOOKUP_URL_CANADA =
+      "https://www.yellowpages.ca/search/si/1/";
+
+  private final String number;
+  private String output = null;
+  private ContactInfo info = null;
+  private final String lookupUrl;
+
+  public YellowPagesApi(String number, String lookupUrl) {
+    this.number = number;
+    this.lookupUrl = lookupUrl;
+  }
+
+  private void fetchPage() throws IOException {
+    output = LookupUtils.httpGet(lookupUrl + number, null);
+  }
+
+  private String getPhotoUrl(String website) throws IOException {
+    String output = LookupUtils.httpGet(website, null);
+    String galleryRef = LookupUtils.firstRegexResult(output,
+        "href=\"([^\"]+gallery\\?lid=[^\"]+)\"", true);
+    if (galleryRef == null) {
+      return null;
+    }
+
+    // Get first image
+    return LookupUtils.firstRegexResult(
+        LookupUtils.httpGet("https://www.yellowpages.com" + galleryRef, null),
+        "\"type\":\"image\",\"src\":\"([^\"]+)\"", true);
+  }
+
+  private String[] parseNameWebsiteUnitedStates() {
+    Pattern regexNameAndWebsite = Pattern.compile(
+        "<a href=\"([^>]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)</a>",
+        Pattern.DOTALL);
+    String name = null;
+    String website = null;
+
+    Matcher m = regexNameAndWebsite.matcher(output);
+    if (m.find()) {
+      website = m.group(1).trim();
+      name = m.group(2).trim();
+    }
+
+    return new String[] { name, website };
+  }
+
+  private String[] parseNameWebsiteCanada() {
+    Pattern regexNameAndWebsite = Pattern.compile(
+        "class=\"ypgListingTitleLink utagLink\".*?href=\"(.*?)\">"
+        + "(<span\\s+class=\"listingTitle\">.*?</span>)",
+        Pattern.DOTALL);
+    String name = null;
+    String website = null;
+
+    Matcher m = regexNameAndWebsite.matcher(output);
+    if (m.find()) {
+      website = m.group(1).trim();
+      name = LookupUtils.fromHtml(m.group(2).trim());
+    }
+
+    if (website != null) {
+      website = "https://www.yellowpages.ca" + website;
+    }
+
+    return new String[] { name, website };
+  }
+
+  private String parseNumberUnitedStates() {
+    return LookupUtils.firstRegexResult(output,
+        "business-phone.*?>\n*([^\n<]+)\n*<", true);
+  }
+
+  private String parseNumberCanada() {
+    return LookupUtils.firstRegexResult(output,
+        "<div\\s+class=\"phoneNumber\">(.*?)</div>", true);
+  }
+
+  private String parseAddressUnitedStates() {
+    String addressStreet = LookupUtils.firstRegexResult(output,
+        "street-address.*?>\n*([^\n<]+)\n*<", true);
+    if (addressStreet != null && addressStreet.endsWith(",")) {
+      addressStreet = addressStreet.substring(0, addressStreet.length() - 1);
+    }
+
+    String addressCity = LookupUtils.firstRegexResult(output,
+        "locality.*?>\n*([^\n<]+)\n*<", true);
+    String addressState = LookupUtils.firstRegexResult(output,
+        "region.*?>\n*([^\n<]+)\n*<", true);
+    String addressZip = LookupUtils.firstRegexResult(output,
+        "postal-code.*?>\n*([^\n<]+)\n*<", true);
+
+    StringBuilder sb = new StringBuilder();
+
+    if (!TextUtils.isEmpty(addressStreet)) {
+      sb.append(addressStreet);
+    }
+    if (!TextUtils.isEmpty(addressCity)) {
+      sb.append(", ");
+      sb.append(addressCity);
+    }
+    if (!TextUtils.isEmpty(addressState)) {
+      sb.append(", ");
+      sb.append(addressState);
+    }
+    if (!TextUtils.isEmpty(addressZip)) {
+      sb.append(", ");
+      sb.append(addressZip);
+    }
+
+    String address = sb.toString();
+    return address.isEmpty() ? null : address;
+  }
+
+  private String parseAddressCanada() {
+    String address = LookupUtils.firstRegexResult(output,
+        "<div\\s+class=\"address\">(.*?)</div>", true);
+    return LookupUtils.fromHtml(address);
+  }
+
+  private void buildContactInfo() throws IOException {
+    Matcher m;
+
+    String name = null;
+    String website = null;
+    String phoneNumber = null;
+    String address = null;
+    String photoUrl = null;
+
+    if (lookupUrl.equals(LOOKUP_URL_UNITED_STATES)) {
+      String[] ret = parseNameWebsiteUnitedStates();
+      name = ret[0];
+      website = ret[1];
+      phoneNumber = parseNumberUnitedStates();
+      address = parseAddressUnitedStates();
+      if (website != null) {
+        photoUrl = getPhotoUrl(website);
+      }
+    } else {
+      String[] ret = parseNameWebsiteCanada();
+      name = ret[0];
+      website = ret[1];
+      phoneNumber = parseNumberCanada();
+      address = parseAddressCanada();
+      // AFAIK, Canada's YellowPages doesn't have photos
+    }
+
+    info = new ContactInfo();
+    info.name = name;
+    info.address = address;
+    info.formattedNumber = phoneNumber != null ? phoneNumber : number;
+    info.website = website;
+    info.photoUrl = photoUrl;
+  }
+
+  public ContactInfo getContactInfo() throws IOException {
+    if (info == null) {
+      fetchPage();
+      buildContactInfo();
+    }
+
+    return info;
+  }
+
+  public static class ContactInfo {
+    String name;
+    String address;
+    String formattedNumber;
+    String website;
+    String photoUrl;
+  }
+}
diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java
new file mode 100644
index 0000000..5638df6
--- /dev/null
+++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.yellowpages;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.LookupUtils;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class YellowPagesReverseLookup extends ReverseLookup {
+  private static final String TAG = YellowPagesReverseLookup.class.getSimpleName();
+
+  private final String type;
+
+  public YellowPagesReverseLookup(Context context, String type) {
+    this.type = type;
+  }
+
+  /**
+   * Lookup image
+   *
+   * @param context The application context
+   * @param uri The image URI
+   */
+  @Override
+  public Bitmap lookupImage(Context context, Uri uri) {
+    if (uri == null) {
+      throw new NullPointerException("URI is null");
+    }
+
+    Log.e(TAG, "Fetching " + uri);
+
+    String scheme = uri.getScheme();
+
+    if (scheme.startsWith("http")) {
+      try {
+        byte[] response = LookupUtils.httpGetBytes(uri.toString(), null);
+        return BitmapFactory.decodeByteArray(response, 0, response.length);
+      } catch (IOException e) {
+        Log.e(TAG, "Failed to retrieve image", e);
+      }
+    } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+      try {
+        ContentResolver cr = context.getContentResolver();
+        return BitmapFactory.decodeStream(cr.openInputStream(uri));
+      } catch (FileNotFoundException e) {
+        Log.e(TAG, "Failed to retrieve image", e);
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Perform phone number lookup.
+   *
+   * @param context The application context
+   * @param normalizedNumber The normalized phone number
+   * @param formattedNumber The formatted phone number
+   * @return The phone number info object
+   */
+  @Override
+  public ContactInfo lookupNumber(Context context,
+      String normalizedNumber, String formattedNumber) throws IOException {
+    String lookupUrl = type.equals(LookupSettings.RLP_YELLOWPAGES_CA)
+        ? YellowPagesApi.LOOKUP_URL_CANADA : YellowPagesApi.LOOKUP_URL_UNITED_STATES;
+    YellowPagesApi ypa = new YellowPagesApi(normalizedNumber, lookupUrl);
+    YellowPagesApi.ContactInfo info = ypa.getContactInfo();
+    if (info.name == null) {
+        return null;
+    }
+
+    ContactBuilder builder = ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+        .setName(ContactBuilder.Name.createDisplayName(info.name))
+        .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+        .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website));
+
+    if (info.address != null) {
+      ContactBuilder.Address a = new ContactBuilder.Address();
+      a.formattedAddress = info.address;
+      a.type = StructuredPostal.TYPE_WORK;
+      builder.addAddress(a);
+    }
+
+    if (info.photoUrl != null) {
+      builder.setPhotoUri(info.photoUrl);
+    } else {
+      builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS);
+    }
+
+    return builder.build();
+  }
+}
diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java
new file mode 100644
index 0000000..6118740
--- /dev/null
+++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.zabasearch;
+
+import android.text.TextUtils;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+
+public class ZabaSearchApi {
+  private static final String TAG = ZabaSearchApi.class.getSimpleName();
+
+  private static final String LOOKUP_URL = "https://www.zabasearch.com/phone/";
+
+  private final String number;
+  public String output = null;
+  private ContactInfo info = null;
+
+  public ZabaSearchApi(String number) {
+    this.number = number;
+  }
+
+  private void fetchPage() throws IOException {
+    output = LookupUtils.httpGet(LOOKUP_URL + number, null);
+  }
+
+  private void buildContactInfo() {
+    // Name
+    String name = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?name\"?>([^<]+)<", true);
+    // Formatted phone number
+    String phoneNumber = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?telephone\"?>([^<]+)<", true);
+    // Address
+    String addressStreet = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?streetAddress\"?>([^<]+?)(&nbsp;)*<", true);
+    String addressCity = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?addressLocality\"?>([^<]+)<", true);
+    String addressState = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?addressRegion\"?>([^<]+)<", true);
+    String addressZip = LookupUtils.firstRegexResult(output,
+        "itemprop=\"?postalCode\"?>([^<]+)<", true);
+
+    StringBuilder sb = new StringBuilder();
+
+    if (!TextUtils.isEmpty(addressStreet)) {
+      sb.append(addressStreet);
+    }
+    if (!TextUtils.isEmpty(addressCity)) {
+      sb.append(", ");
+      sb.append(addressCity);
+    }
+    if (!TextUtils.isEmpty(addressState)) {
+      sb.append(", ");
+      sb.append(addressState);
+    }
+    if (!TextUtils.isEmpty(addressZip)) {
+      sb.append(", ");
+      sb.append(addressZip);
+    }
+
+    String address = sb.toString();
+    if (address.isEmpty()) {
+        address = null;
+    }
+
+    info = new ContactInfo();
+    info.name = name;
+    info.address = address;
+    info.formattedNumber = number;
+    info.website = LOOKUP_URL + info.formattedNumber;
+  }
+
+  public ContactInfo getContactInfo() throws IOException {
+    if (info == null) {
+      fetchPage();
+      buildContactInfo();
+    }
+
+    return info;
+  }
+
+  public static class ContactInfo {
+    String name;
+    String address;
+    String formattedNumber;
+    String website;
+  }
+}
diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java
new file mode 100644
index 0000000..5c6608b
--- /dev/null
+++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.lookup.zabasearch;
+
+import android.content.Context;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class ZabaSearchReverseLookup extends ReverseLookup {
+  private static final String TAG = ZabaSearchReverseLookup.class.getSimpleName();
+
+  public ZabaSearchReverseLookup(Context context) {
+  }
+
+  /**
+   * Perform phone number lookup.
+   *
+   * @param context The application context
+   * @param normalizedNumber The normalized phone number
+   * @param formattedNumber The formatted phone number
+   * @return The phone number info object
+   */
+  @Override
+  public ContactInfo lookupNumber(Context context,
+      String normalizedNumber, String formattedNumber) throws IOException {
+    ZabaSearchApi zsa = new ZabaSearchApi(normalizedNumber);
+    ZabaSearchApi.ContactInfo info = zsa.getContactInfo();
+    if (info.name == null) {
+        return null;
+    }
+
+    return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+        .setName(ContactBuilder.Name.createDisplayName(info.name))
+        .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+        .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website))
+        .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+        .build();
+  }
+}
diff --git a/java/com/android/dialer/main/impl/AndroidManifest.xml b/java/com/android/dialer/main/impl/AndroidManifest.xml
index a1ed5eb..55eea5d 100644
--- a/java/com/android/dialer/main/impl/AndroidManifest.xml
+++ b/java/com/android/dialer/main/impl/AndroidManifest.xml
@@ -30,7 +30,7 @@
         android:label="@string/main_activity_label"
         android:launchMode="singleTask"
         android:resizeableActivity="true"
-        android:theme="@style/MainActivityTheme"
+        android:theme="@style/LaunchTheme"
         android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
 
       <intent-filter>
diff --git a/java/com/android/dialer/main/impl/MainActivity.java b/java/com/android/dialer/main/impl/MainActivity.java
index 1129609..f539bdc 100644
--- a/java/com/android/dialer/main/impl/MainActivity.java
+++ b/java/com/android/dialer/main/impl/MainActivity.java
@@ -29,6 +29,7 @@
 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorListener;
 import com.android.dialer.main.MainActivityPeer;
 import com.android.dialer.main.impl.bottomnav.BottomNavBar.TabIndex;
+import com.android.dialer.R;
 import com.android.dialer.util.TransactionSafeActivity;
 
 /** This is the main activity for dialer. It hosts favorites, call log, search, dialpad, etc... */
@@ -72,6 +73,7 @@
 
   @Override
   protected void onCreate(Bundle savedInstanceState) {
+    setTheme(R.style.MainActivityTheme);
     super.onCreate(savedInstanceState);
     LogUtil.enterBlock("MainActivity.onCreate");
     // If peer was set by the super, don't reset it.
diff --git a/java/com/android/dialer/main/impl/OldMainActivityPeer.java b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
index feb76ac..a1afc61 100644
--- a/java/com/android/dialer/main/impl/OldMainActivityPeer.java
+++ b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
@@ -639,7 +639,7 @@
                 R.string.view_conversation,
                 v ->
                     activity.startActivity(
-                        IntentProvider.getSendSmsIntentProvider(number).getIntent(activity)))
+                        IntentProvider.getSendSmsIntentProvider(number).getClickIntent(activity)))
             .setActionTextColor(
                 ContextCompat.getColor(activity, R.color.dialer_snackbar_action_text_color))
             .show();
diff --git a/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java b/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
index 4794b88..4cba2a4 100644
--- a/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
+++ b/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
@@ -56,8 +56,8 @@
     super.setSelected(selected);
     int colorId =
         selected
-            ? ThemeComponent.get(getContext()).theme().getColorPrimary()
-            : ThemeComponent.get(getContext()).theme().getTextColorSecondary();
+            ? getContext().getResources().getColor(R.color.nav_item_selected)
+            : getContext().getResources().getColor(R.color.nav_item);
     image.setImageTintList(ColorStateList.valueOf(colorId));
     text.setTextColor(colorId);
   }
diff --git a/java/com/android/dialer/main/impl/res/values-night/styles.xml b/java/com/android/dialer/main/impl/res/values-night/styles.xml
new file mode 100644
index 0000000..63d4e9c
--- /dev/null
+++ b/java/com/android/dialer/main/impl/res/values-night/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+<resources>
+
+  <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+    <item name="android:colorPrimary">@color/dialer_theme_color</item>
+    <item name="colorPrimary">@color/dialer_theme_color</item>
+    <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+    <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
+    <item name="android:windowLightStatusBar">false</item>
+  </style>
+
+</resources>
diff --git a/java/com/android/dialer/main/impl/res/values/styles.xml b/java/com/android/dialer/main/impl/res/values/styles.xml
index 25f247e..4b153b6 100644
--- a/java/com/android/dialer/main/impl/res/values/styles.xml
+++ b/java/com/android/dialer/main/impl/res/values/styles.xml
@@ -16,6 +16,14 @@
   -->
 <resources>
 
+  <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+    <item name="android:colorPrimary">@color/dialer_theme_color</item>
+    <item name="colorPrimary">@color/dialer_theme_color</item>
+    <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+    <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
+    <item name="android:windowLightStatusBar">true</item>
+  </style>
+
   <!-- Activities should use this theme as their style -->
   <style name="MainActivityTheme" parent="MainActivityThemeBase"/>
 
@@ -28,6 +36,8 @@
     <item name="actionModeStyle">@style/MainActionModeStyle</item>
     <item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
     <item name="dialpad_style">@style/Dialpad.Themed</item>
+    <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+    <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
   </style>
 
   <style name="MainActivityThemeBase.Dark" parent="Dialer.Dark.ThemeBase.NoActionBar">
diff --git a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
index 88f5a04..2130db4 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
@@ -14,8 +14,15 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-  android:shape="rectangle">
-  <solid android:color="?android:attr/colorBackgroundFloating"/>
-  <corners android:radius="2dp"/>
-</shape>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/dialer_ripple_color">
+
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/white" />
+            <corners android:radius="8dp" />
+        </shape>
+    </item>
+
+  <item android:drawable="@drawable/search_bar_background_rounded_corners_shape" />
+</ripple>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml
new file mode 100644
index 0000000..ee3219e
--- /dev/null
+++ b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+  android:shape="rectangle">
+  <solid android:color="?attr/colorBackgroundSearchBar"/>
+  <corners android:radius="8dp"/>
+</shape>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml b/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
index a7b3aeb..d9051a6 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
@@ -29,7 +29,7 @@
       android:background="?attr/selectableItemBackgroundBorderless"
       android:contentDescription="@string/action_menu_back_from_search"
       android:src="@drawable/quantum_ic_arrow_back_vd_theme_24"
-      android:tint="?android:attr/colorPrimary"/>
+      android:tint="?colorIcon"/>
 
   <EditText
       android:id="@+id/search_view"
@@ -62,4 +62,4 @@
       android:src="@drawable/quantum_ic_close_vd_theme_24"
       android:tint="?colorIcon"
       android:visibility="gone"/>
-</RelativeLayout>
\ No newline at end of file
+</RelativeLayout>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml b/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
index 049cf2e..129aac9 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
@@ -18,7 +18,6 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="@dimen/expanded_search_bar_height"
-    android:background="?android:attr/colorPrimary"
     app:contentInsetEnd="0dp"
     app:contentInsetStart="0dp">
   <FrameLayout
@@ -38,7 +37,6 @@
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="center_vertical"
-          android:background="?android:attr/selectableItemBackground"
           android:gravity="center_vertical">
 
         <ImageView
diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags
index 6d5d373..3b8fe2c 100644
--- a/java/com/android/dialer/proguard/proguard_base.flags
+++ b/java/com/android/dialer/proguard/proguard_base.flags
@@ -71,3 +71,8 @@
 
 # AOSP support library:  Handle classes that use reflection.
 -dontnote android.support.v4.app.NotificationCompatJellybean
+
+-keep class android.support.design.widget.AppBarLayout$ScrollingViewBehavior {
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>();
+}
diff --git a/java/com/android/dialer/theme/base/res/values/attr.xml b/java/com/android/dialer/theme/base/res/values/attr.xml
index 606d161..790ae70 100644
--- a/java/com/android/dialer/theme/base/res/values/attr.xml
+++ b/java/com/android/dialer/theme/base/res/values/attr.xml
@@ -15,6 +15,8 @@
   ~ limitations under the License
   -->
 <resources>
+  <!-- Used to color the search bar background -->
+  <attr name="colorBackgroundSearchBar" format="color"/>
   <!-- Used to style all icons in Dialer. -->
   <attr name="colorIcon" format="color"/>
   <!-- Used to style some icons a little lighter in Dialer. -->
diff --git a/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml b/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
index db06df4..61e0a1b 100644
--- a/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
+++ b/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
@@ -27,6 +27,9 @@
     <item name="actionBarWidgetTheme">@null</item>
     <item name="actionBarTheme">@style/DialerActionBarBaseTheme</item>
     <item name="listChoiceBackgroundIndicator">@drawable/abc_list_selector_holo_dark</item>
+
+    <item name="android:colorPrimaryDark">@color/settings_primary_dark</item>
+    <item name="colorControlHighlight">@color/dialer_ripple_color</item>
   </style>
 
   <style name="Dialer.ThemeBase.NoActionBar">
@@ -34,13 +37,18 @@
         base style once none of our activities depend on ActionBar anymore. -->
     <item name="windowActionBar">false</item>
     <item name="windowNoTitle">true</item>
+
+    <item name="android:colorPrimaryDark">@color/settings_primary_dark</item>
+    <item name="colorControlHighlight">@color/dialer_ripple_color</item>
   </style>
 
   <!-- Activities and Applications should inherit from one of the themes above. -->
   <style name="Dialer.ThemeBase">
+    <item name="android:forceDarkAllowed">true</item>
     <!-- These values should be used to color all backgrounds. -->
     <item name="android:colorBackground">@color/dialer_background_color</item>
     <item name="android:colorBackgroundFloating">@android:color/white</item>
+    <item name="colorBackgroundSearchBar">@color/dialer_search_bar_color</item>
 
     <!-- These values should be used to set text color. -->
     <item name="android:textColorPrimary">@color/dialer_primary_text_color</item>
diff --git a/java/com/android/dialer/theme/common/res/values-night/colors.xml b/java/com/android/dialer/theme/common/res/values-night/colors.xml
new file mode 100644
index 0000000..aa42d8b
--- /dev/null
+++ b/java/com/android/dialer/theme/common/res/values-night/colors.xml
@@ -0,0 +1,21 @@
+<?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
+  -->
+<!-- The colors in this file aren't configured at the theme level. -->
+<resources>
+  <!-- Settings -->
+  <color name="dialer_settings_theme_color_dark">#1C3AA9</color>
+</resources>
diff --git a/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml b/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml
new file mode 100644
index 0000000..8d04c72
--- /dev/null
+++ b/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+<resources>
+  <!-- Essential theme colors -->
+  <color name="dialer_theme_color">#5195EA</color>
+  <color name="dialer_theme_color_20pct">#335195EA</color>
+  <color name="dialer_secondary_color">#5195EA</color>
+  <color name="dialer_ripple_color">#33ffffff</color>
+  <color name="dialer_search_bar_color">@color/google_grey_900</color>
+  <color name="settings_primary_dark">#2374CE</color>
+</resources>
diff --git a/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml b/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
index 988aad7..e4d1e29 100644
--- a/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
+++ b/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
@@ -17,7 +17,10 @@
   <!-- Essential theme colors -->
   <color name="dialer_theme_color">#2A56C6</color>
   <color name="dialer_theme_color_20pct">#332A56C6</color>
-  <color name="dialer_theme_color_dark">#1C3AA9</color>
+  <color name="dialer_theme_color_dark">@color/dialer_background_color</color>
   <color name="dialer_secondary_color">#F50057</color>
   <color name="dialer_background_color">#FAFAFA</color>
+  <color name="dialer_ripple_color">#1f000000</color>
+  <color name="dialer_search_bar_color">@color/google_grey_50</color>
+  <color name="settings_primary_dark">#1C3AA9</color>
 </resources>
diff --git a/java/com/android/dialer/util/CallUtil.java b/java/com/android/dialer/util/CallUtil.java
index 89af0b9..160a40f 100644
--- a/java/com/android/dialer/util/CallUtil.java
+++ b/java/com/android/dialer/util/CallUtil.java
@@ -24,6 +24,8 @@
 import android.telecom.TelecomManager;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+import java.util.ArrayList;
 import java.util.List;
 
 /** Utilities related to calls that can be used by non system apps. */
@@ -89,6 +91,25 @@
   }
 
   /**
+   * Returns a list of phone accounts that are able to call to numbers with the supplied scheme
+   */
+  public static List<PhoneAccount> getCallCapablePhoneAccounts(Context context, String scheme) {
+    if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)) {
+      return null;
+    }
+    TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+    final ArrayList<PhoneAccount> accounts = new ArrayList<>();
+
+    for (PhoneAccountHandle handle : tm.getCallCapablePhoneAccounts()) {
+      final PhoneAccount account = tm.getPhoneAccount(handle);
+      if (account != null && account.supportsUriScheme(scheme)) {
+        accounts.add(account);
+      }
+    }
+    return accounts;
+  }
+
+  /**
    * Determines if one of the call capable phone accounts defined supports video calling.
    *
    * @param context The context.
diff --git a/java/com/android/dialer/widget/LinearColorBar.java b/java/com/android/dialer/widget/LinearColorBar.java
new file mode 100644
index 0000000..b016264
--- /dev/null
+++ b/java/com/android/dialer/widget/LinearColorBar.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.widget.LinearLayout;
+
+import com.android.dialer.R;
+
+public class LinearColorBar extends LinearLayout {
+  private float mFirstRatio;
+  private float mSecondRatio;
+  private float mThirdRatio;
+  private float mFourthRatio;
+
+  private int mBackgroundColor;
+  private int mBlueColor;
+  private int mGreenColor;
+  private int mRedColor;
+  private int mOrangeColor;
+
+  final Rect mRect = new Rect();
+  final Paint mPaint = new Paint();
+
+  int mLastInterestingLeft, mLastInterestingRight;
+  int mLineWidth;
+
+  final Path mColorPath = new Path();
+  final Path mEdgePath = new Path();
+  final Paint mColorGradientPaint = new Paint();
+  final Paint mEdgeGradientPaint = new Paint();
+
+  public LinearColorBar(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    setWillNotDraw(false);
+
+    TypedArray a = context.obtainStyledAttributes(
+            attrs, R.styleable.LinearColorBar, 0, 0);
+    int n = a.getIndexCount();
+
+    for (int i = 0; i < n; i++) {
+      int attr = a.getIndex(i);
+
+      switch (attr) {
+        case R.styleable.LinearColorBar_backgroundColor:
+          mBackgroundColor = a.getColor(attr, 0);
+          break;
+        case R.styleable.LinearColorBar_redColor:
+          mRedColor = a.getColor(attr, 0);
+          break;
+        case R.styleable.LinearColorBar_greenColor:
+          mGreenColor = a.getColor(attr, 0);
+          break;
+        case R.styleable.LinearColorBar_blueColor:
+          mBlueColor = a.getColor(attr, 0);
+          break;
+        case R.styleable.LinearColorBar_orangeColor:
+          mOrangeColor = a.getColor(attr, 0);
+          break;
+      }
+    }
+
+    a.recycle();
+
+    mPaint.setStyle(Paint.Style.FILL);
+    mColorGradientPaint.setStyle(Paint.Style.FILL);
+    mColorGradientPaint.setAntiAlias(true);
+    mEdgeGradientPaint.setStyle(Paint.Style.STROKE);
+    mLineWidth = getResources().getDisplayMetrics().densityDpi >= DisplayMetrics.DENSITY_HIGH
+        ? 2 : 1;
+    mEdgeGradientPaint.setStrokeWidth(mLineWidth);
+    mEdgeGradientPaint.setAntiAlias(true);
+  }
+
+  public void setRatios(float blue, float green, float red, float orange) {
+    mFirstRatio = blue;
+    mSecondRatio = green;
+    mThirdRatio = red;
+    mFourthRatio = orange;
+    invalidate();
+  }
+
+  private void updateIndicator() {
+    int off = Math.max(0, getPaddingTop() - getPaddingBottom());
+    mRect.top = off;
+    mRect.bottom = getHeight();
+
+    mColorGradientPaint.setShader(new LinearGradient(
+        0, 0, 0, off - 2, mBackgroundColor & 0xffffff,
+        mBackgroundColor, Shader.TileMode.CLAMP));
+    mEdgeGradientPaint.setShader(new LinearGradient(
+        0, 0, 0, off / 2, 0x00a0a0a0, 0xffa0a0a0, Shader.TileMode.CLAMP));
+  }
+
+  @Override
+  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+    super.onSizeChanged(w, h, oldw, oldh);
+    updateIndicator();
+  }
+
+  @Override
+  protected void onDraw(Canvas canvas) {
+    super.onDraw(canvas);
+
+    int width = getWidth();
+
+    int left = 0;
+
+    int right = left + (int) (width * mFirstRatio);
+    int right2 = right + (int) (width * mSecondRatio);
+    int right3 = right2 + (int) (width * mThirdRatio);
+    int right4 = right3 + (int) (width * mFourthRatio);
+
+    int indicatorLeft = right4;
+    int indicatorRight = width;
+
+    if (mLastInterestingLeft != indicatorLeft || mLastInterestingRight != indicatorRight) {
+      mColorPath.reset();
+      mEdgePath.reset();
+      if (indicatorLeft < indicatorRight) {
+        final int midTopY = mRect.top;
+        final int midBottomY = 0;
+        final int xoff = 2;
+        mColorPath.moveTo(indicatorLeft, mRect.top);
+        mColorPath.cubicTo(indicatorLeft, midBottomY, -xoff, midTopY, -xoff, 0);
+        mColorPath.lineTo(width + xoff - 1, 0);
+        mColorPath.cubicTo(width + xoff - 1, midTopY,
+            indicatorRight, midBottomY, indicatorRight, mRect.top);
+        mColorPath.close();
+        final float lineOffset = mLineWidth + .5f;
+        mEdgePath.moveTo(-xoff + lineOffset, 0);
+        mEdgePath.cubicTo(-xoff + lineOffset, midTopY,
+            indicatorLeft + lineOffset, midBottomY, indicatorLeft + lineOffset, mRect.top);
+        mEdgePath.moveTo(width + xoff - 1 - lineOffset, 0);
+        mEdgePath.cubicTo(width + xoff - 1 - lineOffset, midTopY,
+            indicatorRight - lineOffset, midBottomY, indicatorRight - lineOffset, mRect.top);
+      }
+      mLastInterestingLeft = indicatorLeft;
+      mLastInterestingRight = indicatorRight;
+    }
+
+    if (!mEdgePath.isEmpty()) {
+      canvas.drawPath(mEdgePath, mEdgeGradientPaint);
+      canvas.drawPath(mColorPath, mColorGradientPaint);
+    }
+
+    if (left < right) {
+      mRect.left = left;
+      mRect.right = right;
+      mPaint.setColor(mBlueColor);
+      canvas.drawRect(mRect, mPaint);
+      width -= (right - left);
+      left = right;
+    }
+
+    right = right2;
+
+    if (left < right) {
+      mRect.left = left;
+      mRect.right = right;
+      mPaint.setColor(mGreenColor);
+      canvas.drawRect(mRect, mPaint);
+      width -= (right - left);
+      left = right;
+    }
+
+    right = right3;
+
+    if (left < right) {
+      mRect.left = left;
+      mRect.right = right;
+      mPaint.setColor(mRedColor);
+      canvas.drawRect(mRect, mPaint);
+      width -= (right - left);
+      left = right;
+    }
+
+    right = right4;
+
+    if (left < right) {
+      mRect.left = left;
+      mRect.right = right;
+      mPaint.setColor(mOrangeColor);
+      canvas.drawRect(mRect, mPaint);
+      width -= (right - left);
+      left = right;
+    }
+
+    right = left + width;
+    if (left < right) {
+      mRect.left = left;
+      mRect.right = right;
+      mPaint.setColor(mBackgroundColor);
+      canvas.drawRect(mRect, mPaint);
+    }
+  }
+}
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
index 7283702..b10e044 100644
--- a/java/com/android/incallui/AndroidManifest.xml
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -46,6 +46,9 @@
   <!-- Set audio selector window type TYPE_APPLICATION_OVERLAY -->
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
+  <!-- Required when the "Enable Do Not Disturb during call" setting is enabled -->
+  <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
+
   <!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper
   navigation. Otherwise system could bring up MainActivity instead, e.g. when user unmerge a
   call.
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index f2f7a40..0fa833e 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -16,9 +16,13 @@
 
 package com.android.incallui;
 
+import android.app.AlertDialog;
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Trace;
+import android.preference.PreferenceManager;
 import android.support.v4.app.Fragment;
 import android.support.v4.os.UserManagerCompat;
 import android.telecom.CallAudioState;
@@ -40,6 +44,7 @@
 import com.android.incallui.audiomode.AudioModeProvider;
 import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
 import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.CameraDirection;
 import com.android.incallui.call.DialerCallListener;
@@ -62,12 +67,33 @@
         InCallButtonUiDelegate,
         DialerCallListener {
 
+  private static final String KEY_RECORDING_WARNING_PRESENTED = "recording_warning_presented";
+
   private final Context context;
   private InCallButtonUi inCallButtonUi;
   private DialerCall call;
   private boolean isInCallButtonUiReady;
   private PhoneAccountHandle otherAccount;
 
+  private CallRecorder.RecordingProgressListener recordingProgressListener =
+      new CallRecorder.RecordingProgressListener() {
+    @Override
+    public void onStartRecording() {
+      inCallButtonUi.setCallRecordingState(true);
+      inCallButtonUi.setCallRecordingDuration(0);
+    }
+
+    @Override
+    public void onStopRecording() {
+      inCallButtonUi.setCallRecordingState(false);
+    }
+
+    @Override
+    public void onRecordingTimeProgress(final long elapsedTimeMs) {
+      inCallButtonUi.setCallRecordingDuration(elapsedTimeMs);
+    }
+  };
+
   public CallButtonPresenter(Context context) {
     this.context = context.getApplicationContext();
   }
@@ -86,6 +112,9 @@
     inCallPresenter.addCanAddCallListener(this);
     inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
 
+    CallRecorder recorder = CallRecorder.getInstance();
+    recorder.addRecordingProgressListener(recordingProgressListener);
+
     // Update the buttons state immediately for the current call
     onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
     isInCallButtonUiReady = true;
@@ -101,6 +130,10 @@
     InCallPresenter.getInstance().removeDetailsListener(this);
     InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
     InCallPresenter.getInstance().removeCanAddCallListener(this);
+
+    CallRecorder recorder = CallRecorder.getInstance();
+    recorder.removeRecordingProgressListener(recordingProgressListener);
+
     isInCallButtonUiReady = false;
 
     if (call != null) {
@@ -301,6 +334,52 @@
   }
 
   @Override
+  public void callRecordClicked(boolean checked) {
+    CallRecorder recorder = CallRecorder.getInstance();
+    if (checked) {
+      final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+      boolean warningPresented = prefs.getBoolean(KEY_RECORDING_WARNING_PRESENTED, false);
+      if (!warningPresented) {
+        new AlertDialog.Builder(getActivity())
+            .setTitle(R.string.recording_warning_title)
+            .setMessage(R.string.recording_warning_text)
+            .setPositiveButton(R.string.onscreenCallRecordText, (dialog, which) -> {
+              prefs.edit()
+                  .putBoolean(KEY_RECORDING_WARNING_PRESENTED, true)
+                  .apply();
+              startCallRecordingOrAskForPermission();
+            })
+            .setNegativeButton(android.R.string.cancel, null)
+            .show();
+      } else {
+        startCallRecordingOrAskForPermission();
+      }
+    } else {
+      if (recorder.isRecording()) {
+        recorder.finishRecording();
+      }
+    }
+  }
+
+  private void startCallRecordingOrAskForPermission() {
+    if (hasAllPermissions(CallRecorder.REQUIRED_PERMISSIONS)) {
+      CallRecorder recorder = CallRecorder.getInstance();
+      recorder.startRecording(call.getNumber(), call.getCreationTimeMillis());
+    } else {
+      inCallButtonUi.requestCallRecordingPermissions(CallRecorder.REQUIRED_PERMISSIONS);
+    }
+  }
+
+  private boolean hasAllPermissions(String[] permissions) {
+    for (String p : permissions) {
+      if (context.checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
   public void changeToVideoClicked() {
     LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
     Logger.get(context)
@@ -486,6 +565,10 @@
             && call.getState() != DialerCallState.DIALING
             && call.getState() != DialerCallState.CONNECTING;
 
+    final CallRecorder recorder = CallRecorder.getInstance();
+    final boolean showCallRecordOption = recorder.canRecordInCurrentCountry()
+        && !isVideo && call.getState() == DialerCallState.ACTIVE;
+
     otherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle());
     boolean showSwapSim =
         !call.isEmergencyCall()
@@ -519,6 +602,7 @@
     }
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
     inCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
+    inCallButtonUi.showButton(InCallButtonIds.BUTTON_RECORD_CALL, showCallRecordOption);
 
     inCallButtonUi.updateButtonStates();
   }
diff --git a/java/com/android/incallui/InCallDndHandler.java b/java/com/android/incallui/InCallDndHandler.java
new file mode 100644
index 0000000..4ab3e84
--- /dev/null
+++ b/java/com/android/incallui/InCallDndHandler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.incallui;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.state.DialerCallState;
+import com.android.incallui.InCallPresenter.InCallState;
+
+public class InCallDndHandler implements InCallPresenter.InCallStateListener {
+
+  private static final String KEY_ENABLE_DND = "incall_enable_dnd";
+
+  private SharedPreferences prefs;
+  private DialerCall activeCall;
+  private NotificationManager notificationManager;
+  private int userSelectedDndMode;
+
+  public InCallDndHandler(Context context) {
+    prefs = PreferenceManager.getDefaultSharedPreferences(context);
+    notificationManager = context.getSystemService(NotificationManager.class);
+
+    // Save the user's Do Not Disturb mode so that it can be restored when the call ends
+    userSelectedDndMode = notificationManager.getCurrentInterruptionFilter();
+  }
+
+  @Override
+  public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+    DialerCall activeCall = callList.getActiveCall();
+
+    if (activeCall != null && this.activeCall == null) {
+      Log.d(this, "Transition to active call " + activeCall);
+      handleDndState(activeCall);
+      this.activeCall = activeCall;
+    } else if (activeCall == null && this.activeCall != null) {
+      Log.d(this, "Transition from active call " + this.activeCall);
+      handleDndState(this.activeCall);
+      this.activeCall = null;
+    }
+  }
+
+  private void handleDndState(DialerCall call) {
+    if (!prefs.getBoolean(KEY_ENABLE_DND, false)) {
+      return;
+    }
+    if (DialerCallState.isConnectingOrConnected(call.getState())) {
+      Log.d(this, "Enabling Do Not Disturb mode");
+      setDoNotDisturbMode(NotificationManager.INTERRUPTION_FILTER_NONE);
+    } else {
+      Log.d(this, "Restoring previous Do Not Disturb mode");
+      setDoNotDisturbMode(userSelectedDndMode);
+    }
+  }
+
+  private void setDoNotDisturbMode(int newMode) {
+    if (notificationManager.isNotificationPolicyAccessGranted()) {
+      notificationManager.setInterruptionFilter(newMode);
+    } else {
+      Log.e(this, "Failed to set Do Not Disturb mode " + newMode + " due to lack of permissions");
+    }
+  }
+}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 17af756..8881029 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -127,6 +127,8 @@
 
   private StatusBarNotifier statusBarNotifier;
   private ExternalCallNotifier externalCallNotifier;
+  private InCallVibrationHandler vibrationHandler;
+  private InCallDndHandler dndHandler;
   private ContactInfoCache contactInfoCache;
   private Context context;
   private final OnCheckBlockedListener onCheckBlockedListener =
@@ -364,6 +366,12 @@
         .getEnrichedCallManager()
         .registerStateChangedListener(this.statusBarNotifier);
 
+    vibrationHandler = new InCallVibrationHandler(context);
+    addListener(vibrationHandler);
+
+    dndHandler = new InCallDndHandler(context);
+    addListener(dndHandler);
+
     this.proximitySensor = proximitySensor;
     addListener(this.proximitySensor);
 
@@ -1653,6 +1661,16 @@
       }
       statusBarNotifier = null;
 
+      if (vibrationHandler != null) {
+        removeListener(vibrationHandler);
+      }
+      vibrationHandler = null;
+
+      if (dndHandler != null) {
+        removeListener(dndHandler);
+      }
+      dndHandler = null;
+
       if (callList != null) {
         callList.removeListener(this);
         callList.removeListener(spamCallListListener);
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index b9d0ecc..b2d318f 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -27,6 +27,7 @@
 import com.android.dialer.feedback.FeedbackComponent;
 import com.android.incallui.audiomode.AudioModeProvider;
 import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
 import com.android.incallui.call.ExternalCallList;
 import com.android.incallui.call.TelecomAdapter;
 import com.android.incallui.speakeasy.SpeakEasyCallManager;
@@ -112,6 +113,7 @@
     InCallPresenter.getInstance().onServiceBind();
     InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
     TelecomAdapter.getInstance().setInCallService(this);
+    CallRecorder.getInstance().setUp(context);
     returnToCallController =
         new ReturnToCallController(this, ContactInfoCache.getInstance(context));
     feedbackListener = FeedbackComponent.get(context).getCallFeedbackListener();
diff --git a/java/com/android/incallui/InCallVibrationHandler.java b/java/com/android/incallui/InCallVibrationHandler.java
new file mode 100644
index 0000000..067d6bf
--- /dev/null
+++ b/java/com/android/incallui/InCallVibrationHandler.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.incallui;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+import android.telecom.DisconnectCause;
+
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.state.DialerCallState;
+import com.android.incallui.InCallPresenter.InCallState;
+
+public class InCallVibrationHandler extends Handler implements
+    InCallPresenter.InCallStateListener {
+
+  private static final int MSG_VIBRATE_45_SEC = 1;
+
+  private static final String KEY_VIBRATE_CALL_WAITING = "incall_vibrate_call_waiting";
+  private static final String KEY_VIBRATE_OUTGOING = "incall_vibrate_outgoing";
+  private static final String KEY_VIBRATE_45SECS = "incall_vibrate_45secs";
+  private static final String KEY_VIBRATE_HANGUP = "incall_vibrate_hangup";
+
+  private SharedPreferences prefs;
+  private Vibrator vibrator;
+  private DialerCall activeCall;
+
+  public InCallVibrationHandler(Context context) {
+    prefs = PreferenceManager.getDefaultSharedPreferences(context);
+    vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+  }
+
+  @Override
+  public void handleMessage(Message msg) {
+    switch (msg.what) {
+      case MSG_VIBRATE_45_SEC:
+        vibrate(70, 0, 0);
+        sendEmptyMessageDelayed(MSG_VIBRATE_45_SEC, 60000);
+        break;
+    }
+  }
+
+  @Override
+  public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+    DialerCall activeCall = callList.getActiveCall();
+
+    if (activeCall != null && this.activeCall == null) {
+      Log.d(this, "Transition to active call " + activeCall);
+      if (activeCall.isOutgoing()) {
+        handleOutgoingCallVibration(activeCall);
+      }
+      this.activeCall = activeCall;
+    } else if (activeCall != null && callList.getIncomingCall() != null
+        && !callList.getIncomingCall().equals(activeCall)) {
+      Log.d(this, "New incoming call" + callList.getIncomingCall());
+      handleCallWaitingVibration(activeCall);
+    } else if (activeCall == null && this.activeCall != null) {
+      Log.d(this, "Transition from active call " + this.activeCall);
+      handleCallEnd(this.activeCall);
+      this.activeCall = null;
+    }
+  }
+
+  private void handleOutgoingCallVibration(DialerCall call) {
+    long durationMillis = System.currentTimeMillis() - call.getConnectTimeMillis();
+    Log.d(this, "Start outgoing call: duration = " + durationMillis);
+
+    if (prefs.getBoolean(KEY_VIBRATE_OUTGOING, false) && durationMillis < 200) {
+      vibrate(100, 200, 0);
+    }
+    if (prefs.getBoolean(KEY_VIBRATE_45SECS, false)) {
+      start45SecondVibration(durationMillis);
+    }
+  }
+
+  private void handleCallWaitingVibration(DialerCall call) {
+    Log.d(this, "Start call waiting vibration");
+    if (prefs.getBoolean(KEY_VIBRATE_CALL_WAITING, false)) {
+      vibrate(200, 300, 500);
+    }
+  }
+
+  private void handleCallEnd(DialerCall call) {
+    long durationMillis = System.currentTimeMillis() - call.getConnectTimeMillis();
+    DisconnectCause cause = call.getDisconnectCause();
+    boolean localDisconnect =
+        // Disconnection not yet processed
+        call.getState() == DialerCallState.DISCONNECTING ||
+        // Disconnection already processed
+        (cause != null && cause.getCode() == DisconnectCause.LOCAL);
+
+    Log.d(this, "Ending active call: duration = " + durationMillis
+        + ", locally disconnected = " + localDisconnect);
+
+    if (prefs.getBoolean(KEY_VIBRATE_HANGUP, false)
+        && !localDisconnect && durationMillis > 500) {
+      vibrate(50, 100, 50);
+    }
+    // Stop 45-second vibration
+    removeMessages(MSG_VIBRATE_45_SEC);
+  }
+
+  private void start45SecondVibration(long callDurationMillis) {
+    callDurationMillis = callDurationMillis % 60000;
+    Log.d(this, "vibrate start @" + callDurationMillis);
+    removeMessages(MSG_VIBRATE_45_SEC);
+
+    long timer;
+    if (callDurationMillis > 45000) {
+        // Schedule the alarm at the next minute + 45 secs
+        timer = 45000 + 60000 - callDurationMillis;
+    } else {
+        // Schedule the alarm at the first 45 second mark
+        timer = 45000 - callDurationMillis;
+    }
+    sendEmptyMessageDelayed(MSG_VIBRATE_45_SEC, timer);
+  }
+
+  private void vibrate(int v1, int p1, int v2) {
+    long[] pattern = new long[] {
+      0, v1, p1, v2
+    };
+    vibrator.vibrate(pattern, -1);
+  }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
index a21073d..7abea54 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
@@ -367,21 +367,6 @@
   }
 
   private boolean isFalseTouch() {
-    if (falsingManager != null && falsingManager.isEnabled()) {
-      if (falsingManager.isFalseTouch()) {
-        if (touchUsesFalsing) {
-          LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
-          return true;
-        } else {
-          LogUtil.i(
-              "FlingUpDownTouchHandler.isFalseTouch",
-              "Suspected false touch, but not using false touch rejection for this gesture");
-          return false;
-        }
-      } else {
-        return false;
-      }
-    }
     return !touchAboveFalsingThreshold;
   }
 
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index 8aeb05f..dbb8df9 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -27,6 +27,8 @@
 import com.android.incallui.call.DialerCallListener;
 import com.android.incallui.call.state.DialerCallState;
 
+import com.android.incallui.R;
+
 /**
  * This class prevents users from accidentally answering calls by keeping the screen off until the
  * proximity sensor is unblocked. If the screen is already on or if this is a call waiting call then
@@ -61,6 +63,11 @@
       return false;
     }
 
+    if (!context.getResources().getBoolean(R.bool.config_answer_proximity_sensor_enabled)) {
+      LogUtil.i("AnswerProximitySensor.shouldUse", "disabled by overlay");
+      return false;
+    }
+
     if (!context
         .getSystemService(PowerManager.class)
         .isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
diff --git a/java/com/android/incallui/audiomode/AudioModeProvider.java b/java/com/android/incallui/audiomode/AudioModeProvider.java
index 47b021e..cefee0d 100644
--- a/java/com/android/incallui/audiomode/AudioModeProvider.java
+++ b/java/com/android/incallui/audiomode/AudioModeProvider.java
@@ -89,6 +89,7 @@
           hasBluetooth = true;
           continue;
         case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+        case AudioDeviceInfo.TYPE_USB_HEADSET:
           hasHeadset = true;
           continue;
         default:
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 634a302..8428c84 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -26,6 +26,7 @@
 import android.telecom.Call;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.common.Assert;
@@ -535,6 +536,15 @@
     return retval;
   }
 
+  public DialerCall getCallWithStateAndNumber(int state, String number) {
+    for (DialerCall call : callById.values()) {
+      if (TextUtils.equals(call.getNumber(), number) && call.getState() == state) {
+        return call;
+      }
+    }
+    return null;
+  }
+
   /**
    * Return if there is any active or background call which was not a parent call (never had a child
    * call)
diff --git a/java/com/android/incallui/call/CallRecorder.java b/java/com/android/incallui/call/CallRecorder.java
new file mode 100644
index 0000000..867d5a5
--- /dev/null
+++ b/java/com/android/incallui/call/CallRecorder.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.incallui.call;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.XmlResourceParser;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.dialer.R;
+import com.android.dialer.callrecord.CallRecordingDataStore;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+import com.android.dialer.location.GeoUtil;
+import com.android.incallui.call.state.DialerCallState;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+/**
+ * InCall UI's interface to the call recorder
+ *
+ * Manages the call recorder service lifecycle.  We bind to the service whenever an active call
+ * is established, and unbind when all calls have been disconnected.
+ */
+public class CallRecorder implements CallList.Listener {
+  public static final String TAG = "CallRecorder";
+
+  public static final String[] REQUIRED_PERMISSIONS = new String[] {
+    android.Manifest.permission.RECORD_AUDIO,
+    android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+  };
+  private static final HashMap<String, Boolean> RECORD_ALLOWED_STATE_BY_COUNTRY = new HashMap<>();
+
+  private static CallRecorder instance = null;
+  private Context context;
+  private boolean initialized = false;
+  private ICallRecorderService service = null;
+
+  private HashSet<RecordingProgressListener> progressListeners =
+      new HashSet<RecordingProgressListener>();
+  private Handler handler = new Handler();
+
+  private ServiceConnection connection = new ServiceConnection() {
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+      CallRecorder.this.service = ICallRecorderService.Stub.asInterface(service);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+      CallRecorder.this.service = null;
+    }
+  };
+
+  public static CallRecorder getInstance() {
+    if (instance == null) {
+      instance = new CallRecorder();
+    }
+    return instance;
+  }
+
+  public boolean isEnabled() {
+    return CallRecorderService.isEnabled(context);
+  }
+
+  public boolean canRecordInCurrentCountry() {
+      if (!isEnabled()) {
+          return false;
+      }
+      if (RECORD_ALLOWED_STATE_BY_COUNTRY.isEmpty()) {
+          loadAllowedStates();
+      }
+
+      String currentCountryIso = GeoUtil.getCurrentCountryIso(context);
+      Boolean allowedState = RECORD_ALLOWED_STATE_BY_COUNTRY.get(currentCountryIso);
+
+      return allowedState != null && allowedState;
+  }
+
+  private CallRecorder() {
+    CallList.getInstance().addListener(this);
+  }
+
+  public void setUp(Context context) {
+    this.context = context.getApplicationContext();
+  }
+
+  private void initialize() {
+    if (isEnabled() && !initialized) {
+      Intent serviceIntent = new Intent(context, CallRecorderService.class);
+      context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+      initialized = true;
+    }
+  }
+
+  private void uninitialize() {
+    if (initialized) {
+      context.unbindService(connection);
+      initialized = false;
+    }
+  }
+
+  public boolean startRecording(final String phoneNumber, final long creationTime) {
+    if (service == null) {
+      return false;
+    }
+
+    try {
+      if (service.startRecording(phoneNumber, creationTime)) {
+        for (RecordingProgressListener l : progressListeners) {
+          l.onStartRecording();
+        }
+        updateRecordingProgressTask.run();
+        return true;
+      } else {
+        Toast.makeText(context, R.string.call_recording_failed_message, Toast.LENGTH_SHORT)
+            .show();
+      }
+    } catch (RemoteException e) {
+      Log.w(TAG, "Failed to start recording " + phoneNumber + ", " + new Date(creationTime), e);
+    }
+
+    return false;
+  }
+
+  public boolean isRecording() {
+    if (service == null) {
+      return false;
+    }
+
+    try {
+      return service.isRecording();
+    } catch (RemoteException e) {
+      Log.w(TAG, "Exception checking recording status", e);
+    }
+    return false;
+  }
+
+  public CallRecording getActiveRecording() {
+    if (service == null) {
+      return null;
+    }
+
+    try {
+      return service.getActiveRecording();
+    } catch (RemoteException e) {
+      Log.w("Exception getting active recording", e);
+    }
+    return null;
+  }
+
+  public void finishRecording() {
+    if (service != null) {
+      try {
+        final CallRecording recording = service.stopRecording();
+        if (recording != null) {
+          if (!TextUtils.isEmpty(recording.phoneNumber)) {
+            new Thread(() -> {
+              CallRecordingDataStore dataStore = new CallRecordingDataStore();
+              dataStore.open(context);
+              dataStore.putRecording(recording);
+              dataStore.close();
+            }).start();
+          } else {
+            // Data store is an index by number so that we can link recordings in the
+            // call detail page.  If phone number is not available (conference call or
+            // unknown number) then just display a toast.
+            String msg = context.getResources().getString(
+                R.string.call_recording_file_location, recording.fileName);
+            Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+          }
+        }
+      } catch (RemoteException e) {
+        Log.w(TAG, "Failed to stop recording", e);
+      }
+    }
+
+    for (RecordingProgressListener l : progressListeners) {
+      l.onStopRecording();
+    }
+    handler.removeCallbacks(updateRecordingProgressTask);
+  }
+
+  //
+  // Call list listener methods.
+  //
+  @Override
+  public void onIncomingCall(DialerCall call) {
+    // do nothing
+  }
+
+  @Override
+  public void onCallListChange(final CallList callList) {
+    if (!initialized && callList.getActiveCall() != null) {
+      // we'll come here if this is the first active call
+      initialize();
+    } else {
+      // we can come down this branch to resume a call that was on hold
+      CallRecording active = getActiveRecording();
+      if (active != null) {
+        DialerCall call =
+            callList.getCallWithStateAndNumber(DialerCallState.ONHOLD, active.phoneNumber);
+        if (call != null) {
+          // The call associated with the active recording has been placed
+          // on hold, so stop the recording.
+          finishRecording();
+        }
+      }
+    }
+  }
+
+  @Override
+  public void onDisconnect(final DialerCall call) {
+    CallRecording active = getActiveRecording();
+    if (active != null && TextUtils.equals(call.getNumber(), active.phoneNumber)) {
+      // finish the current recording if the call gets disconnected
+      finishRecording();
+    }
+
+    // tear down the service if there are no more active calls
+    if (CallList.getInstance().getActiveCall() == null) {
+      uninitialize();
+    }
+  }
+
+  @Override
+  public void onUpgradeToVideo(DialerCall call) {}
+
+  @Override
+  public void onSessionModificationStateChange(DialerCall call) {}
+
+  @Override
+  public void onWiFiToLteHandover(DialerCall call) {}
+
+  @Override
+  public void onHandoverToWifiFailed(DialerCall call) {}
+
+  @Override
+  public void onInternationalCallOnWifi(DialerCall call) {}
+
+  // allow clients to listen for recording progress updates
+  public interface RecordingProgressListener {
+    void onStartRecording();
+    void onStopRecording();
+    void onRecordingTimeProgress(long elapsedTimeMs);
+  }
+
+  public void addRecordingProgressListener(RecordingProgressListener listener) {
+    progressListeners.add(listener);
+  }
+
+  public void removeRecordingProgressListener(RecordingProgressListener listener) {
+    progressListeners.remove(listener);
+  }
+
+  private static final int UPDATE_INTERVAL = 500;
+
+  private Runnable updateRecordingProgressTask = new Runnable() {
+    @Override
+    public void run() {
+      CallRecording active = getActiveRecording();
+      if (active != null) {
+        long elapsed = System.currentTimeMillis() - active.startRecordingTime;
+        for (RecordingProgressListener l : progressListeners) {
+          l.onRecordingTimeProgress(elapsed);
+        }
+      }
+      handler.postDelayed(this, UPDATE_INTERVAL);
+    }
+  };
+
+  private void loadAllowedStates() {
+    XmlResourceParser parser = context.getResources().getXml(R.xml.call_record_states);
+    try {
+        // Consume all START_DOCUMENT which can appear more than once.
+        while (parser.next() == XmlPullParser.START_DOCUMENT) {}
+
+        parser.require(XmlPullParser.START_TAG, null, "call-record-allowed-flags");
+
+        while (parser.next() != XmlPullParser.END_DOCUMENT) {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                continue;
+            }
+            parser.require(XmlPullParser.START_TAG, null, "country");
+
+            String iso = parser.getAttributeValue(null, "iso");
+            String allowed = parser.getAttributeValue(null, "allowed");
+            if (iso != null && ("true".equals(allowed) || "false".equals(allowed))) {
+                for (String splittedIso : iso.split(",")) {
+                    RECORD_ALLOWED_STATE_BY_COUNTRY.put(
+                            splittedIso.toUpperCase(Locale.US), Boolean.valueOf(allowed));
+                }
+            } else {
+                throw new XmlPullParserException("Unexpected country specification", parser, null);
+            }
+        }
+        Log.d(TAG, "Loaded " + RECORD_ALLOWED_STATE_BY_COUNTRY.size() + " country records");
+    } catch (XmlPullParserException | IOException e) {
+        Log.e(TAG, "Could not parse allowed country list", e);
+        RECORD_ALLOWED_STATE_BY_COUNTRY.clear();
+    } finally {
+        parser.close();
+    }
+  }
+}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index 76d3e8b..b028946 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -173,6 +173,7 @@
 
   @Nullable private SpamStatus spamStatus;
   private boolean isBlocked;
+  private boolean isOutgoing;
 
   private boolean didShowCameraPermission;
   private boolean didDismissVideoChargesAlertDialog;
@@ -905,6 +906,8 @@
           logState.dialerConnectTimeMillisElapsedRealtime == 0
               ? 0
               : SystemClock.elapsedRealtime() - logState.dialerConnectTimeMillisElapsedRealtime;
+    } else if (state == DialerCallState.DIALING || state == DialerCallState.CONNECTING) {
+      isOutgoing = true;
     }
   }
 
@@ -913,6 +916,10 @@
     this.clock = clock;
   }
 
+  public boolean isOutgoing() {
+    return isOutgoing;
+  }
+
   public int getNumberPresentation() {
     return telecomCall == null ? -1 : telecomCall.getDetails().getHandlePresentation();
   }
diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java
index 4086e14..5177783 100644
--- a/java/com/android/incallui/callpending/CallPendingActivity.java
+++ b/java/com/android/incallui/callpending/CallPendingActivity.java
@@ -285,6 +285,9 @@
           public void swapSimClicked() {}
 
           @Override
+          public void callRecordClicked(boolean checked) {}
+
+          @Override
           public Context getContext() {
             return CallPendingActivity.this;
           }
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
index 757d813..733dcf9 100644
--- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -117,9 +117,10 @@
     mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
     mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
     mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
-    mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(5).build());
-    mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
-    mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build());
+    mapping.put(InCallButtonIds.BUTTON_RECORD_CALL, MappingInfo.builder(3).build());
+    mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(4).setSlotOrder(5).build());
+    mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(4).build());
+    mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(5).build());
     return mapping;
   }
 }
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
index 328ebbe..2ad3d3e 100644
--- a/java/com/android/incallui/incall/impl/ButtonController.java
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -16,6 +16,7 @@
 
 package com.android.incallui.incall.impl;
 
+import android.content.res.Resources;
 import android.graphics.drawable.AnimationDrawable;
 import android.support.annotation.CallSuper;
 import android.support.annotation.DrawableRes;
@@ -23,6 +24,7 @@
 import android.support.annotation.StringRes;
 import android.telecom.CallAudioState;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.view.View;
 import android.view.View.OnClickListener;
 import com.android.dialer.common.Assert;
@@ -411,6 +413,95 @@
     }
   }
 
+  class CallRecordButtonController implements ButtonController, OnClickListener {
+    @NonNull private final InCallButtonUiDelegate delegate;
+    private boolean isEnabled;
+    private boolean isAllowed;
+    private boolean isChecked;
+    private long recordingSeconds;
+    private CheckableLabeledButton button;
+
+    public CallRecordButtonController(@NonNull InCallButtonUiDelegate delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public boolean isEnabled() {
+      return isEnabled;
+    }
+
+    @Override
+    public void setEnabled(boolean isEnabled) {
+      this.isEnabled = isEnabled;
+      if (button != null) {
+        button.setEnabled(isEnabled);
+      }
+    }
+
+    @Override
+    public boolean isAllowed() {
+      return isAllowed;
+    }
+
+    @Override
+    public void setAllowed(boolean isAllowed) {
+      this.isAllowed = isAllowed;
+      if (button != null) {
+        button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+      }
+    }
+
+    @Override
+    public void setChecked(boolean isChecked) {
+      this.isChecked = isChecked;
+      if (button != null) {
+        button.setChecked(isChecked);
+      }
+    }
+
+    @Override
+    public int getInCallButtonId() {
+      return InCallButtonIds.BUTTON_RECORD_CALL;
+    }
+
+    @Override
+    public void setButton(CheckableLabeledButton button) {
+      this.button = button;
+      if (button != null) {
+        final Resources res = button.getContext().getResources();
+        if (isChecked) {
+          CharSequence duration = DateUtils.formatElapsedTime(recordingSeconds);
+          button.setLabelText(res.getString(R.string.onscreenCallRecordingText, duration));
+        } else {
+          button.setLabelText(R.string.onscreenCallRecordText);
+        }
+        button.setEnabled(isEnabled);
+        button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+        button.setChecked(isChecked);
+        button.setOnClickListener(this);
+        button.setIconDrawable(R.drawable.quantum_ic_record_white_36);
+        button.setContentDescription(res.getText(
+            isChecked ? R.string.onscreenStopCallRecordText : R.string.onscreenCallRecordText));
+        button.setShouldShowMoreIndicator(false);
+      }
+    }
+
+    public void setRecordingState(boolean recording) {
+      isChecked = recording;
+      setButton(button);
+    }
+
+    public void setRecordingDuration(long durationMs) {
+      recordingSeconds = (durationMs + 500) / 1000;
+      setButton(button);
+    }
+
+    @Override
+    public void onClick(View v) {
+      delegate.callRecordClicked(!isChecked);
+    }
+  }
+
   class DialpadButtonController extends SimpleCheckableButtonController {
 
     public DialpadButtonController(@NonNull InCallButtonUiDelegate delegate) {
diff --git a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
index bfc2781..ec932b9 100644
--- a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
+++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
@@ -156,6 +156,10 @@
     labelView.setText(stringRes);
   }
 
+  public void setLabelText(CharSequence label) {
+    labelView.setText(label);
+  }
+
   /** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
   public void setShouldShowMoreIndicator(boolean shouldShow) {
     iconView.setBackground(shouldShow ? backgroundMore : background);
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index 3062069..336550d 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -54,6 +54,7 @@
 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
 import com.android.incallui.contactgrid.ContactGridManager;
 import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.impl.ButtonController.CallRecordButtonController;
 import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
 import com.android.incallui.incall.impl.ButtonController.UpgradeToRttButtonController;
 import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
@@ -95,6 +96,8 @@
   private int phoneType;
   private boolean stateRestored;
 
+  private static final int REQUEST_CODE_CALL_RECORD_PERMISSION = 1000;
+
   // Add animation to educate users. If a call has enriched calling attachments then we'll
   // initially show the attachment page. After a delay seconds we'll animate to the button grid.
   private final Handler handler = new Handler();
@@ -117,7 +120,8 @@
         || id == InCallButtonIds.BUTTON_MERGE
         || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE
         || id == InCallButtonIds.BUTTON_SWAP_SIM
-        || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT;
+        || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT
+        || id == InCallButtonIds.BUTTON_RECORD_CALL;
   }
 
   @Override
@@ -232,6 +236,7 @@
         new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
     buttonControllers.add(
         new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate));
+    buttonControllers.add(new ButtonController.CallRecordButtonController(inCallButtonUiDelegate));
 
     inCallScreenDelegate.onInCallScreenDelegateInit(this);
     inCallScreenDelegate.onInCallScreenReady();
@@ -467,6 +472,39 @@
   }
 
   @Override
+  public void setCallRecordingState(boolean isRecording) {
+    ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+        .setRecordingState(isRecording);
+  }
+
+  @Override
+  public void setCallRecordingDuration(long durationMs) {
+    ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+        .setRecordingDuration(durationMs);
+  }
+
+  @Override
+  public void requestCallRecordingPermissions(String[] permissions) {
+    requestPermissions(permissions, REQUEST_CODE_CALL_RECORD_PERMISSION);
+  }
+
+  @Override
+  public void onRequestPermissionsResult(int requestCode,
+      @NonNull String[] permissions, @NonNull int[] grantResults) {
+    if (requestCode == REQUEST_CODE_CALL_RECORD_PERMISSION) {
+      boolean allGranted = grantResults.length > 0;
+      for (int i = 0; i < grantResults.length; i++) {
+        allGranted &= grantResults[i] == PackageManager.PERMISSION_GRANTED;
+      }
+      if (allGranted) {
+        inCallButtonUiDelegate.callRecordClicked(true);
+      }
+    } else {
+      super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+    }
+  }
+
+  @Override
   public void updateButtonStates() {
     // When the incall screen is ready, this method is called from #setSecondary, even though the
     // incall button ui is not ready yet. This method is called again once the incall button ui is
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
index 80ea75e..2c956c8 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -38,6 +38,7 @@
   InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
   InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
   InCallButtonIds.BUTTON_SWAP_SIM,
+  InCallButtonIds.BUTTON_RECORD_CALL,
   InCallButtonIds.BUTTON_COUNT,
   InCallButtonIds.BUTTON_UPGRADE_TO_RTT
 })
@@ -58,6 +59,7 @@
   int BUTTON_MANAGE_VOICE_CONFERENCE = 12;
   int BUTTON_SWITCH_TO_SECONDARY = 13;
   int BUTTON_SWAP_SIM = 14;
-  int BUTTON_COUNT = 15;
+  int BUTTON_RECORD_CALL = 15;
   int BUTTON_UPGRADE_TO_RTT = 16;
+  int BUTTON_COUNT = 17;
 }
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
index 5239d9d..ee03c3d 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -58,6 +58,8 @@
       return "SWAP_SIM";
     } else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT) {
       return "UPGRADE_TO_RTT";
+    } else if (id == InCallButtonIds.BUTTON_RECORD_CALL) {
+      return "RECORD_CALL";
     } else {
       return "INVALID_BUTTON: " + id;
     }
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUi.java b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
index 28dd84c..f22efeb 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUi.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
@@ -37,6 +37,12 @@
 
   void setAudioState(CallAudioState audioState);
 
+  void setCallRecordingState(boolean isRecording);
+
+  void setCallRecordingDuration(long durationMs);
+
+  void requestCallRecordingPermissions(String[] permissions);
+
   /**
    * Once showButton() has been called on each of the individual buttons in the UI, call this to
    * configure the overflow menu appropriately.
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
index 4cf40ef..4e25ff0 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -65,5 +65,7 @@
 
   void swapSimClicked();
 
+  void callRecordClicked(boolean checked);
+
   Context getContext();
 }
diff --git a/java/com/android/incallui/res/values/cm_strings.xml b/java/com/android/incallui/res/values/cm_strings.xml
new file mode 100644
index 0000000..5e65ffc
--- /dev/null
+++ b/java/com/android/incallui/res/values/cm_strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="call_recording_failed_message">Failed to start call recording</string>
+    <string name="call_recording_file_location">Saved call recording to <xliff:g id="filename">%s</xliff:g></string>
+    <string name="onscreenCallRecordText">Record call</string>
+    <string name="onscreenCallRecordingText">Recording call - <xliff:g id="duration" example="00:10">%1$s</xliff:g></string>
+    <string name="onscreenStopCallRecordText">Stop recording</string>
+    <string name="recording_time_text">Recording</string>
+    <string name="recording_warning_title">Enable call recording?</string>
+    <string name="recording_warning_text">Notice: You are responsible for compliance with any laws, regulations and rules that apply to the use of call recording functionality and the use or distribution of those recordings.</string>
+</resources>
diff --git a/java/com/android/incallui/res/values/lineage_config.xml b/java/com/android/incallui/res/values/lineage_config.xml
new file mode 100644
index 0000000..e9a958d
--- /dev/null
+++ b/java/com/android/incallui/res/values/lineage_config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2018 The LineageOS 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.
+-->
+<resources>
+  <!-- Whether to check that the proximity is unblocked before showing the incoming call UI. -->
+  <bool name="config_answer_proximity_sensor_enabled">true</bool>
+</resources>
diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java
index 649e808..3e76f1f 100644
--- a/java/com/android/incallui/rtt/impl/RttChatFragment.java
+++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java
@@ -595,4 +595,13 @@
 
   @Override
   public void onAudioRouteSelectorDismiss() {}
+
+  @Override
+  public void requestCallRecordingPermissions(String[] permissions) {}
+
+  @Override
+  public void setCallRecordingDuration(long duration) {}
+
+  @Override
+  public void setCallRecordingState(boolean isRecording) {}
 }
diff --git a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
index 89db079..07965b9 100644
--- a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
@@ -797,6 +797,18 @@
   }
 
   @Override
+  public void setCallRecordingState(boolean isRecording) {
+  }
+
+  @Override
+  public void setCallRecordingDuration(long durationMs) {
+  }
+
+  @Override
+  public void requestCallRecordingPermissions(String[] permissions) {
+  }
+
+  @Override
   public void updateButtonStates() {
     LogUtil.i("SurfaceViewVideoCallFragment.updateButtonState", null);
     speakerButtonController.updateButtonState();
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index edfc17c..3fbce5c 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -909,6 +909,18 @@
   }
 
   @Override
+  public void setCallRecordingState(boolean isRecording) {
+  }
+
+  @Override
+  public void setCallRecordingDuration(long durationMs) {
+  }
+
+  @Override
+  public void requestCallRecordingPermissions(String[] permissions) {
+  }
+
+  @Override
   public void updateButtonStates() {
     LogUtil.i("VideoCallFragment.updateButtonState", null);
     speakerButtonController.updateButtonState();
diff --git a/privapp_whitelist_com.android.dialer-ext.xml b/privapp_whitelist_com.android.dialer-ext.xml
new file mode 100644
index 0000000..09e6e0f
--- /dev/null
+++ b/privapp_whitelist_com.android.dialer-ext.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019-2020 The LineageOS 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.
+-->
+<permissions>
+    <!-- Additional permissions on top of privapp_whitelist_com.android.dialer.xml -->
+    <privapp-permissions package="com.android.dialer">
+        <permission name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
+    </privapp-permissions>
+</permissions>
diff --git a/proguard.flags b/proguard.flags
index 160020b..cb80337 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,4 +1,8 @@
 # These are referenced by com.google.common.util.concurrent and
 # com.google.errorprone.annotations but don't exist on Android.
 -dontwarn java.lang.ClassValue
--dontwarn javax.lang.model.element.Modifier
\ No newline at end of file
+-dontwarn javax.lang.model.element.Modifier
+
+-keep public class * extends android.app.Fragment {
+  public <init>();
+}