Merge changes I6a5a8ad8,I096ea022,Id164d08c

* changes:
  Implement preferred SIM
  Allow external apps to start pre-call flow
  Play voicemails downloaded from the voicemail server in the NUI VM Fragment
diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
index 6c6aebc..e21fded 100644
--- a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
+++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -123,6 +123,11 @@
     return mListener;
   }
 
+  @VisibleForTesting
+  public boolean canSetDefault() {
+    return getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+  }
+
   @Override
   public void onSaveInstanceState(Bundle outState) {
     super.onSaveInstanceState(outState);
diff --git a/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml b/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml
new file mode 100644
index 0000000..b1c625a
--- /dev/null
+++ b/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<!--
+ ~ 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.precall.externalreceiver">
+
+  <application>
+    <activity
+        android:excludeFromRecents="true"
+        android:exported="true"
+        android:name="com.android.dialer.precall.externalreceiver.LaunchPreCallActivity"
+        android:noHistory="true"
+        android:permission="android.permission.CALL_PHONE"
+        android:theme="@style/Theme.PreCall.DialogHolder">
+      <intent-filter>
+        <action android:name="com.android.dialer.LAUNCH_PRE_CALL"/>
+        <category android:name="android.intent.category.DEFAULT" />
+        <data android:scheme="tel" />
+      </intent-filter>
+      <intent-filter>
+        <action android:name="com.android.dialer.LAUNCH_PRE_CALL"/>
+        <category android:name="android.intent.category.DEFAULT" />
+        <data android:scheme="voicemail" />
+      </intent-filter>
+    </activity>
+  </application>
+</manifest>
diff --git a/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java b/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java
new file mode 100644
index 0000000..121e6a6
--- /dev/null
+++ b/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java
@@ -0,0 +1,60 @@
+/*
+ * 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
+ */
+
+package com.android.dialer.precall.externalreceiver;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import com.android.dialer.callintent.CallInitiationType.Type;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.precall.PreCall;
+
+/**
+ * Activity that forwards to {@link PreCall#start(Context, CallIntentBuilder)} so the pre-call flow
+ * can be initiated by external apps. This activity is exported but can only be started by apps with
+ * {@link android.Manifest.permission#CALL_PHONE}. Keyguard will be triggered if phone is locked.
+ *
+ * @see CallIntentBuilder
+ */
+public class LaunchPreCallActivity extends Activity {
+
+  public static final String ACTION_LAUNCH_PRE_CALL = "com.android.dialer.LAUNCH_PRE_CALL";
+
+  public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "phone_account_handle";
+
+  public static final String EXTRA_IS_VIDEO_CALL = "is_video_call";
+
+  public static final String EXTRA_CALL_SUBJECT = "call_subject";
+
+  public static final String EXTRA_ALLOW_ASSISTED_DIAL = "allow_assisted_dial";
+
+  @Override
+  public void onCreate(@Nullable Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    Intent intent = getIntent();
+    CallIntentBuilder builder = new CallIntentBuilder(intent.getData(), Type.EXTERNAL_INITIATION);
+    builder
+        .setPhoneAccountHandle(intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE))
+        .setIsVideoCall(intent.getBooleanExtra(EXTRA_IS_VIDEO_CALL, false))
+        .setCallSubject(intent.getStringExtra(EXTRA_CALL_SUBJECT))
+        .setAllowAssistedDial(intent.getBooleanExtra(EXTRA_ALLOW_ASSISTED_DIAL, false));
+    PreCall.start(this, builder);
+    finish();
+  }
+}
diff --git a/java/com/android/dialer/precall/impl/CallingAccountSelector.java b/java/com/android/dialer/precall/impl/CallingAccountSelector.java
index ca8798c..ca74bef 100644
--- a/java/com/android/dialer/precall/impl/CallingAccountSelector.java
+++ b/java/com/android/dialer/precall/impl/CallingAccountSelector.java
@@ -17,10 +17,18 @@
 package com.android.dialer.precall.impl;
 
 import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.PhoneLookup;
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.support.v4.util.ArraySet;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
@@ -28,9 +36,17 @@
 import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
 import com.android.dialer.callintent.CallIntentBuilder;
 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.DialerExecutorComponent;
 import com.android.dialer.precall.PreCallAction;
 import com.android.dialer.precall.PreCallCoordinator;
+import com.android.dialer.precall.PreCallCoordinator.PendingAction;
+import com.android.dialer.preferredsim.PreferredSimFallbackContract;
+import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim;
+import com.google.common.base.Optional;
 import java.util.List;
+import java.util.Set;
 
 /** PreCallAction to select which phone account to call with. Ignored if there's only one account */
 @SuppressWarnings("MissingPermission")
@@ -43,6 +59,7 @@
   private boolean isDiscarding;
 
   @Override
+  @MainThread
   public void run(PreCallCoordinator coordinator) {
     CallIntentBuilder builder = coordinator.getBuilder();
     if (builder.getPhoneAccountHandle() != null) {
@@ -54,43 +71,189 @@
     if (accounts.size() <= 1) {
       return;
     }
-    boolean isVoicemail = builder.getUri().getScheme().equals(PhoneAccount.SCHEME_VOICEMAIL);
-
-    if (!isVoicemail) {
-      PhoneAccountHandle defaultPhoneAccount =
-          telecomManager.getDefaultOutgoingPhoneAccount(builder.getUri().getScheme());
-      if (defaultPhoneAccount != null) {
-        builder.setPhoneAccountHandle(defaultPhoneAccount);
-        return;
-      }
+    switch (builder.getUri().getScheme()) {
+      case PhoneAccount.SCHEME_VOICEMAIL:
+        showDialog(coordinator, coordinator.startPendingAction(), null);
+        break;
+      case PhoneAccount.SCHEME_TEL:
+        processPreferredAccount(coordinator);
+        break;
+      default:
+        // might be PhoneAccount.SCHEME_SIP
+        LogUtil.e(
+            "CallingAccountSelector.run",
+            "unable to process scheme " + builder.getUri().getScheme());
+        break;
     }
+  }
 
+  /** Initiates a background worker to find if there's any preferred account. */
+  @MainThread
+  private void processPreferredAccount(PreCallCoordinator coordinator) {
+    Assert.isMainThread();
+    CallIntentBuilder builder = coordinator.getBuilder();
+    Activity activity = coordinator.getActivity();
+    String phoneNumber = builder.getUri().getSchemeSpecificPart();
+    PendingAction pendingAction = coordinator.startPendingAction();
+    DialerExecutorComponent.get(coordinator.getActivity())
+        .dialerExecutorFactory()
+        .createUiTaskBuilder(
+            activity.getFragmentManager(),
+            "PreferredAccountWorker",
+            new PreferredAccountWorker(phoneNumber))
+        .onSuccess(
+            (result -> {
+              if (result.phoneAccountHandle.isPresent()) {
+                coordinator.getBuilder().setPhoneAccountHandle(result.phoneAccountHandle.get());
+                pendingAction.finish();
+                return;
+              }
+              PhoneAccountHandle defaultPhoneAccount =
+                  activity
+                      .getSystemService(TelecomManager.class)
+                      .getDefaultOutgoingPhoneAccount(builder.getUri().getScheme());
+              if (defaultPhoneAccount != null) {
+                builder.setPhoneAccountHandle(defaultPhoneAccount);
+                pendingAction.finish();
+                return;
+              }
+              showDialog(coordinator, pendingAction, result.dataId.orNull());
+            }))
+        .build()
+        .executeParallel(activity);
+  }
+
+  @MainThread
+  private void showDialog(
+      PreCallCoordinator coordinator, PendingAction pendingAction, @Nullable String dataId) {
+    Assert.isMainThread();
     selectPhoneAccountDialogFragment =
         SelectPhoneAccountDialogFragment.newInstance(
             R.string.pre_call_select_phone_account,
-            false /* canSetDefault */, // TODO(twyen): per contact defaults
-            accounts,
-            new SelectedListener(coordinator, coordinator.startPendingAction()),
+            dataId != null /* canSetDefault */,
+            coordinator
+                .getActivity()
+                .getSystemService(TelecomManager.class)
+                .getCallCapablePhoneAccounts(),
+            new SelectedListener(coordinator, pendingAction, dataId),
             null /* call ID */);
     selectPhoneAccountDialogFragment.show(
-        activity.getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR);
+        coordinator.getActivity().getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR);
   }
 
+  @MainThread
   @Override
   public void onDiscard() {
     isDiscarding = true;
     selectPhoneAccountDialogFragment.dismiss();
   }
 
+  private static class PreferredAccountWorkerResult {
+
+    /** The preferred phone account for the number. Absent if not set or invalid. */
+    Optional<PhoneAccountHandle> phoneAccountHandle = Optional.absent();
+
+    /**
+     * {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the
+     * preferred account is to be set it should be stored in this row
+     */
+    Optional<String> dataId = Optional.absent();
+  }
+
+  private static class PreferredAccountWorker
+      implements Worker<Context, PreferredAccountWorkerResult> {
+
+    private final String phoneNumber;
+
+    public PreferredAccountWorker(String phoneNumber) {
+      this.phoneNumber = phoneNumber;
+    }
+
+    @NonNull
+    @Override
+    @WorkerThread
+    public PreferredAccountWorkerResult doInBackground(Context context) throws Throwable {
+      PreferredAccountWorkerResult result = new PreferredAccountWorkerResult();
+      result.dataId = getDataId(context.getContentResolver(), phoneNumber);
+      if (result.dataId.isPresent()) {
+        result.phoneAccountHandle = getPreferredAccount(context, result.dataId.get());
+      }
+      return result;
+    }
+  }
+
+  @WorkerThread
+  @NonNull
+  private static Optional<String> getDataId(
+      @NonNull ContentResolver contentResolver, @Nullable String phoneNumber) {
+    Assert.isWorkerThread();
+    try (Cursor cursor =
+        contentResolver.query(
+            Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)),
+            new String[] {PhoneLookup.DATA_ID},
+            null,
+            null,
+            null)) {
+      if (cursor == null) {
+        return Optional.absent();
+      }
+      Set<String> result = new ArraySet<>();
+      while (cursor.moveToNext()) {
+        result.add(cursor.getString(0));
+      }
+      // TODO(twyen): if there are multiples attempt to grab from the contact that initiated the
+      // call.
+      if (result.size() == 1) {
+        return Optional.of(result.iterator().next());
+      } else {
+        LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring");
+        return Optional.absent();
+      }
+    }
+  }
+
+  @WorkerThread
+  @NonNull
+  private static Optional<PhoneAccountHandle> getPreferredAccount(
+      @NonNull Context context, @NonNull String dataId) {
+    Assert.isWorkerThread();
+    Assert.isNotNull(dataId);
+    try (Cursor cursor =
+        context
+            .getContentResolver()
+            .query(
+                PreferredSimFallbackContract.CONTENT_URI,
+                new String[] {
+                  PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME,
+                  PreferredSim.PREFERRED_PHONE_ACCOUNT_ID
+                },
+                PreferredSim.DATA_ID + " = ?",
+                new String[] {dataId},
+                null)) {
+      if (cursor == null) {
+        return Optional.absent();
+      }
+      if (!cursor.moveToFirst()) {
+        return Optional.absent();
+      }
+      return PreferredAccountUtil.getValidPhoneAccount(
+          context, cursor.getString(0), cursor.getString(1));
+    }
+  }
+
   private class SelectedListener extends SelectPhoneAccountListener {
 
     private final PreCallCoordinator coordinator;
     private final PreCallCoordinator.PendingAction listener;
+    private final String dataId;
 
     public SelectedListener(
-        @NonNull PreCallCoordinator builder, @NonNull PreCallCoordinator.PendingAction listener) {
+        @NonNull PreCallCoordinator builder,
+        @NonNull PreCallCoordinator.PendingAction listener,
+        @Nullable String dataId) {
       this.coordinator = Assert.isNotNull(builder);
       this.listener = Assert.isNotNull(listener);
+      this.dataId = dataId;
     }
 
     @MainThread
@@ -98,6 +261,17 @@
     public void onPhoneAccountSelected(
         PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {
       coordinator.getBuilder().setPhoneAccountHandle(selectedAccountHandle);
+
+      if (dataId != null && setDefault) {
+        DialerExecutorComponent.get(coordinator.getActivity())
+            .dialerExecutorFactory()
+            .createNonUiTaskBuilder(new WritePreferredAccountWorker())
+            .build()
+            .executeParallel(
+                new WritePreferredAccountWorkerInput(
+                    coordinator.getActivity(), dataId, selectedAccountHandle));
+      }
+
       listener.finish();
     }
 
@@ -111,4 +285,43 @@
       listener.finish();
     }
   }
+
+  private static class WritePreferredAccountWorkerInput {
+    private final Context context;
+    private final String dataId;
+    private final PhoneAccountHandle phoneAccountHandle;
+
+    WritePreferredAccountWorkerInput(
+        @NonNull Context context,
+        @NonNull String dataId,
+        @NonNull PhoneAccountHandle phoneAccountHandle) {
+      this.context = Assert.isNotNull(context);
+      this.dataId = Assert.isNotNull(dataId);
+      this.phoneAccountHandle = Assert.isNotNull(phoneAccountHandle);
+    }
+  }
+
+  private static class WritePreferredAccountWorker
+      implements Worker<WritePreferredAccountWorkerInput, Void> {
+
+    @Nullable
+    @Override
+    @WorkerThread
+    public Void doInBackground(WritePreferredAccountWorkerInput input) throws Throwable {
+      ContentValues values = new ContentValues();
+      values.put(
+          PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME,
+          input.phoneAccountHandle.getComponentName().flattenToString());
+      values.put(PreferredSim.PREFERRED_PHONE_ACCOUNT_ID, input.phoneAccountHandle.getId());
+      input
+          .context
+          .getContentResolver()
+          .update(
+              PreferredSimFallbackContract.CONTENT_URI,
+              values,
+              PreferredSim.DATA_ID + " = ?",
+              new String[] {String.valueOf(input.dataId)});
+      return null;
+    }
+  }
 }
diff --git a/java/com/android/dialer/precall/impl/PreferredAccountUtil.java b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java
new file mode 100644
index 0000000..a41cb6e
--- /dev/null
+++ b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java
@@ -0,0 +1,94 @@
+/*
+ * 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
+ */
+
+package com.android.dialer.precall.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.dialer.common.LogUtil;
+import com.google.common.base.Optional;
+
+/**
+ * Utilities for looking up and validating preferred {@link PhoneAccountHandle}. Contacts should
+ * follow the same logic.
+ */
+public class PreferredAccountUtil {
+
+  /**
+   * Validates {@code componentNameString} and {@code idString} maps to SIM that is present on the
+   * device.
+   */
+  @NonNull
+  public static Optional<PhoneAccountHandle> getValidPhoneAccount(
+      @NonNull Context context, @Nullable String componentNameString, @Nullable String idString) {
+    if (TextUtils.isEmpty(componentNameString) || TextUtils.isEmpty(idString)) {
+      LogUtil.i("PreferredAccountUtil.getValidPhoneAccount", "empty componentName or id");
+      return Optional.absent();
+    }
+    ComponentName componentName = ComponentName.unflattenFromString(componentNameString);
+    if (componentName == null) {
+      LogUtil.e("PreferredAccountUtil.getValidPhoneAccount", "cannot parse component name");
+      return Optional.absent();
+    }
+    PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, idString);
+
+    if (isPhoneAccountValid(context, phoneAccountHandle)) {
+      return Optional.of(phoneAccountHandle);
+    }
+    return Optional.absent();
+  }
+
+  private static boolean isPhoneAccountValid(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    if (VERSION.SDK_INT >= VERSION_CODES.O) {
+      return context
+              .getSystemService(TelephonyManager.class)
+              .createForPhoneAccountHandle(phoneAccountHandle)
+          != null;
+    }
+
+    PhoneAccount phoneAccount =
+        context.getSystemService(TelecomManager.class).getPhoneAccount(phoneAccountHandle);
+    if (phoneAccount == null) {
+      LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "invalid phone account");
+      return false;
+    }
+
+    if (!phoneAccount.isEnabled()) {
+      LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "disabled phone account");
+      return false;
+    }
+    for (SubscriptionInfo info :
+        SubscriptionManager.from(context).getActiveSubscriptionInfoList()) {
+      if (phoneAccountHandle.getId().startsWith(info.getIccId())) {
+        LogUtil.i("PreferredAccountUtil.isPhoneAccountValid", "sim found");
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
index f4b1916..e208210 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java
@@ -29,6 +29,7 @@
 import com.android.dialer.databasepopulator.VoicemailPopulator;
 import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity;
 import com.android.dialer.persistentlog.PersistentLogger;
+import com.android.dialer.preferredsim.PreferredSimFallbackContract;
 
 /** Implements the top level simulator menu. */
 final class SimulatorMainMenu {
@@ -40,6 +41,7 @@
         .addItem("Notifications", SimulatorNotifications.getActionProvider(context))
         .addItem("Populate database", () -> populateDatabase(context))
         .addItem("Clean database", () -> cleanDatabase(context))
+        .addItem("clear preferred SIM", () -> clearPreferredSim(context))
         .addItem("Sync voicemail", () -> syncVoicemail(context))
         .addItem("Share persistent log", () -> sharePersistentLog(context))
         .addItem(
@@ -63,6 +65,14 @@
         .executeSerial(context);
   }
 
+  private static void clearPreferredSim(Context context) {
+    DialerExecutorComponent.get(context)
+        .dialerExecutorFactory()
+        .createNonUiTaskBuilder(new ClearPreferredSimWorker())
+        .build()
+        .executeSerial(context);
+  }
+
   private static void syncVoicemail(@NonNull Context context) {
     Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
     context.sendBroadcast(intent);
@@ -109,6 +119,15 @@
     }
   }
 
+  private static class ClearPreferredSimWorker implements Worker<Context, Void> {
+    @Nullable
+    @Override
+    public Void doInBackground(Context context) {
+      context.getContentResolver().delete(PreferredSimFallbackContract.CONTENT_URI, null, null);
+      return null;
+    }
+  }
+
   private static class ShareLogWorker implements Worker<Void, String> {
     @Nullable
     @Override
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
index d04143f..6f6a87c 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
@@ -15,6 +15,7 @@
  */
 package com.android.dialer.voicemail.listui;
 
+import android.app.FragmentManager;
 import android.database.Cursor;
 import android.support.v7.widget.RecyclerView;
 import android.util.ArraySet;
@@ -33,6 +34,7 @@
 
   private final Cursor cursor;
   private final Clock clock;
+  private final FragmentManager fragmentManager;
   /** A valid id for {@link VoicemailEntry} is greater than 0 */
   private int currentlyExpandedViewHolderId = -1;
 
@@ -40,13 +42,16 @@
   private final Set<NewVoicemailViewHolder> newVoicemailViewHolderSet = new ArraySet<>();
 
   /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */
-  NewVoicemailAdapter(Cursor cursor, Clock clock) {
+  NewVoicemailAdapter(Cursor cursor, Clock clock, FragmentManager fragmentManager) {
+    LogUtil.enterBlock("NewVoicemailAdapter");
     this.cursor = cursor;
     this.clock = clock;
+    this.fragmentManager = fragmentManager;
   }
 
   @Override
   public NewVoicemailViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+    LogUtil.enterBlock("NewVoicemailAdapter.onCreateViewHolder");
     LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
     View view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false);
     NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view, clock, this);
@@ -56,9 +61,8 @@
 
   @Override
   public void onBindViewHolder(NewVoicemailViewHolder viewHolder, int position) {
-    LogUtil.i("onBindViewHolder", "position" + position);
     cursor.moveToPosition(position);
-    viewHolder.bind(cursor);
+    viewHolder.bind(cursor, fragmentManager);
     expandOrCollapseViewHolder(viewHolder);
   }
 
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
index 9c1fd8b..9a89dbe 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java
@@ -54,7 +54,9 @@
   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
     LogUtil.i("NewVoicemailFragment.onCreateLoader", "cursor size is %d", data.getCount());
     recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
-    recyclerView.setAdapter(new NewVoicemailAdapter(data, System::currentTimeMillis));
+    recyclerView.setAdapter(
+        new NewVoicemailAdapter(
+            data, System::currentTimeMillis, getActivity().getFragmentManager()));
   }
 
   @Override
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java
deleted file mode 100644
index 11aa9ac..0000000
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.dialer.voicemail.listui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import com.android.dialer.common.LogUtil;
-
-/**
- * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded.
- */
-public class NewVoicemailMediaPlayer extends LinearLayout {
-
-  private Button playButton;
-  private Button speakerButton;
-  private Button deleteButton;
-
-  public NewVoicemailMediaPlayer(Context context, AttributeSet attrs) {
-    super(context, attrs);
-    LogUtil.enterBlock("NewVoicemailMediaPlayer");
-    LayoutInflater inflater =
-        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-    inflater.inflate(R.layout.new_voicemail_media_player_layout, this);
-  }
-
-  @Override
-  protected void onFinishInflate() {
-    super.onFinishInflate();
-    LogUtil.enterBlock("NewVoicemailMediaPlayer.onFinishInflate");
-    initializeMediaPlayerButtons();
-    setupListenersForMediaPlayerButtons();
-  }
-
-  private void initializeMediaPlayerButtons() {
-    playButton = findViewById(R.id.playButton);
-    speakerButton = findViewById(R.id.speakerButton);
-    deleteButton = findViewById(R.id.deleteButton);
-  }
-
-  private void setupListenersForMediaPlayerButtons() {
-    playButton.setOnClickListener(playButtonListener);
-    speakerButton.setOnClickListener(speakerButtonListener);
-    deleteButton.setOnClickListener(deleteButtonListener);
-  }
-
-  private final View.OnClickListener playButtonListener =
-      new View.OnClickListener() {
-        @Override
-        public void onClick(View view) {
-          LogUtil.i("NewVoicemailMediaPlayer.playButtonListener", "onClick");
-        }
-      };
-
-  private final View.OnClickListener speakerButtonListener =
-      new View.OnClickListener() {
-        @Override
-        public void onClick(View view) {
-          LogUtil.i("NewVoicemailMediaPlayer.speakerButtonListener", "onClick");
-        }
-      };
-
-  private final View.OnClickListener deleteButtonListener =
-      new View.OnClickListener() {
-        @Override
-        public void onClick(View view) {
-          LogUtil.i("NewVoicemailMediaPlayer.deleteButtonListener", "onClick");
-        }
-      };
-}
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java
new file mode 100644
index 0000000..1e56a81
--- /dev/null
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+package com.android.dialer.voicemail.listui;
+
+import android.app.FragmentManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.Pair;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+
+/**
+ * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded.
+ */
+public class NewVoicemailMediaPlayerView extends LinearLayout {
+
+  private Button playButton;
+  private Button speakerButton;
+  private Button deleteButton;
+  private Uri voicemailUri;
+  private FragmentManager fragmentManager;
+  private MediaPlayer mediaPlayer;
+
+  public NewVoicemailMediaPlayerView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    LogUtil.enterBlock("NewVoicemailMediaPlayer");
+    LayoutInflater inflater =
+        (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    inflater.inflate(R.layout.new_voicemail_media_player_layout, this);
+  }
+
+  @Override
+  protected void onFinishInflate() {
+    super.onFinishInflate();
+    LogUtil.enterBlock("NewVoicemailMediaPlayer.onFinishInflate");
+    initializeMediaPlayerButtons();
+    setupListenersForMediaPlayerButtons();
+  }
+
+  private void initializeMediaPlayerButtons() {
+    playButton = findViewById(R.id.playButton);
+    speakerButton = findViewById(R.id.speakerButton);
+    deleteButton = findViewById(R.id.deleteButton);
+  }
+
+  private void setupListenersForMediaPlayerButtons() {
+    playButton.setOnClickListener(playButtonListener);
+    speakerButton.setOnClickListener(speakerButtonListener);
+    deleteButton.setOnClickListener(deleteButtonListener);
+  }
+
+  private final View.OnClickListener playButtonListener =
+      view -> playVoicemailWhenAvailableLocally();
+
+  /**
+   * Plays the voicemail when we are able to play the voicemail locally from the device. This
+   * involves checking if the voicemail is available to play locally, if it is, then we setup the
+   * Media Player to play the voicemail. If the voicemail is not available, then we need download
+   * the voicemail from the voicemail server to the device, and then have the Media player play it.
+   */
+  private void playVoicemailWhenAvailableLocally() {
+    LogUtil.enterBlock("playVoicemailWhenAvailableLocally");
+    Worker<Pair<Context, Uri>, Pair<Boolean, Uri>> checkVoicemailHasContent =
+        this::queryVoicemailHasContent;
+    SuccessListener<Pair<Boolean, Uri>> checkVoicemailHasContentCallBack = this::prepareMediaPlayer;
+
+    DialerExecutorComponent.get(getContext())
+        .dialerExecutorFactory()
+        .createUiTaskBuilder(fragmentManager, "lookup_voicemail_content", checkVoicemailHasContent)
+        .onSuccess(checkVoicemailHasContentCallBack)
+        .build()
+        .executeSerial(new Pair<>(getContext(), voicemailUri));
+  }
+
+  private Pair<Boolean, Uri> queryVoicemailHasContent(Pair<Context, Uri> contextUriPair) {
+    Context context = contextUriPair.first;
+    Uri uri = contextUriPair.second;
+
+    try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
+      if (cursor != null && cursor.moveToNext()) {
+        return new Pair<>(
+            cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1,
+            uri);
+      }
+      return new Pair<>(false, uri);
+    }
+  }
+
+  /**
+   * If the voicemail is available to play locally, setup the media player to play it. Otherwise
+   * send a request to download the voicemail and then play it.
+   */
+  private void prepareMediaPlayer(Pair<Boolean, Uri> booleanUriPair) {
+    boolean voicemailAvailableLocally = booleanUriPair.first;
+    Uri uri = booleanUriPair.second;
+    LogUtil.i(
+        "NewVoicemailMediaPlayer.prepareMediaPlayer",
+        "voicemail available locally: %b for voicemailUri: %s",
+        voicemailAvailableLocally,
+        uri.toString());
+
+    if (voicemailAvailableLocally) {
+      try {
+        mediaPlayer = new MediaPlayer();
+        mediaPlayer.setOnPreparedListener(onPreparedListener);
+        mediaPlayer.setOnErrorListener(onErrorListener);
+        mediaPlayer.setOnCompletionListener(onCompletionListener);
+
+        mediaPlayer.reset();
+        mediaPlayer.setDataSource(getContext(), uri);
+
+        mediaPlayer.prepareAsync();
+      } catch (Exception e) {
+        LogUtil.e("NewVoicemailMediaPlayer.prepareMediaPlayer", "IOException " + e);
+      }
+    } else {
+      // TODO(a bug): Add logic for downloading voicemail content from the server.
+      LogUtil.i(
+          "NewVoicemailMediaPlayer.prepareVoicemailForMediaPlayer", "need to download content");
+    }
+  }
+
+  private final View.OnClickListener speakerButtonListener =
+      new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+          LogUtil.i(
+              "NewVoicemailMediaPlayer.speakerButtonListener",
+              "speaker request for voicemailUri: %s",
+              voicemailUri.toString());
+        }
+      };
+
+  private final View.OnClickListener deleteButtonListener =
+      new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+          LogUtil.i(
+              "NewVoicemailMediaPlayer.deleteButtonListener",
+              "delete voicemailUri %s",
+              voicemailUri.toString());
+        }
+      };
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  OnCompletionListener onCompletionListener =
+      new OnCompletionListener() {
+
+        @Override
+        public void onCompletion(MediaPlayer mp) {
+          LogUtil.i(
+              "NewVoicemailMediaPlayer.onCompletionListener",
+              "completed playing voicemailUri: %s",
+              voicemailUri.toString());
+        }
+      };
+
+  private final OnPreparedListener onPreparedListener =
+      new OnPreparedListener() {
+
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+          LogUtil.i(
+              "NewVoicemailMediaPlayer.onPreparedListener",
+              "about to play voicemailUri: %s",
+              voicemailUri.toString());
+          mediaPlayer.start();
+        }
+      };
+
+  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+  OnErrorListener onErrorListener =
+      new OnErrorListener() {
+        @Override
+        public boolean onError(MediaPlayer mp, int what, int extra) {
+          LogUtil.i(
+              "NewVoicemailMediaPlayer.onErrorListener",
+              "error playing voicemailUri: %s",
+              voicemailUri.toString());
+          return false;
+        }
+      };
+
+  public void setVoicemailUri(Uri voicemailUri) {
+    Assert.isNotNull(voicemailUri);
+    this.voicemailUri = voicemailUri;
+  }
+
+  public void setFragmentManager(FragmentManager fragmentManager) {
+    this.fragmentManager = fragmentManager;
+  }
+}
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
index d4bfefd..078a029 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java
@@ -15,6 +15,7 @@
  */
 package com.android.dialer.voicemail.listui;
 
+import android.app.FragmentManager;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
@@ -25,6 +26,7 @@
 import android.view.View.OnClickListener;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
+import com.android.dialer.common.LogUtil;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.lettertile.LetterTileDrawable;
 import com.android.dialer.time.Clock;
@@ -38,7 +40,7 @@
   private final TextView secondaryTextView;
   private final TextView transcriptionTextView;
   private final QuickContactBadge quickContactBadge;
-  private final View mediaPlayerView;
+  private final NewVoicemailMediaPlayerView mediaPlayerView;
   private final Clock clock;
   private boolean isViewHolderExpanded;
   private int viewHolderId;
@@ -47,6 +49,7 @@
   NewVoicemailViewHolder(
       View view, Clock clock, NewVoicemailViewHolderListener newVoicemailViewHolderListener) {
     super(view);
+    LogUtil.enterBlock("NewVoicemailViewHolder");
     this.context = view.getContext();
     primaryTextView = view.findViewById(R.id.primary_text);
     secondaryTextView = view.findViewById(R.id.secondary_text);
@@ -57,7 +60,7 @@
     voicemailViewHolderListener = newVoicemailViewHolderListener;
   }
 
-  void bind(Cursor cursor) {
+  void bind(Cursor cursor, FragmentManager fragmentManager) {
     VoicemailEntry voicemailEntry = VoicemailCursorLoader.toVoicemailEntry(cursor);
     viewHolderId = voicemailEntry.id();
     primaryTextView.setText(VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntry));
@@ -76,6 +79,8 @@
 
     itemView.setOnClickListener(this);
     setPhoto(voicemailEntry);
+    mediaPlayerView.setVoicemailUri(Uri.parse(voicemailEntry.voicemailUri()));
+    mediaPlayerView.setFragmentManager(fragmentManager);
   }
 
   // TODO(uabdullah): Consider/Implement TYPE (e.g Spam, TYPE_VOICEMAIL)
@@ -97,6 +102,7 @@
   }
 
   void expandViewHolder() {
+    LogUtil.i("NewVoicemailViewHolder.expandViewHolder", "voicemail id: %d", viewHolderId);
     transcriptionTextView.setMaxLines(999);
     isViewHolderExpanded = true;
     mediaPlayerView.setVisibility(View.VISIBLE);
@@ -117,6 +123,7 @@
 
   @Override
   public void onClick(View v) {
+    LogUtil.i("NewVoicemailViewHolder.onClick", "voicemail id: %d", viewHolderId);
     if (isViewHolderExpanded) {
       collapseViewHolder();
     } else {
diff --git a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
index 80bb1b5..78d2785 100644
--- a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
+++ b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml
@@ -82,7 +82,7 @@
         android:layout_gravity="center_vertical"
         android:visibility="gone"/>
 
-    <com.android.dialer.voicemail.listui.NewVoicemailMediaPlayer
+    <com.android.dialer.voicemail.listui.NewVoicemailMediaPlayerView
         android:id="@+id/new_voicemail_media_player"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/packages.mk b/packages.mk
index 223e40d..86d8407 100644
--- a/packages.mk
+++ b/packages.mk
@@ -41,6 +41,7 @@
 	com.android.dialer.phonenumberutil \
 	com.android.dialer.postcall \
 	com.android.dialer.precall.impl \
+	com.android.dialer.precall.externalreceiver \
 	com.android.dialer.preferredsim.impl \
 	com.android.dialer.searchfragment.common \
 	com.android.dialer.searchfragment.cp2 \