Update Dialer source from latest green build.

* Refactor voicemail component
* Add new enriched calling components

Test: treehugger, manual aosp testing

Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
index a21876b..442ad26 100644
--- a/java/com/android/incallui/AnswerScreenPresenter.java
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -20,6 +20,7 @@
 import android.support.annotation.FloatRange;
 import android.support.annotation.NonNull;
 import android.support.v4.os.UserManagerCompat;
+import android.telecom.VideoProfile;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.answer.protocol.AnswerScreen;
@@ -71,18 +72,26 @@
   }
 
   @Override
-  public void onAnswer(int videoState) {
+  public void onAnswer(boolean answerVideoAsAudio) {
     if (answerScreen.isVideoUpgradeRequest()) {
-      call.acceptUpgradeRequest(videoState);
+      if (answerVideoAsAudio) {
+        call.getVideoTech().acceptVideoRequestAsAudio();
+      } else {
+        call.getVideoTech().acceptVideoRequest();
+      }
     } else {
-      call.answer(videoState);
+      if (answerVideoAsAudio) {
+        call.answer(VideoProfile.STATE_AUDIO_ONLY);
+      } else {
+        call.answer();
+      }
     }
   }
 
   @Override
   public void onReject() {
     if (answerScreen.isVideoUpgradeRequest()) {
-      call.declineUpgradeRequest();
+      call.getVideoTech().declineVideoRequest();
     } else {
       call.reject(false /* rejectWithMessage */, null);
     }
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
index fc47bf5..fc4e7df 100644
--- a/java/com/android/incallui/AnswerScreenPresenterStub.java
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -34,7 +34,7 @@
   public void onRejectCallWithMessage(String message) {}
 
   @Override
-  public void onAnswer(int videoState) {}
+  public void onAnswer(boolean answerVideoAsAudio) {}
 
   @Override
   public void onReject() {}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index d6f4cdd..c5c43f7 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -17,17 +17,13 @@
 package com.android.incallui;
 
 import android.content.Context;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.os.UserManagerCompat;
 import android.telecom.CallAudioState;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
 import com.android.contacts.common.compat.CallCompat;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
-import com.android.dialer.compat.SdkVersionOverride;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.incallui.AudioModeProvider.AudioModeListener;
@@ -39,6 +35,7 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CameraDirection;
 import com.android.incallui.call.TelecomAdapter;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.protocol.InCallButtonIds;
@@ -212,6 +209,13 @@
   @Override
   public void muteClicked(boolean checked) {
     LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+    Logger.get(mContext)
+        .logCallImpression(
+            checked
+                ? DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_MUTE
+                : DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_MUTE,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
     TelecomAdapter.getInstance().mute(checked);
   }
 
@@ -262,18 +266,8 @@
 
   @Override
   public void changeToVideoClicked() {
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
-    int currVideoState = mCall.getVideoState();
-    int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
-    currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
-
-    VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
-    videoCall.sendSessionModifyRequest(videoProfile);
-    mCall.setSessionModificationState(
-        DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+    LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
+    mCall.getVideoTech().upgradeToVideo();
   }
 
   @Override
@@ -300,26 +294,25 @@
     InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
     cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
 
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
-
     String cameraId = cameraManager.getActiveCameraId();
     if (cameraId != null) {
       final int cameraDir =
           cameraManager.isUsingFrontFacingCamera()
-              ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
-              : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
-      mCall.getVideoSettings().setCameraDir(cameraDir);
-      videoCall.setCamera(cameraId);
-      videoCall.requestCameraCapabilities();
+              ? CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+              : CameraDirection.CAMERA_DIRECTION_BACK_FACING;
+      mCall.setCameraDir(cameraDir);
+      mCall.getVideoTech().setCamera(cameraId);
     }
   }
 
   @Override
   public void toggleCameraClicked() {
     LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+    Logger.get(mContext)
+        .logCallImpression(
+            DialerImpression.Type.IN_CALL_SCREEN_SWAP_CAMERA,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
     switchCameraClicked(
         !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
   }
@@ -333,24 +326,19 @@
   @Override
   public void pauseVideoClicked(boolean pause) {
     LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
-    VideoCall videoCall = mCall.getVideoCall();
-    if (videoCall == null) {
-      return;
-    }
 
-    int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+    Logger.get(mContext)
+        .logCallImpression(
+            pause
+                ? DialerImpression.Type.IN_CALL_SCREEN_TURN_OFF_VIDEO
+                : DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_VIDEO,
+            mCall.getUniqueCallId(),
+            mCall.getTimeAddedMs());
+
     if (pause) {
-      videoCall.setCamera(null);
-      VideoProfile videoProfile =
-          new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
-      videoCall.sendSessionModifyRequest(videoProfile);
+      mCall.getVideoTech().stopTransmission();
     } else {
-      InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
-      videoCall.setCamera(cameraManager.getActiveCameraId());
-      VideoProfile videoProfile =
-          new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
-      videoCall.sendSessionModifyRequest(videoProfile);
-      mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+      mCall.getVideoTech().resumeTransmission();
     }
 
     mInCallButtonUi.setVideoPaused(pause);
@@ -386,7 +374,7 @@
    */
   private void updateButtonsState(DialerCall call) {
     LogUtil.v("CallButtonPresenter.updateButtonsState", "");
-    final boolean isVideo = VideoUtils.isVideoCall(call);
+    final boolean isVideo = call.isVideoCall();
 
     // Common functionality (audio, hold, etc).
     // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
@@ -402,7 +390,7 @@
     final boolean showAddCall =
         TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
     final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
-    final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+    final boolean showUpgradeToVideo = !isVideo && (hasVideoCallCapabilities(call));
     final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
     final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
 
@@ -427,8 +415,7 @@
         InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
     if (isVideo) {
-      mInCallButtonUi.setVideoPaused(
-          !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+      mInCallButtonUi.setVideoPaused(!call.getVideoTech().isTransmitting() || !hasCameraPermission);
     }
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
     mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
@@ -437,12 +424,7 @@
   }
 
   private boolean hasVideoCallCapabilities(DialerCall call) {
-    if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
-      return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
-          && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
-    }
-    // In L, this single flag represents both video transmitting and receiving capabilities
-    return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+    return call.getVideoTech().isAvailable();
   }
 
   /**
@@ -454,6 +436,7 @@
    * @return {@code true} if downgrading to an audio-only call from a video call is supported.
    */
   private boolean isDowngradeToAudioSupported(DialerCall call) {
+    // TODO(b/33676907): If there is an RCS video share session, return true here
     return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
   }
 
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
index 9307757..668692d 100644
--- a/java/com/android/incallui/CallCardPresenter.java
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -19,7 +19,6 @@
 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
 
 import android.Manifest;
-import android.app.Application;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -29,6 +28,7 @@
 import android.hardware.display.DisplayManager;
 import android.os.BatteryManager;
 import android.os.Handler;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v4.content.ContextCompat;
@@ -46,9 +46,12 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.enrichedcall.Session;
 import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.oem.MotorolaUtils;
 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
 import com.android.incallui.InCallPresenter.InCallDetailsListener;
@@ -58,14 +61,16 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.calllocation.CallLocation;
+import com.android.incallui.calllocation.CallLocationComponent;
 import com.android.incallui.incall.protocol.ContactPhotoType;
 import com.android.incallui.incall.protocol.InCallScreen;
 import com.android.incallui.incall.protocol.InCallScreenDelegate;
 import com.android.incallui.incall.protocol.PrimaryCallState;
 import com.android.incallui.incall.protocol.PrimaryInfo;
 import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.videotech.VideoTech;
 import java.lang.ref.WeakReference;
 
 /**
@@ -116,7 +121,8 @@
   private InCallScreen mInCallScreen;
   private boolean isInCallScreenReady;
   private boolean shouldSendAccessibilityEvent;
-  private final String locationModule = null;
+
+  @NonNull private final CallLocation callLocation;
   private final Runnable sendAccessibilityEventRunnable =
       new Runnable() {
         @Override
@@ -135,6 +141,7 @@
   public CallCardPresenter(Context context) {
     LogUtil.i("CallCardController.constructor", null);
     mContext = Assert.isNotNull(context).getApplicationContext();
+    callLocation = CallLocationComponent.get(mContext).getCallLocation();
   }
 
   private static boolean hasCallSubject(DialerCall call) {
@@ -175,8 +182,7 @@
       mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
     }
 
-    EnrichedCallManager.Accessor.getInstance(((Application) mContext))
-        .registerStateChangedListener(this);
+    EnrichedCallComponent.get(mContext).getEnrichedCallManager().registerStateChangedListener(this);
 
     // Contact search may have completed before ui is ready.
     if (mPrimaryContactInfo != null) {
@@ -189,6 +195,11 @@
     InCallPresenter.getInstance().addDetailsListener(this);
     InCallPresenter.getInstance().addInCallEventListener(this);
     isInCallScreenReady = true;
+
+    // Showing the location may have been skipped if the UI wasn't ready during previous layout.
+    if (shouldShowLocation()) {
+      updatePrimaryDisplayInfo();
+    }
   }
 
   @Override
@@ -196,7 +207,8 @@
     LogUtil.i("CallCardController.onInCallScreenUnready", null);
     Assert.checkState(isInCallScreenReady);
 
-    EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+    EnrichedCallComponent.get(mContext)
+        .getEnrichedCallManager()
         .unregisterStateChangedListener(this);
     // stop getting call state changes
     InCallPresenter.getInstance().removeListener(this);
@@ -207,6 +219,8 @@
       mPrimary.removeListener(this);
     }
 
+    callLocation.close();
+
     mPrimary = null;
     mPrimaryContactInfo = null;
     mSecondaryContactInfo = null;
@@ -282,7 +296,6 @@
               mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
       updatePrimaryDisplayInfo();
       maybeStartSearch(mPrimary, true);
-      maybeClearSessionModificationState(mPrimary);
     }
 
     if (previousPrimary != null && mPrimary == null) {
@@ -300,7 +313,6 @@
               mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
       updateSecondaryDisplayInfo();
       maybeStartSearch(mSecondary, false);
-      maybeClearSessionModificationState(mSecondary);
     }
 
     // Set the call state
@@ -373,25 +385,18 @@
   @Override
   public void onDialerCallUpgradeToVideo() {}
 
-  /**
-   * Handles a change to the session modification state for a call.
-   *
-   * @param sessionModificationState The new session modification state.
-   */
+  /** Handles a change to the session modification state for a call. */
   @Override
-  public void onDialerCallSessionModificationStateChange(
-      @SessionModificationState int sessionModificationState) {
-    LogUtil.v(
-        "CallCardPresenter.onDialerCallSessionModificationStateChange",
-        "state: " + sessionModificationState);
+  public void onDialerCallSessionModificationStateChange() {
+    LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange");
 
     if (mPrimary == null) {
       return;
     }
     getUi()
         .setEndCallButtonEnabled(
-            sessionModificationState
-                != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+            mPrimary.getVideoTech().getSessionModificationState()
+                != VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
             true /* shouldAnimate */);
     updatePrimaryCallState();
   }
@@ -418,6 +423,13 @@
                   && mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
       boolean isHdAudioCall =
           isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+      boolean isAttemptingHdAudioCall =
+          !isHdAudioCall
+              && !mPrimary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN)
+              && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(mContext);
+
+      boolean isBusiness = mPrimaryContactInfo != null && mPrimaryContactInfo.isBusiness;
+
       // Check for video state change and update the visibility of the contact photo.  The contact
       // photo is hidden when the incoming video surface is shown.
       // The contact photo visibility can also change in setPrimary().
@@ -427,8 +439,8 @@
           .setCallState(
               new PrimaryCallState(
                   mPrimary.getState(),
-                  mPrimary.getVideoState(),
-                  mPrimary.getSessionModificationState(),
+                  mPrimary.isVideoCall(),
+                  mPrimary.getVideoTech().getSessionModificationState(),
                   mPrimary.getDisconnectCause(),
                   getConnectionLabel(),
                   getCallStateIcon(),
@@ -438,12 +450,14 @@
                   mPrimary.hasProperty(Details.PROPERTY_WIFI),
                   mPrimary.isConferenceCall(),
                   isWorkCall,
+                  isAttemptingHdAudioCall,
                   isHdAudioCall,
                   !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
                   shouldShowContactPhoto,
                   mPrimary.getConnectTimeMillis(),
                   CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
-                  mPrimary.isRemotelyHeld()));
+                  mPrimary.isRemotelyHeld(),
+                  isBusiness));
 
       InCallActivity activity =
           (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
@@ -508,15 +522,6 @@
     }
   }
 
-  private void maybeClearSessionModificationState(DialerCall call) {
-    @SessionModificationState int state = call.getSessionModificationState();
-    if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
-        && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
-      LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
-  }
-
   /** Starts a query for more contact data for the save primary and secondary calls. */
   private void startContactInfoSearch(
       final DialerCall call, final boolean isPrimary, boolean isIncoming) {
@@ -642,13 +647,17 @@
     // DialerCall placed through a work phone account.
     boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
 
-    Session enrichedCallSession =
-        mPrimary.getNumber() == null
-            ? null
-            : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
-                .getSession(mPrimary.getNumber());
-    MultimediaData enrichedCallMultimediaData =
-        enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+    MultimediaData multimediaData = null;
+    if (mPrimary.getNumber() != null) {
+      Session enrichedCallSession =
+          EnrichedCallComponent.get(mContext)
+              .getEnrichedCallManager()
+              .getSession(mPrimary.getUniqueCallId(), mPrimary.getNumber());
+      if (enrichedCallSession != null) {
+        enrichedCallSession.setUniqueDialerCallId(mPrimary.getUniqueCallId());
+        multimediaData = enrichedCallSession.getMultimediaData();
+      }
+    }
 
     if (mPrimary.isConferenceCall()) {
       LogUtil.v(
@@ -671,7 +680,8 @@
               false /* answeringDisconnectsOngoingCall */,
               shouldShowLocation(),
               null /* contactInfoLookupKey */,
-              null /* enrichedCallMultimediaData */));
+              null /* enrichedCallMultimediaData */,
+              mPrimary.getNumberPresentation()));
     } else if (mPrimaryContactInfo != null) {
       LogUtil.v(
           "CallCardPresenter.updatePrimaryDisplayInfo",
@@ -696,6 +706,7 @@
       }
 
       boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+
       // DialerCall with caller that is a work contact.
       boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
       mInCallScreen.setPrimary(
@@ -714,13 +725,52 @@
               mPrimary.answeringDisconnectsForegroundVideoCall(),
               shouldShowLocation(),
               mPrimaryContactInfo.lookupKey,
-              enrichedCallMultimediaData));
+              multimediaData,
+              mPrimary.getNumberPresentation()));
     } else {
       // Clear the primary display info.
       mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
     }
 
-    mInCallScreen.showLocationUi(null);
+    if (isInCallScreenReady) {
+      mInCallScreen.showLocationUi(getLocationFragment());
+    } else {
+      LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location");
+    }
+  }
+
+  private Fragment getLocationFragment() {
+    if (!ConfigProviderBindings.get(mContext)
+        .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config.");
+      return null;
+    }
+    if (!shouldShowLocation()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location");
+      return null;
+    }
+    if (!hasLocationPermission()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission.");
+      return null;
+    }
+    if (isBatteryTooLowForEmergencyLocation()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "low battery.");
+      return null;
+    }
+    if (ActivityCompat.isInMultiWindowMode(mInCallScreen.getInCallScreenFragment().getActivity())) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode");
+      return null;
+    }
+    if (mPrimary.isVideoCall()) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported");
+      return null;
+    }
+    if (!callLocation.canGetLocation(mContext)) {
+      LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location");
+      return null;
+    }
+    LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment");
+    return callLocation.getLocationFragment(mContext);
   }
 
   private boolean shouldShowLocation() {
@@ -972,8 +1022,8 @@
         || callState == DialerCall.State.INCOMING) {
       return false;
     }
-    if (mPrimary.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    if (mPrimary.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       return false;
     }
     return true;
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
index f8d7ac6..d620d47 100644
--- a/java/com/android/incallui/CallerInfoAsyncQuery.java
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -55,7 +55,7 @@
 public class CallerInfoAsyncQuery {
 
   /** Interface for a CallerInfoAsyncQueryHandler result return. */
-  public interface OnQueryCompleteListener {
+  interface OnQueryCompleteListener {
 
     /** Called when the query is complete. */
     @MainThread
@@ -85,7 +85,7 @@
   private CallerInfoAsyncQuery() {}
 
   @RequiresPermission(Manifest.permission.READ_CONTACTS)
-  public static void startQuery(
+  static void startQuery(
       final int token,
       final Context context,
       final CallerInfo info,
@@ -99,7 +99,7 @@
         new OnQueryCompleteListener() {
           @Override
           public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
-            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
             // If there are no other directory queries, make sure that the listener is
             // notified of this result.  see b/27621628
             if ((ci != null && ci.contactExists)
@@ -112,6 +112,7 @@
 
           @Override
           public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+            Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
             listener.onDataLoaded(token, cookie, ci);
           }
         };
@@ -270,9 +271,9 @@
   /* Directory lookup related code - END */
 
   /** Simple exception used to communicate problems with the query pool. */
-  public static class QueryPoolException extends SQLException {
+  private static class QueryPoolException extends SQLException {
 
-    public QueryPoolException(String error) {
+    QueryPoolException(String error) {
       super(error);
     }
   }
@@ -337,7 +338,7 @@
       }
     }
 
-    public OnQueryCompleteListener newListener(long directoryId) {
+    OnQueryCompleteListener newListener(long directoryId) {
       return new DirectoryQueryCompleteListener(directoryId);
     }
 
@@ -351,11 +352,13 @@
 
       @Override
       public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+        Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
         mListener.onDataLoaded(token, cookie, ci);
       }
 
       @Override
       public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+        Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
         onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
       }
     }
@@ -446,7 +449,7 @@
       mCallerInfo = null;
     }
 
-    protected void updateData(int token, Object cookie, Cursor cursor) {
+    void updateData(int token, Object cookie, Cursor cursor) {
       try {
         Log.d(this, "##### updateData() #####  for token: " + token);
 
@@ -549,9 +552,9 @@
      * times before the query is complete. All accesses (listeners) must be queued up and informed
      * in order when the query is complete.
      */
-    protected class CallerInfoWorkerHandler extends WorkerHandler {
+    class CallerInfoWorkerHandler extends WorkerHandler {
 
-      public CallerInfoWorkerHandler(Looper looper) {
+      CallerInfoWorkerHandler(Looper looper) {
         super(looper);
       }
 
@@ -624,7 +627,7 @@
             case EVENT_ADD_LISTENER:
               updateData(msg.arg1, cw, (Cursor) args.result);
               break;
-            default:
+            default: // fall out
           }
           Message reply = args.handler.obtainMessage(msg.what);
           reply.obj = args;
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
index 9f57fba..7c14533 100644
--- a/java/com/android/incallui/CallerInfoUtils.java
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -22,6 +22,7 @@
 import android.content.Loader.OnLoadCompleteListener;
 import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.support.annotation.NonNull;
 import android.support.v4.content.ContextCompat;
 import android.telecom.PhoneAccount;
 import android.telecom.TelecomManager;
@@ -53,7 +54,7 @@
    * OnQueryCompleteListener (which contains information about the phone number label, user's name,
    * etc).
    */
-  public static CallerInfo getCallerInfoForCall(
+  static CallerInfo getCallerInfoForCall(
       Context context,
       DialerCall call,
       Object cookie,
@@ -81,7 +82,7 @@
     return info;
   }
 
-  public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+  static CallerInfo buildCallerInfo(Context context, DialerCall call) {
     CallerInfo info = new CallerInfo();
 
     // Store CNAP information retrieved from the Connection (we want to do this
@@ -91,6 +92,7 @@
     info.numberPresentation = call.getNumberPresentation();
     info.namePresentation = call.getCnapNamePresentation();
     info.callSubject = call.getCallSubject();
+    info.contactExists = false;
 
     String number = call.getNumber();
     if (!TextUtils.isEmpty(number)) {
@@ -109,9 +111,7 @@
     // Because the InCallUI is immediately launched before the call is connected, occasionally
     // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
     // This call should still be handled as a voicemail call.
-    if ((call.getHandle() != null
-            && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
-        || isVoiceMailNumber(context, call)) {
+    if (isVoiceMailNumber(context, call)) {
       info.markAsVoiceMail(context);
     }
 
@@ -145,11 +145,17 @@
     return cacheInfo;
   }
 
-  public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+  public static boolean isVoiceMailNumber(Context context, @NonNull DialerCall call) {
+    if (call.getHandle() != null
+        && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) {
+      return true;
+    }
+
     if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
         != PackageManager.PERMISSION_GRANTED) {
       return false;
     }
+
     return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
   }
 
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
index 4d4d94a..c4e25e7 100644
--- a/java/com/android/incallui/ContactInfoCache.java
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -35,6 +35,7 @@
 import android.support.annotation.WorkerThread;
 import android.support.v4.os.UserManagerCompat;
 import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -74,10 +75,11 @@
   private final PhoneNumberService mPhoneNumberService;
   // Cache info map needs to be thread-safe since it could be modified by both main thread and
   // worker thread.
-  private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
   private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
   private Drawable mDefaultContactPhotoDrawable;
   private Drawable mConferencePhotoDrawable;
+  private int mQueryId;
 
   private ContactInfoCache(Context context) {
     mContext = context;
@@ -91,7 +93,7 @@
     return sCache;
   }
 
-  public static ContactCacheEntry buildCacheEntryFromCall(
+  static ContactCacheEntry buildCacheEntryFromCall(
       Context context, DialerCall call, boolean isIncoming) {
     final ContactCacheEntry entry = new ContactCacheEntry();
 
@@ -103,7 +105,7 @@
   }
 
   /** Populate a cache entry from a call (which got converted into a caller info). */
-  public static void populateCacheEntry(
+  private static void populateCacheEntry(
       @NonNull Context context,
       @NonNull CallerInfo info,
       @NonNull ContactCacheEntry cce,
@@ -153,7 +155,7 @@
       // (Typically, we promote the phone number up to the "name" slot
       // onscreen, and possibly display a descriptive string in the
       // "number" slot.)
-      if (TextUtils.isEmpty(number)) {
+      if (TextUtils.isEmpty(number) && TextUtils.isEmpty(info.cnapName)) {
         // No name *or* number! Display a generic "unknown" string
         // (or potentially some other default based on the presentation.)
         displayName = getPresentationString(context, presentation, info.callSubject);
@@ -236,6 +238,7 @@
     cce.label = label;
     cce.isSipCall = isSipCall;
     cce.userType = info.userType;
+    cce.originalPhoneNumber = info.phoneNumber;
 
     if (info.contactExists) {
       cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
@@ -261,11 +264,11 @@
     return name;
   }
 
-  public ContactCacheEntry getInfo(String callId) {
+  ContactCacheEntry getInfo(String callId) {
     return mInfoMap.get(callId);
   }
 
-  public void maybeInsertCnapInformationIntoCache(
+  void maybeInsertCnapInformationIntoCache(
       Context context, final DialerCall call, final CallerInfo info) {
     final CachedNumberLookupService cachedNumberLookupService =
         PhoneNumberCache.get(context).getCachedNumberLookupService();
@@ -331,8 +334,13 @@
     final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
     Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
 
-    // If we have a previously obtained intermediate result return that now
-    if (cacheEntry != null) {
+    // We need to force a new query if phone number has changed.
+    boolean forceQuery = needForceQuery(call, cacheEntry);
+    Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
+
+    // If we have a previously obtained intermediate result return that now except needs
+    // force query.
+    if (cacheEntry != null && !forceQuery) {
       Log.d(
           TAG,
           "Contact lookup. In memory cache hit; lookup "
@@ -346,14 +354,19 @@
 
     // If the entry already exists, add callback
     if (callBacks != null) {
+      Log.d(TAG, "Another query is in progress, add callback only.");
       callBacks.add(callback);
-      return;
+      if (!forceQuery) {
+        Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
+        return;
+      }
+    } else {
+      Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+      // New lookup
+      callBacks = new ArraySet<>();
+      callBacks.add(callback);
+      mCallBacks.put(callId, callBacks);
     }
-    Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
-    // New lookup
-    callBacks = new ArraySet<>();
-    callBacks.add(callback);
-    mCallBacks.put(callId, callBacks);
 
     /**
      * Performs a query for caller information. Save any immediate data we get from the query. An
@@ -361,25 +374,47 @@
      * such as those for voicemail and emergency call information, will not perform an additional
      * asynchronous query.
      */
+    final CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
+    mQueryId++;
     final CallerInfo callerInfo =
         CallerInfoUtils.getCallerInfoForCall(
             mContext,
             call,
             new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
-            new FindInfoCallback(isIncoming));
+            new FindInfoCallback(isIncoming, queryToken));
 
-    updateCallerInfoInCacheOnAnyThread(
-        callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
-    sendInfoNotifications(callId, mInfoMap.get(callId));
+    if (cacheEntry != null) {
+      // We should not override the old cache item until the new query is
+      // back. We should only update the queryId. Otherwise, we may see
+      // flicker of the name and image (old cache -> new cache before query
+      // -> new cache after query)
+      cacheEntry.queryId = queryToken.mQueryId;
+      Log.d(TAG, "There is an existing cache. Do not override until new query is back");
+    } else {
+      ContactCacheEntry initialCacheEntry =
+          updateCallerInfoInCacheOnAnyThread(
+              callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
+      sendInfoNotifications(callId, initialCacheEntry);
+    }
   }
 
   @AnyThread
-  private void updateCallerInfoInCacheOnAnyThread(
+  private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
       String callId,
       int numberPresentation,
       CallerInfo callerInfo,
       boolean isIncoming,
-      boolean didLocalLookup) {
+      boolean didLocalLookup,
+      CallerInfoQueryToken queryToken) {
+    Log.d(
+        TAG,
+        "updateCallerInfoInCacheOnAnyThread: callId = "
+            + callId
+            + "; queryId = "
+            + queryToken.mQueryId
+            + "; didLocalLookup = "
+            + didLocalLookup);
+
     int presentationMode = numberPresentation;
     if (callerInfo.contactExists
         || callerInfo.isEmergencyNumber()
@@ -387,38 +422,57 @@
       presentationMode = TelecomManager.PRESENTATION_ALLOWED;
     }
 
-    synchronized (mInfoMap) {
-      ContactCacheEntry cacheEntry = mInfoMap.get(callId);
-      // Ensure we always have a cacheEntry. Replace the existing entry if
-      // it has no name or if we found a local contact.
-      if (cacheEntry == null
-          || TextUtils.isEmpty(cacheEntry.namePrimary)
-          || callerInfo.contactExists) {
-        cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
-        mInfoMap.put(callId, cacheEntry);
-      }
-      if (didLocalLookup) {
-        // Before issuing a request for more data from other services, we only check that the
-        // contact wasn't found in the local DB.  We don't check the if the cache entry already
-        // has a name because we allow overriding cnap data with data from other services.
-        if (!callerInfo.contactExists && mPhoneNumberService != null) {
-          Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
-          final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
-          mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
-        } else if (cacheEntry.displayPhotoUri != null) {
-          Log.d(TAG, "Contact lookup. Local contact found, starting image load");
-          // Load the image with a callback to update the image state.
-          // When the load is finished, onImageLoadComplete() will be called.
-          cacheEntry.hasPhotoToLoad = true;
-          ContactsAsyncHelper.startObtainPhotoAsync(
-              TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
-              mContext,
-              cacheEntry.displayPhotoUri,
-              ContactInfoCache.this,
-              callId);
+    // We always replace the entry. The only exception is the same photo case.
+    ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+    cacheEntry.queryId = queryToken.mQueryId;
+
+    ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+    Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
+
+    if (didLocalLookup) {
+      // Before issuing a request for more data from other services, we only check that the
+      // contact wasn't found in the local DB.  We don't check the if the cache entry already
+      // has a name because we allow overriding cnap data with data from other services.
+      if (!callerInfo.contactExists && mPhoneNumberService != null) {
+        Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+        final PhoneNumberServiceListener listener =
+            new PhoneNumberServiceListener(callId, queryToken.mQueryId);
+        cacheEntry.hasPendingQuery = true;
+        mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+      } else if (cacheEntry.displayPhotoUri != null) {
+        // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
+        // we will still trigger force query so that the number can be updated on
+        // the calling screen. We need not query the image again if the previous
+        // query already has the image to avoid flickering.
+        if (existingCacheEntry != null
+            && existingCacheEntry.displayPhotoUri != null
+            && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
+            && existingCacheEntry.photo != null) {
+          Log.d(TAG, "Same picture. Do not need start image load.");
+          cacheEntry.photo = existingCacheEntry.photo;
+          cacheEntry.photoType = existingCacheEntry.photoType;
+          return cacheEntry;
         }
+
+        Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+        // Load the image with a callback to update the image state.
+        // When the load is finished, onImageLoadComplete() will be called.
+        cacheEntry.hasPendingQuery = true;
+        ContactsAsyncHelper.startObtainPhotoAsync(
+            TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+            mContext,
+            cacheEntry.displayPhotoUri,
+            ContactInfoCache.this,
+            queryToken);
       }
+      Log.d(TAG, "put entry into map: " + cacheEntry);
+      mInfoMap.put(callId, cacheEntry);
+    } else {
+      // Don't overwrite if there is existing cache.
+      Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
+      mInfoMap.putIfAbsent(callId, cacheEntry);
     }
+    return cacheEntry;
   }
 
   /**
@@ -429,35 +483,42 @@
   @Override
   public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
     Assert.isWorkerThread();
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
+    final int queryId = myCookie.mQueryId;
+    if (!isWaitingForThisQuery(callId, queryId)) {
+      return;
+    }
     loadImage(photo, photoIcon, cookie);
   }
 
   private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
-    Log.d(this, "Image load complete with context: ", mContext);
+    Log.d(TAG, "Image load complete with context: ", mContext);
     // TODO: may be nice to update the image view again once the newer one
     // is available on contacts database.
-    String callId = (String) cookie;
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
     ContactCacheEntry entry = mInfoMap.get(callId);
 
     if (entry == null) {
-      Log.e(this, "Image Load received for empty search entry.");
+      Log.e(TAG, "Image Load received for empty search entry.");
       clearCallbacks(callId);
       return;
     }
 
-    Log.d(this, "setting photo for entry: ", entry);
+    Log.d(TAG, "setting photo for entry: ", entry);
 
     // Conference call icons are being handled in CallCardPresenter.
     if (photo != null) {
-      Log.v(this, "direct drawable: ", photo);
+      Log.v(TAG, "direct drawable: ", photo);
       entry.photo = photo;
       entry.photoType = ContactPhotoType.CONTACT;
     } else if (photoIcon != null) {
-      Log.v(this, "photo icon: ", photoIcon);
+      Log.v(TAG, "photo icon: ", photoIcon);
       entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
       entry.photoType = ContactPhotoType.CONTACT;
     } else {
-      Log.v(this, "unknown photo");
+      Log.v(TAG, "unknown photo");
       entry.photo = null;
       entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
     }
@@ -471,9 +532,13 @@
   @Override
   public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
     Assert.isMainThread();
-    String callId = (String) cookie;
-    ContactCacheEntry entry = mInfoMap.get(callId);
-    sendImageNotifications(callId, entry);
+    CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+    final String callId = myCookie.mCallId;
+    final int queryId = myCookie.mQueryId;
+    if (!isWaitingForThisQuery(callId, queryId)) {
+      return;
+    }
+    sendImageNotifications(callId, mInfoMap.get(callId));
 
     clearCallbacks(callId);
   }
@@ -482,6 +547,7 @@
   public void clearCache() {
     mInfoMap.clear();
     mCallBacks.clear();
+    mQueryId = 0;
   }
 
   private ContactCacheEntry buildEntry(
@@ -500,9 +566,6 @@
         cce.photo = getDefaultContactPhotoDrawable();
         cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
       }
-    } else if (info.contactDisplayPhotoUri == null) {
-      cce.photo = getDefaultContactPhotoDrawable();
-      cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
     } else {
       cce.displayPhotoUri = info.contactDisplayPhotoUri;
       cce.photo = null;
@@ -528,7 +591,9 @@
   }
 
   /** Sends the updated information to call the callbacks for the entry. */
+  @MainThread
   private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+    Assert.isMainThread();
     final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
     if (callBacks != null) {
       for (ContactInfoCacheCallback callBack : callBacks) {
@@ -537,7 +602,9 @@
     }
   }
 
+  @MainThread
   private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+    Assert.isMainThread();
     final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
     if (callBacks != null && entry.photo != null) {
       for (ContactInfoCacheCallback callBack : callBacks) {
@@ -583,21 +650,26 @@
     public String location;
     public String label;
     public Drawable photo;
-    @ContactPhotoType public int photoType;
-    public boolean isSipCall;
+    @ContactPhotoType int photoType;
+    boolean isSipCall;
     // Note in cache entry whether this is a pending async loading action to know whether to
     // wait for its callback or not.
-    public boolean hasPhotoToLoad;
+    boolean hasPendingQuery;
     /** This will be used for the "view" notification. */
     public Uri contactUri;
     /** Either a display photo or a thumbnail URI. */
-    public Uri displayPhotoUri;
+    Uri displayPhotoUri;
 
     public Uri lookupUri; // Sent to NotificationMananger
     public String lookupKey;
     public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
     public long userType = ContactsUtils.USER_TYPE_CURRENT;
-    public Uri contactRingtoneUri;
+    Uri contactRingtoneUri;
+    /** Query id to identify the query session. */
+    int queryId;
+    /** The phone number without any changes to display to the user (ex: cnap...) */
+    String originalPhoneNumber;
+    boolean isBusiness;
 
     @Override
     public String toString() {
@@ -631,6 +703,10 @@
           + userType
           + ", contactRingtoneUri="
           + contactRingtoneUri
+          + ", queryId="
+          + queryId
+          + ", originalPhoneNumber="
+          + originalPhoneNumber
           + '}';
     }
   }
@@ -648,16 +724,22 @@
   private class FindInfoCallback implements OnQueryCompleteListener {
 
     private final boolean mIsIncoming;
+    private final CallerInfoQueryToken mQueryToken;
 
-    public FindInfoCallback(boolean isIncoming) {
+    public FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
       mIsIncoming = isIncoming;
+      mQueryToken = queryToken;
     }
 
     @Override
     public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
       Assert.isWorkerThread();
       DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
-      updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+      if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+        return;
+      }
+      updateCallerInfoInCacheOnAnyThread(
+          cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
     }
 
     @Override
@@ -665,6 +747,9 @@
       Assert.isMainThread();
       DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
       String callId = cw.callId;
+      if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+        return;
+      }
       ContactCacheEntry cacheEntry = mInfoMap.get(callId);
       // This may happen only when InCallPresenter attempt to cleanup.
       if (cacheEntry == null) {
@@ -673,7 +758,7 @@
         return;
       }
       sendInfoNotifications(callId, cacheEntry);
-      if (!cacheEntry.hasPhotoToLoad) {
+      if (!cacheEntry.hasPendingQuery) {
         if (callerInfo.contactExists) {
           Log.d(TAG, "Contact lookup done. Local contact found, no image.");
         } else {
@@ -691,13 +776,20 @@
       implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
 
     private final String mCallId;
+    private final int mQueryIdOfRemoteLookup;
 
-    PhoneNumberServiceListener(String callId) {
+    PhoneNumberServiceListener(String callId, int queryId) {
       mCallId = callId;
+      mQueryIdOfRemoteLookup = queryId;
     }
 
     @Override
     public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+      Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
+      if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+        return;
+      }
+
       // If we got a miss, this is the end of the lookup pipeline,
       // so clear the callbacks and return.
       if (info == null) {
@@ -705,11 +797,11 @@
         clearCallbacks(mCallId);
         return;
       }
-
       ContactCacheEntry entry = new ContactCacheEntry();
       entry.namePrimary = info.getDisplayName();
       entry.number = info.getNumber();
       entry.contactLookupResult = info.getLookupSource();
+      entry.isBusiness = info.isBusiness();
       final int type = info.getPhoneType();
       final String label = info.getPhoneLabel();
       if (type == Phone.TYPE_CUSTOM) {
@@ -718,33 +810,32 @@
         final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
         entry.label = typeStr == null ? null : typeStr.toString();
       }
-      synchronized (mInfoMap) {
-        final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
-        if (oldEntry != null) {
-          // Location is only obtained from local lookup so persist
-          // the value for remote lookups. Once we have a name this
-          // field is no longer used; it is persisted here in case
-          // the UI is ever changed to use it.
-          entry.location = oldEntry.location;
-          // Contact specific ringtone is obtained from local lookup.
-          entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
-        }
-
-        // If no image and it's a business, switch to using the default business avatar.
-        if (info.getImageUrl() == null && info.isBusiness()) {
-          Log.d(TAG, "Business has no image. Using default.");
-          entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
-          entry.photoType = ContactPhotoType.BUSINESS;
-        }
-
-        mInfoMap.put(mCallId, entry);
+      final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+      if (oldEntry != null) {
+        // Location is only obtained from local lookup so persist
+        // the value for remote lookups. Once we have a name this
+        // field is no longer used; it is persisted here in case
+        // the UI is ever changed to use it.
+        entry.location = oldEntry.location;
+        // Contact specific ringtone is obtained from local lookup.
+        entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
       }
+
+      // If no image and it's a business, switch to using the default business avatar.
+      if (info.getImageUrl() == null && info.isBusiness()) {
+        Log.d(TAG, "Business has no image. Using default.");
+        entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+        entry.photoType = ContactPhotoType.BUSINESS;
+      }
+
+      Log.d(TAG, "put entry into map: " + entry);
+      mInfoMap.put(mCallId, entry);
       sendInfoNotifications(mCallId, entry);
 
-      entry.hasPhotoToLoad = info.getImageUrl() != null;
+      entry.hasPendingQuery = info.getImageUrl() != null;
 
       // If there is no image then we should not expect another callback.
-      if (!entry.hasPhotoToLoad) {
+      if (!entry.hasPendingQuery) {
         // We're done, so clear callbacks
         clearCallbacks(mCallId);
       }
@@ -752,8 +843,59 @@
 
     @Override
     public void onImageFetchComplete(Bitmap bitmap) {
-      loadImage(null, bitmap, mCallId);
-      onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+      Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
+      if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+        return;
+      }
+      CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
+      loadImage(null, bitmap, queryToken);
+      onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
+    }
+  }
+
+  private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
+    if (call == null || call.isConferenceCall()) {
+      return false;
+    }
+
+    String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
+    if (cacheEntry == null) {
+      // No info in the map yet so it is the 1st query
+      Log.d(TAG, "needForceQuery: first query");
+      return true;
+    }
+    String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
+
+    if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
+      Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
+      return true;
+    }
+
+    return false;
+  }
+
+  private static final class CallerInfoQueryToken {
+    final int mQueryId;
+    final String mCallId;
+
+    CallerInfoQueryToken(int queryId, String callId) {
+      mQueryId = queryId;
+      mCallId = callId;
+    }
+  }
+
+  /** Check if the queryId in the cached map is the same as the one from query result. */
+  private boolean isWaitingForThisQuery(String callId, int queryId) {
+    final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+    if (existingCacheEntry == null) {
+      // This might happen if lookup on background thread comes back before the initial entry is
+      // created.
+      Log.d(TAG, "Cached entry is null.");
+      return true;
+    } else {
+      int waitingQueryId = existingCacheEntry.queryId;
+      Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
+      return waitingQueryId == queryId;
     }
   }
 }
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
index 466e12a..6ec94a6 100644
--- a/java/com/android/incallui/ExternalCallNotifier.java
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -41,6 +41,8 @@
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.BitmapUtil;
 import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCallDelegate;
 import com.android.incallui.call.ExternalCallList;
@@ -57,9 +59,9 @@
 public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
 
   /** Tag used with the notification manager to uniquely identify external call notifications. */
-  private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+  private static final int NOTIFICATION_ID = R.id.notification_external_call;
 
-  private static final int SUMMARY_ID = -1;
+  private static final String NOTIFICATION_GROUP = "ExternalCallNotifier";
   private final Context mContext;
   private final ContactInfoCache mContactInfoCache;
   private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
@@ -186,14 +188,15 @@
 
     NotificationManager notificationManager =
         (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-    notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+    notificationManager.cancel(
+        String.valueOf(mNotifications.get(call).getNotificationId()), NOTIFICATION_ID);
 
     mNotifications.remove(call);
 
     if (mShowingSummary && mNotifications.size() <= 1) {
       // Where a summary notification is showing and there is now not enough notifications to
       // necessitate a summary, cancel the summary.
-      notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+      notificationManager.cancel(NOTIFICATION_GROUP, NOTIFICATION_ID);
       mShowingSummary = false;
 
       // If there is still a single call requiring a notification, re-post the notification as a
@@ -234,7 +237,7 @@
     builder.setOngoing(true);
     // Make the notification prioritized over the other normal notifications.
     builder.setPriority(Notification.PRIORITY_HIGH);
-    builder.setGroup(NOTIFICATION_TAG);
+    builder.setGroup(NOTIFICATION_GROUP);
 
     boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
     // Set the content ("Ongoing call on another device")
@@ -249,6 +252,9 @@
     builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
     builder.addPerson(info.getPersonReference());
 
+    NotificationChannelManager.applyChannel(
+        builder, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+
     // Where the external call supports being transferred to the local device, add an action
     // to the notification to initiate the call pull process.
     if (CallCompat.canPullExternalCall(info.getCall())) {
@@ -281,12 +287,19 @@
     publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
     publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
 
+    NotificationChannelManager.applyChannel(
+        publicBuilder,
+        mContext,
+        Channel.EXTERNAL_CALL,
+        info.getCall().getDetails().getAccountHandle());
+
     builder.setPublicVersion(publicBuilder.build());
     Notification notification = builder.build();
 
     NotificationManager notificationManager =
         (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-    notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+    notificationManager.notify(
+        String.valueOf(info.getNotificationId()), NOTIFICATION_ID, notification);
 
     if (!mShowingSummary && mNotifications.size() > 1) {
       // If the number of notifications shown is > 1, and we're not already showing a group summary,
@@ -297,10 +310,12 @@
       summary.setOngoing(true);
       // Make the notification prioritized over the other normal notifications.
       summary.setPriority(Notification.PRIORITY_HIGH);
-      summary.setGroup(NOTIFICATION_TAG);
+      summary.setGroup(NOTIFICATION_GROUP);
       summary.setGroupSummary(true);
       summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
-      notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+      NotificationChannelManager.applyChannel(
+          summary, mContext, Channel.EXTERNAL_CALL, info.getCall().getDetails().getAccountHandle());
+      notificationManager.notify(NOTIFICATION_GROUP, NOTIFICATION_ID, summary.build());
       mShowingSummary = true;
     }
   }
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
index 3074159..7c43948 100644
--- a/java/com/android/incallui/InCallActivity.java
+++ b/java/com/android/incallui/InCallActivity.java
@@ -32,6 +32,7 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.ActivityCompat;
 import com.android.dialer.logging.Logger;
@@ -44,7 +45,6 @@
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.bindings.InCallBindings;
 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
@@ -89,11 +89,7 @@
   }
 
   public static Intent getIntent(
-      Context context,
-      boolean showDialpad,
-      boolean newOutgoingCall,
-      boolean isVideoCall,
-      boolean isForFullScreen) {
+      Context context, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
     Intent intent = new Intent(Intent.ACTION_MAIN, null);
     intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
     intent.setClass(context, InCallActivity.class);
@@ -192,7 +188,22 @@
   @Override
   public void finish() {
     if (shouldCloseActivityOnFinish()) {
-      super.finish();
+      // When user select incall ui from recents after the call is disconnected, it tries to launch
+      // a new InCallActivity but InCallPresenter is already teared down at this point, which causes
+      // crash.
+      // By calling finishAndRemoveTask() instead of finish() the task associated with
+      // InCallActivity is cleared completely. So system won't try to create a new InCallActivity in
+      // this case.
+      //
+      // Calling finish won't clear the task and normally when an activity finishes it shouldn't
+      // clear the task since there could be parent activity in the same task that's still alive.
+      // But InCallActivity is special since it's singleInstance which means it's root activity and
+      // only instance of activity in the task. So it should be safe to also remove task when
+      // finishing.
+      // It's also necessary in the sense of it's excluded from recents. So whenever the activity
+      // finishes, the task should also be removed since it doesn't make sense to go back to it in
+      // anyway anymore.
+      super.finishAndRemoveTask();
     }
   }
 
@@ -260,18 +271,12 @@
 
   @Override
   public boolean onKeyUp(int keyCode, KeyEvent event) {
-    if (common.onKeyUp(keyCode, event)) {
-      return true;
-    }
-    return super.onKeyUp(keyCode, event);
+    return common.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
   }
 
   @Override
   public boolean onKeyDown(int keyCode, KeyEvent event) {
-    if (common.onKeyDown(keyCode, event)) {
-      return true;
-    }
-    return super.onKeyDown(keyCode, event);
+    return common.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
   }
 
   public boolean isInCallScreenAnimating() {
@@ -411,13 +416,6 @@
     common.setExcludeFromRecents(exclude);
   }
 
-  public void onResolveIntent(
-      DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
-    if (didShowAccountSelectionDialog) {
-      hideMainInCallFragment();
-    }
-  }
-
   @Nullable
   public FragmentManager getDialpadFragmentManager() {
     InCallScreen inCallScreen = getInCallScreen();
@@ -488,7 +486,7 @@
     enableInCallOrientationEventListener(allowOrientationChange);
   }
 
-  private void hideMainInCallFragment() {
+  public void hideMainInCallFragment() {
     LogUtil.i("InCallActivity.hideMainInCallFragment", "");
     if (didShowInCallScreen || didShowVideoCallScreen) {
       FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
@@ -513,8 +511,8 @@
     }
 
     isInShowMainInCallFragment = true;
-    ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
-    boolean shouldShowVideoUi = getShouldShowVideoUi();
+    ShouldShowUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+    ShouldShowUiResult shouldShowVideoUi = getShouldShowVideoUi();
     LogUtil.i(
         "InCallActivity.showMainInCallFragment",
         "shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
@@ -525,7 +523,7 @@
         didShowInCallScreen,
         didShowVideoCallScreen);
     // Only video call ui allows orientation change.
-    setAllowOrientationChange(shouldShowVideoUi);
+    setAllowOrientationChange(shouldShowVideoUi.shouldShow);
 
     FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
     boolean didChangeInCall;
@@ -535,9 +533,9 @@
       didChangeInCall = hideInCallScreenFragment(transaction);
       didChangeVideo = hideVideoCallScreenFragment(transaction);
       didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
-    } else if (shouldShowVideoUi) {
+    } else if (shouldShowVideoUi.shouldShow) {
       didChangeInCall = hideInCallScreenFragment(transaction);
-      didChangeVideo = showVideoCallScreenFragment(transaction);
+      didChangeVideo = showVideoCallScreenFragment(transaction, shouldShowVideoUi.call);
       didChangeAnswer = hideAnswerScreenFragment(transaction);
     } else {
       didChangeInCall = showInCallScreenFragment(transaction);
@@ -552,17 +550,17 @@
     isInShowMainInCallFragment = false;
   }
 
-  private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+  private ShouldShowUiResult getShouldShowAnswerUi() {
     DialerCall call = CallList.getInstance().getIncomingCall();
     if (call != null) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
     call = CallList.getInstance().getVideoUpgradeRequestCall();
     if (call != null) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
     // Check if we're showing the answer screen and the call is disconnected. If this condition is
@@ -574,30 +572,30 @@
     }
     if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
       LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
-      return new ShouldShowAnswerUiResult(true, call);
+      return new ShouldShowUiResult(true, call);
     }
 
-    return new ShouldShowAnswerUiResult(false, null);
+    return new ShouldShowUiResult(false, null);
   }
 
-  private boolean getShouldShowVideoUi() {
+  private static ShouldShowUiResult getShouldShowVideoUi() {
     DialerCall call = CallList.getInstance().getFirstCall();
     if (call == null) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
-      return false;
+      return new ShouldShowUiResult(false, null);
     }
 
-    if (VideoUtils.isVideoCall(call)) {
+    if (call.isVideoCall()) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
-      return true;
+      return new ShouldShowUiResult(true, call);
     }
 
-    if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+    if (call.hasSentVideoUpgradeRequest()) {
       LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
-      return true;
+      return new ShouldShowUiResult(true, call);
     }
 
-    return false;
+    return new ShouldShowUiResult(false, null);
   }
 
   private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
@@ -607,14 +605,15 @@
       return false;
     }
 
-    boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
-    int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+    Assert.checkArgument(call != null, "didShowAnswerScreen was false but call was still null");
+
+    boolean isVideoUpgradeRequest = call.hasReceivedVideoUpgradeRequest();
 
     // Check if we're already showing an answer screen for this call.
     if (didShowAnswerScreen) {
       AnswerScreen answerScreen = getAnswerScreen();
       if (answerScreen.getCallId().equals(call.getId())
-          && answerScreen.getVideoState() == videoState
+          && answerScreen.isVideoCall() == call.isVideoCall()
           && answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
         return false;
       }
@@ -626,7 +625,7 @@
 
     // Show a new answer screen.
     AnswerScreen answerScreen =
-        AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+        AnswerBindings.createAnswerScreen(call.getId(), call.isVideoCall(), isVideoUpgradeRequest);
     transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
 
     Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
@@ -675,12 +674,21 @@
     return true;
   }
 
-  private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+  private boolean showVideoCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
     if (didShowVideoCallScreen) {
-      return false;
+      VideoCallScreen videoCallScreen = getVideoCallScreen();
+      if (videoCallScreen.getCallId().equals(call.getId())) {
+        return false;
+      }
+      LogUtil.i(
+          "InCallActivity.showVideoCallScreenFragment",
+          "video call fragment exists but arguments do not match");
+      hideVideoCallScreenFragment(transaction);
     }
 
-    VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+    LogUtil.i("InCallActivity.showVideoCallScreenFragment", "call: %s", call);
+
+    VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen(call.getId());
     transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
 
     Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
@@ -744,11 +752,11 @@
     return super.dispatchTouchEvent(event);
   }
 
-  private static class ShouldShowAnswerUiResult {
+  private static class ShouldShowUiResult {
     public final boolean shouldShow;
     public final DialerCall call;
 
-    ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+    ShouldShowUiResult(boolean shouldShow, DialerCall call) {
       this.shouldShow = shouldShow;
       this.call = call;
     }
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
index a2467dd..2cdb913 100644
--- a/java/com/android/incallui/InCallActivityCommon.java
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -21,7 +21,6 @@
 import android.app.ActivityManager.TaskDescription;
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.DialogFragment;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
@@ -99,6 +98,7 @@
   private String showPostCharWaitDialogCallId;
   private String showPostCharWaitDialogChars;
   private Dialog dialog;
+  private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
   private InCallOrientationEventListener inCallOrientationEventListener;
   private Animation dialpadSlideInAnimation;
   private Animation dialpadSlideOutAnimation;
@@ -496,11 +496,15 @@
     }
   }
 
-  public void dismissPendingDialogs() {
+  void dismissPendingDialogs() {
     if (dialog != null) {
       dialog.dismiss();
       dialog = null;
     }
+    if (selectPhoneAccountDialogFragment != null) {
+      selectPhoneAccountDialogFragment.dismiss();
+      selectPhoneAccountDialogFragment = null;
+    }
   }
 
   private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
@@ -769,9 +773,7 @@
       outgoingCall = CallList.getInstance().getPendingOutgoingCall();
     }
 
-    boolean isNewOutgoingCall = false;
     if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
-      isNewOutgoingCall = true;
       intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
 
       // InCallActivity is responsible for disconnecting a new outgoing call if there
@@ -789,16 +791,18 @@
     }
 
     boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
-    inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+    if (didShowAccountSelectionDialog) {
+      inCallActivity.hideMainInCallFragment();
+    }
   }
 
   private boolean maybeShowAccountSelectionDialog() {
-    DialerCall call = CallList.getInstance().getWaitingForAccountCall();
-    if (call == null) {
+    DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
+    if (waitingForAccountCall == null) {
       return false;
     }
 
-    Bundle extras = call.getIntentExtras();
+    Bundle extras = waitingForAccountCall.getIntentExtras();
     List<PhoneAccountHandle> phoneAccountHandles;
     if (extras != null) {
       phoneAccountHandles =
@@ -807,14 +811,15 @@
       phoneAccountHandles = new ArrayList<>();
     }
 
-    DialogFragment dialogFragment =
+    selectPhoneAccountDialogFragment =
         SelectPhoneAccountDialogFragment.newInstance(
             R.string.select_phone_account_for_calls,
             true,
             phoneAccountHandles,
             selectAccountListener,
-            call.getId());
-    dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+            waitingForAccountCall.getId());
+    selectPhoneAccountDialogFragment.show(
+        inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
     return true;
   }
 }
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 97105fb..0f3982c 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -42,23 +42,22 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.postcall.PostCall;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.TouchPointManager;
 import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
 import com.android.incallui.answerproximitysensor.PseudoScreenState;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.ExternalCallList;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier;
 import com.android.incallui.call.TelecomAdapter;
-import com.android.incallui.call.VideoUtils;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
 import com.android.incallui.spam.SpamCallListListener;
 import com.android.incallui.util.TelecomCallUtil;
 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -74,8 +73,7 @@
  * presenters that want to listen in on the in-call state changes. TODO: This class has become more
  * of a state machine at this point. Consider renaming.
  */
-public class InCallPresenter
-    implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+public class InCallPresenter implements CallList.Listener {
 
   private static final String EXTRA_FIRST_TIME_SHOWN =
       "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
@@ -173,7 +171,6 @@
   private ProximitySensor mProximitySensor;
   private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
   private boolean mServiceConnected;
-  private boolean mAccountSelectionCancelled;
   private InCallCameraManager mInCallCameraManager;
   private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
   private CallList.Listener mSpamCallListListener;
@@ -347,7 +344,6 @@
     mCallList.addListener(mSpamCallListListener);
 
     VideoPauseController.getInstance().setUp(this);
-    InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
 
     mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
     mContext
@@ -376,7 +372,6 @@
 
     attemptCleanup();
     VideoPauseController.getInstance().tearDown();
-    InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
   }
 
   private void attemptFinishActivity() {
@@ -385,12 +380,6 @@
     if (doFinish) {
       mInCallActivity.setExcludeFromRecents(true);
       mInCallActivity.finish();
-
-      if (mAccountSelectionCancelled) {
-        // This finish is a result of account selection cancellation
-        // do not include activity ending transition
-        mInCallActivity.overridePendingTransition(0, 0);
-      }
     }
   }
 
@@ -664,6 +653,19 @@
     InCallState newState = getPotentialStateFromCallList(callList);
     InCallState oldState = mInCallState;
     Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+
+    // If the user placed a call and was asked to choose the account, but then pressed "Home", the
+    // incall activity for that call will still exist (even if it's not visible). In the case of
+    // an incoming call in that situation, just disconnect that "waiting for account" call and
+    // dismiss the dialog. The same activity will be reused to handle the new incoming call. See
+    // b/33247755 for more details.
+    DialerCall waitingForAccountCall;
+    if (newState == InCallState.INCOMING
+        && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) {
+      waitingForAccountCall.disconnect();
+      mInCallActivity.dismissPendingDialogs();
+    }
+
     newState = startOrFinishUi(newState);
     Log.d(this, "onCallListChange newState changed to " + newState);
 
@@ -705,13 +707,13 @@
 
   @Override
   public void onUpgradeToVideo(DialerCall call) {
-    if (call.getSessionModificationState()
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+    if (call.getVideoTech().getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
         && mInCallState == InCallPresenter.InCallState.INCOMING) {
       LogUtil.i(
           "InCallPresenter.onUpgradeToVideo",
           "rejecting upgrade request due to existing incoming call");
-      call.declineUpgradeRequest();
+      call.getVideoTech().declineVideoRequest();
     }
 
     if (mInCallActivity != null) {
@@ -721,15 +723,15 @@
   }
 
   @Override
-  public void onSessionModificationStateChange(@SessionModificationState int newState) {
+  public void onSessionModificationStateChange(DialerCall call) {
+    int newState = call.getVideoTech().getSessionModificationState();
     LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
     if (mProximitySensor == null) {
       LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
       return;
     }
     mProximitySensor.setIsAttemptingVideoCall(
-        VideoUtils.hasSentVideoUpgradeRequest(newState)
-            || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+        call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
     if (mInCallActivity != null) {
       // Re-evaluate which fragment is being shown.
       mInCallActivity.onPrimaryCallStateChanged();
@@ -754,19 +756,10 @@
     if (call.isEmergencyCall()) {
       FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
     }
-  }
 
-  @Override
-  public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
-    LogUtil.d(
-        "InCallPresenter.onUpgradeToVideoRequest",
-        "call = " + call + " video state = " + videoState);
-
-    if (call == null) {
-      return;
+    if (!call.getLogState().isIncoming && !mCallList.hasLiveCall()) {
+      PostCall.onCallDisconnected(mContext, call.getNumber(), call.getConnectTimeMillis());
     }
-
-    call.setRequestedVideoState(videoState);
   }
 
   /** Given the call list, return the state in which the in-call screen should be. */
@@ -916,6 +909,24 @@
         && !mInCallActivity.isFinishing());
   }
 
+  private boolean isActivityVisible() {
+    return mInCallActivity != null && mInCallActivity.isVisible();
+  }
+
+  boolean shouldShowFullScreenNotification() {
+    /**
+     * This is to cover the case where the incall activity is started but in the background, e.g.
+     * when the user pressed Home from the account selection dialog or an existing call. In the case
+     * that incall activity is already visible, there's no need to configure the notification with a
+     * full screen intent.
+     */
+    LogUtil.d(
+        "InCallPresenter.shouldShowFullScreenNotification",
+        "isActivityVisible: %b",
+        isActivityVisible());
+    return !isActivityVisible();
+  }
+
   /**
    * Determines if the In-Call app is currently changing configuration.
    *
@@ -1018,7 +1029,7 @@
     // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
     // bring it up the UI regardless.
     if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
-      showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+      showInCall(showDialpad, false /* newOutgoingCall */);
     }
   }
 
@@ -1281,7 +1292,7 @@
 
     if (showCallUi || showAccountPicker) {
       Log.i(this, "Start in call UI");
-      showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+      showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
     } else if (startIncomingCallSequence) {
       Log.i(this, "Start Full Screen in call UI");
 
@@ -1332,7 +1343,7 @@
         mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
 
     if (isCallWaiting) {
-      showInCall(false, false, false /* isVideoCall */);
+      showInCall(false, false);
     } else {
       mStatusBarNotifier.updateNotification(mCallList);
     }
@@ -1403,11 +1414,11 @@
     }
   }
 
-  public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+  public void showInCall(boolean showDialpad, boolean newOutgoingCall) {
     Log.i(this, "Showing InCallActivity");
     mContext.startActivity(
         InCallActivity.getIntent(
-            mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+            mContext, showDialpad, newOutgoingCall, false /* forFullScreen */));
   }
 
   public void onServiceBind() {
@@ -1441,15 +1452,11 @@
     final PhoneAccountHandle accountHandle =
         intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
     final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
-    int videoState =
-        extras.getInt(
-            TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
 
     InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
 
     final Intent activityIntent =
-        InCallActivity.getIntent(
-            mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+        InCallActivity.getIntent(mContext, false, true, false /* forFullScreen */);
     activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
     mContext.startActivity(activityIntent);
   }
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
index 5c5d255..cef1895 100644
--- a/java/com/android/incallui/NotificationBroadcastReceiver.java
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -27,7 +27,6 @@
 import com.android.dialer.logging.nano.DialerImpression;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.VideoUtils;
 
 /**
  * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
@@ -96,7 +95,7 @@
     } else {
       DialerCall call = callList.getVideoUpgradeRequestCall();
       if (call != null) {
-        call.acceptUpgradeRequest(call.getRequestedVideoState());
+        call.getVideoTech().acceptVideoRequest();
       }
     }
   }
@@ -109,7 +108,7 @@
     } else {
       DialerCall call = callList.getVideoUpgradeRequestCall();
       if (call != null) {
-        call.declineUpgradeRequest();
+        call.getVideoTech().declineVideoRequest();
       }
     }
   }
@@ -142,10 +141,7 @@
       if (call != null) {
         call.answer(videoState);
         InCallPresenter.getInstance()
-            .showInCall(
-                false /* showDialpad */,
-                false /* newOutgoingCall */,
-                VideoUtils.isVideoCall(videoState));
+            .showInCall(false /* showDialpad */, false /* newOutgoingCall */);
       }
     }
   }
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
index 9122062..229b58c 100644
--- a/java/com/android/incallui/ProximitySensor.java
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -28,7 +28,7 @@
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
 import com.android.incallui.call.CallList;
-import com.android.incallui.call.VideoUtils;
+import com.android.incallui.call.DialerCall;
 
 /**
  * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
@@ -103,7 +103,8 @@
     boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
     boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
 
-    boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+    DialerCall activeCall = callList.getActiveCall();
+    boolean isVideoCall = activeCall != null && activeCall.isVideoCall();
 
     if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
       mIsPhoneOffhook = isOffhook;
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
index c722675..d6262be 100644
--- a/java/com/android/incallui/StatusBarNotifier.java
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -24,8 +24,10 @@
 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
 
+import android.Manifest;
 import android.app.ActivityManager;
 import android.app.Notification;
+import android.app.Notification.Builder;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -34,6 +36,7 @@
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.net.Uri;
 import android.os.Build.VERSION;
@@ -41,10 +44,13 @@
 import android.support.annotation.ColorRes;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
 import android.support.annotation.StringRes;
 import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
 import android.telecom.Call.Details;
 import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.text.BidiFormatter;
 import android.text.Spannable;
@@ -54,10 +60,13 @@
 import android.text.style.ForegroundColorSpan;
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.util.BitmapUtil;
 import com.android.contacts.common.util.ContactDisplayUtils;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.util.DrawableConverter;
 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
@@ -65,11 +74,13 @@
 import com.android.incallui.async.PausableExecutorImpl;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCallListener;
 import com.android.incallui.ringtone.DialerRingtoneManager;
 import com.android.incallui.ringtone.InCallTonePlayer;
 import com.android.incallui.ringtone.ToneGeneratorFactory;
+import com.android.incallui.videotech.VideoTech;
+import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 
 /** This class adds Notifications to the status bar for the in-call experience. */
@@ -79,9 +90,9 @@
   // Indicates that no notification is currently showing.
   private static final int NOTIFICATION_NONE = 0;
   // Notification for an active call. This is non-interruptive, but cannot be dismissed.
-  private static final int NOTIFICATION_IN_CALL = 1;
+  private static final int NOTIFICATION_IN_CALL = R.id.notification_ongoing_call;
   // Notification for incoming calls. This is interruptive and will show up as a HUN.
-  private static final int NOTIFICATION_INCOMING_CALL = 2;
+  private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call;
 
   private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
   private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
@@ -101,8 +112,9 @@
   private String mSavedContentTitle;
   private Uri mRingtone;
   private StatusBarCallListener mStatusBarCallListener;
+  private boolean mShowFullScreenIntent;
 
-  public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+  StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
     Objects.requireNonNull(context);
     mContext = context;
     mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
@@ -120,9 +132,9 @@
    * notifications.
    */
   static void clearAllCallNotifications(Context backupContext) {
-    Log.i(
-        StatusBarNotifier.class.getSimpleName(),
-        "Something terrible happened. Clear all InCall notifications");
+    LogUtil.i(
+        "StatusBarNotifier.clearAllCallNotifications",
+        "something terrible happened, clear all InCall notifications");
 
     NotificationManager notificationManager =
         backupContext.getSystemService(NotificationManager.class);
@@ -153,10 +165,17 @@
     return PendingIntent.getBroadcast(context, 0, intent, 0);
   }
 
+  private static void setColorized(@NonNull Builder builder) {
+    if (BuildCompat.isAtLeastO()) {
+      builder.setColorized(true);
+    }
+  }
+
   /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
   @Override
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
-    Log.d(this, "onStateChange");
+    LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
     updateNotification(callList);
   }
 
@@ -177,7 +196,8 @@
    *
    * @see #updateInCallNotification(CallList)
    */
-  public void updateNotification(CallList callList) {
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+  void updateNotification(CallList callList) {
     updateInCallNotification(callList);
   }
 
@@ -191,7 +211,7 @@
       setStatusBarCallListener(null);
     }
     if (mCurrentNotification != NOTIFICATION_NONE) {
-      Log.d(this, "cancelInCall()...");
+      LogUtil.d("StatusBarNotifier.cancelNotification", "cancel");
       mNotificationManager.cancel(mCurrentNotification);
     }
     mCurrentNotification = NOTIFICATION_NONE;
@@ -202,8 +222,9 @@
    * status bar notification based on the current telephony state, or cancels the notification if
    * the phone is totally idle.
    */
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void updateInCallNotification(CallList callList) {
-    Log.d(this, "updateInCallNotification...");
+    LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
 
     final DialerCall call = getCallToShow(callList);
 
@@ -214,6 +235,7 @@
     }
   }
 
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void showNotification(final CallList callList, final DialerCall call) {
     final boolean isIncoming =
         (call.getState() == DialerCall.State.INCOMING
@@ -230,6 +252,7 @@
         isIncoming,
         new ContactInfoCacheCallback() {
           @Override
+          @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
           public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
             DialerCall call = callList.getCallById(callId);
             if (call != null) {
@@ -239,6 +262,7 @@
           }
 
           @Override
+          @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
           public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
             DialerCall call = callList.getCallById(callId);
             if (call != null) {
@@ -249,6 +273,7 @@
   }
 
   /** Sets up the main Ui for the notification */
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
   private void buildAndSendNotification(
       CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
     // This can get called to update an existing notification after contact information has come
@@ -268,8 +293,8 @@
     final String contentTitle = getContentTitle(contactInfo, call);
 
     final boolean isVideoUpgradeRequest =
-        call.getSessionModificationState()
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+        call.getVideoTech().getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
     final int notificationType;
     if (callState == DialerCall.State.INCOMING
         || callState == DialerCall.State.CALL_WAITING
@@ -286,7 +311,8 @@
         contentTitle,
         callState,
         notificationType,
-        contactInfo.contactRingtoneUri)) {
+        contactInfo.contactRingtoneUri,
+        InCallPresenter.getInstance().shouldShowFullScreenNotification())) {
       return;
     }
 
@@ -300,9 +326,10 @@
     Notification.Builder publicBuilder = new Notification.Builder(mContext);
     publicBuilder
         .setSmallIcon(iconResId)
-        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+        .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
         // Hide work call state for the lock screen notification
         .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+    setColorized(publicBuilder);
     setNotificationWhen(call, callState, publicBuilder);
 
     // Builder for the notification shown when the device is unlocked or the user has set their
@@ -311,28 +338,26 @@
     builder.setPublicVersion(publicBuilder.build());
 
     // Set up the main intent to send the user to the in-call screen
-    builder.setContentIntent(
-        createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+    builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
 
     // Set the intent as a full screen intent as well if a call is incoming
+    PhoneAccountHandle accountHandle = call.getAccountHandle();
+    if (accountHandle == null) {
+      accountHandle = getAnyPhoneAccount();
+    }
     if (notificationType == NOTIFICATION_INCOMING_CALL) {
-      if (!InCallPresenter.getInstance().isActivityStarted()) {
+      NotificationChannelManager.applyChannel(
+          builder, mContext, Channel.INCOMING_CALL, accountHandle);
+      if (InCallPresenter.getInstance().shouldShowFullScreenNotification()) {
         configureFullScreenIntent(
-            builder,
-            createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
-            callList,
-            call);
-      } else {
-        // If the incall screen is already up, we don't want to show HUN but regular notification
-        // should still be shown. In order to do that the previous one with full screen intent
-        // needs to be cancelled.
-        LogUtil.d(
-            "StatusBarNotifier.buildAndSendNotification",
-            "cancel previous incoming call notification");
-        mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+            builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
       }
-      // Set the notification category for incoming calls
+      // Set the notification category and bump the priority for incoming calls
       builder.setCategory(Notification.CATEGORY_CALL);
+      builder.setPriority(Notification.PRIORITY_MAX);
+    } else {
+      NotificationChannelManager.applyChannel(
+          builder, mContext, Channel.ONGOING_CALL, accountHandle);
     }
 
     // Set the content
@@ -340,7 +365,9 @@
     builder.setSmallIcon(iconResId);
     builder.setContentTitle(contentTitle);
     builder.setLargeIcon(largeIcon);
-    builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+    builder.setColor(
+        mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
+    setColorized(builder);
 
     if (isVideoUpgradeRequest) {
       builder.setUsesChronometer(false);
@@ -367,15 +394,20 @@
       }
     }
     if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
-      Log.v(this, "Playing call waiting tone");
+      LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
       mDialerRingtoneManager.playCallWaitingTone();
     }
     if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
-      Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+      LogUtil.i(
+          "StatusBarNotifier.buildAndSendNotification",
+          "previous notification already showing - cancelling " + mCurrentNotification);
       mNotificationManager.cancel(mCurrentNotification);
     }
 
-    Log.i(this, "Displaying notification for " + notificationType);
+    LogUtil.i(
+        "StatusBarNotifier.buildAndSendNotification",
+        "displaying notification for " + notificationType);
+
     try {
       mNotificationManager.notify(notificationType, notification);
     } catch (RuntimeException e) {
@@ -385,14 +417,32 @@
       activityManager.getMemoryInfo(memoryInfo);
       throw new RuntimeException(
           String.format(
+              Locale.US,
               "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
-              contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+              contactInfo.photoType,
+              memoryInfo.lowMemory,
+              memoryInfo.availMem),
           e);
     }
     call.getLatencyReport().onNotificationShown();
     mCurrentNotification = notificationType;
   }
 
+  @Nullable
+  @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+  private PhoneAccountHandle getAnyPhoneAccount() {
+    PhoneAccountHandle accountHandle;
+    TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
+    accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+    if (accountHandle == null) {
+      List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
+      if (!accountHandles.isEmpty()) {
+        accountHandle = accountHandles.get(0);
+      }
+    }
+    return accountHandle;
+  }
+
   private void createIncomingCallNotification(
       DialerCall call, int state, Notification.Builder builder) {
     setNotificationWhen(call, state, builder);
@@ -438,7 +488,8 @@
       String contentTitle,
       int state,
       int notificationType,
-      Uri ringtone) {
+      Uri ringtone,
+      boolean showFullScreenIntent) {
 
     // The two are different:
     // if new title is not null, it should be different from saved version OR
@@ -454,13 +505,15 @@
             || (mCallState != state)
             || (mSavedLargeIcon != largeIcon)
             || contentTitleChanged
-            || !Objects.equals(mRingtone, ringtone);
+            || !Objects.equals(mRingtone, ringtone)
+            || mShowFullScreenIntent != showFullScreenIntent;
 
     // If we aren't showing a notification right now or the notification type is changing,
     // definitely do an update.
     if (mCurrentNotification != notificationType) {
       if (mCurrentNotification == NOTIFICATION_NONE) {
-        Log.d(this, "Showing notification for first time.");
+        LogUtil.d(
+            "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
       }
       retval = true;
     }
@@ -471,9 +524,11 @@
     mSavedLargeIcon = largeIcon;
     mSavedContentTitle = contentTitle;
     mRingtone = ringtone;
+    mShowFullScreenIntent = showFullScreenIntent;
 
     if (retval) {
-      Log.d(this, "Data changed.  Showing notification");
+      LogUtil.d(
+          "StatusBarNotifier.checkForChangeAndSaveData", "data changed.  Showing notification");
     }
 
     return retval;
@@ -520,8 +575,34 @@
     if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
       largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
     }
+    if (contactInfo.photo == null) {
+      int width =
+          (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+      int height =
+          (int)
+              mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+      int contactType = LetterTileDrawable.TYPE_DEFAULT;
+      LetterTileDrawable lettertile = new LetterTileDrawable(mContext.getResources());
+
+      // TODO: Deduplicate across Dialer. b/36195917
+      if (CallerInfoUtils.isVoiceMailNumber(mContext, call)) {
+        contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+      } else if (contactInfo.isBusiness) {
+        contactType = LetterTileDrawable.TYPE_BUSINESS;
+      } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
+        contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
+      }
+      lettertile.setCanonicalDialerLetterTileDetails(
+          contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
+          contactInfo.lookupKey,
+          LetterTileDrawable.SHAPE_CIRCLE,
+          contactType);
+      largeIcon = lettertile.getBitmap(width, height);
+    }
+
     if (call.isSpam()) {
-      Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+      Drawable drawable =
+          mContext.getResources().getDrawable(R.drawable.blocked_contact, mContext.getTheme());
       largeIcon = DrawableConverter.drawableToBitmap(drawable);
     }
     return largeIcon;
@@ -552,8 +633,8 @@
     // display that regardless of the state of the other calls.
     if (call.getState() == DialerCall.State.ONHOLD) {
       return R.drawable.ic_phone_paused_white_24dp;
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    } else if (call.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       return R.drawable.ic_videocam;
     }
     return R.anim.on_going_call;
@@ -594,8 +675,8 @@
       resId = R.string.notification_on_hold;
     } else if (DialerCall.State.isDialing(call.getState())) {
       resId = R.string.notification_dialing;
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+    } else if (call.getVideoTech().getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       resId = R.string.notification_requesting_video_call;
     }
 
@@ -639,64 +720,98 @@
   }
 
   private void addAnswerAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addAnswerAction",
+        "will show \"answer\" action in the incoming call Notification");
     PendingIntent answerVoicePendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+    // We put animation resources in "anim" folder instead of "drawable", which causes Android
+    // Studio to complain.
+    // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
+    //noinspection ResourceType
     builder.addAction(
-        R.anim.on_going_call,
-        getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
-        answerVoicePendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.anim.on_going_call),
+                getActionText(
+                    R.string.notification_action_answer, R.color.notification_action_accept),
+                answerVoicePendingIntent)
+            .build());
   }
 
   private void addDismissAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addDismissAction",
+        "will show \"decline\" action in the incoming call Notification");
     PendingIntent declinePendingIntent =
         createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
     builder.addAction(
-        R.drawable.ic_close_dk,
-        getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
-        declinePendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_close_dk),
+                getActionText(
+                    R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+                declinePendingIntent)
+            .build());
   }
 
   private void addHangupAction(Notification.Builder builder) {
-    Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+    LogUtil.d(
+        "StatusBarNotifier.addHangupAction",
+        "will show \"hang-up\" action in the ongoing active call Notification");
     PendingIntent hangupPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
     builder.addAction(
-        R.drawable.ic_call_end_white_24dp,
-        getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
-        hangupPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
+                getActionText(
+                    R.string.notification_action_end_call, R.color.notification_action_end_call),
+                hangupPendingIntent)
+            .build());
   }
 
   private void addVideoCallAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"video\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addVideoCallAction",
+        "will show \"video\" action in the incoming call Notification");
     PendingIntent answerVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(
-            R.string.notification_action_answer_video, R.color.notification_action_answer_video),
-        answerVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_answer_video,
+                    R.color.notification_action_answer_video),
+                answerVideoPendingIntent)
+            .build());
   }
 
   private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addAcceptUpgradeRequestAction",
+        "will show \"accept upgrade\" action in the incoming call Notification");
     PendingIntent acceptVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
-        acceptVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_accept, R.color.notification_action_accept),
+                acceptVideoPendingIntent)
+            .build());
   }
 
   private void addDismissUpgradeRequestAction(Notification.Builder builder) {
-    Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+    LogUtil.i(
+        "StatusBarNotifier.addDismissUpgradeRequestAction",
+        "will show \"dismiss upgrade\" action in the incoming call Notification");
     PendingIntent declineVideoPendingIntent =
         createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
     builder.addAction(
-        R.drawable.ic_videocam,
-        getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
-        declineVideoPendingIntent);
+        new Notification.Action.Builder(
+                Icon.createWithResource(mContext, R.drawable.ic_videocam),
+                getActionText(
+                    R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+                declineVideoPendingIntent)
+            .build());
   }
 
   /** Adds fullscreen intent to the builder. */
@@ -707,7 +822,7 @@
     // to the status bar).  Setting fullScreenIntent will cause
     // the InCallScreen to be launched immediately *unless* the
     // current foreground activity is marked as "immersive".
-    Log.d(this, "- Setting fullScreenIntent: " + intent);
+    LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
     builder.setFullScreenIntent(intent, true);
 
     // Ugly hack alert:
@@ -740,7 +855,9 @@
                 && callList.getBackgroundCall() != null));
 
     if (isCallWaiting) {
-      Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+      LogUtil.i(
+          "StatusBarNotifier.configureFullScreenIntent",
+          "updateInCallNotification: call-waiting! force relaunch...");
       // Cancel the IN_CALL_NOTIFICATION immediately before
       // (re)posting it; this seems to force the
       // NotificationManager to launch the fullScreenIntent.
@@ -751,21 +868,15 @@
   private Notification.Builder getNotificationBuilder() {
     final Notification.Builder builder = new Notification.Builder(mContext);
     builder.setOngoing(true);
-
-    // Make the notification prioritized over the other normal notifications.
-    builder.setPriority(Notification.PRIORITY_HIGH);
+    builder.setOnlyAlertOnce(true);
 
     return builder;
   }
 
-  private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+  private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
     Intent intent =
         InCallActivity.getIntent(
-            mContext,
-            false /* showDialpad */,
-            false /* newOutgoingCall */,
-            isVideoCall,
-            isFullScreen);
+            mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
 
     int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
     if (isFullScreen) {
@@ -832,8 +943,9 @@
      * bar notification as required.
      */
     @Override
-    public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
-      if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+    public void onDialerCallSessionModificationStateChange() {
+      if (mDialerCall.getVideoTech().getSessionModificationState()
+          == VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST) {
         cleanup();
         updateNotification(CallList.getInstance());
       }
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index 971b695..20dc987 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -21,7 +21,6 @@
 import android.graphics.Point;
 import android.os.Handler;
 import android.support.annotation.Nullable;
-import android.telecom.Connection;
 import android.telecom.InCallService.VideoCall;
 import android.telecom.VideoProfile;
 import android.telecom.VideoProfile.CameraCapabilities;
@@ -36,17 +35,18 @@
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.CameraDirection;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.InCallVideoCallCallbackNotifier;
 import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
-import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.util.AccessibilityUtil;
 import com.android.incallui.video.protocol.VideoCallScreen;
 import com.android.incallui.video.protocol.VideoCallScreenDelegate;
 import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 import java.util.Objects;
 
 /**
@@ -78,7 +78,6 @@
         InCallStateListener,
         InCallDetailsListener,
         SurfaceChangeListener,
-        VideoEventListener,
         InCallPresenter.InCallEventListener,
         VideoCallScreenDelegate {
 
@@ -90,32 +89,6 @@
   /** The current context. */
   private Context mContext;
 
-  @Override
-  public boolean shouldShowCameraPermissionDialog() {
-    if (mPrimaryCall == null) {
-      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
-      return false;
-    }
-    if (mPrimaryCall.didShowCameraPermission()) {
-      LogUtil.i(
-          "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
-      return false;
-    }
-    if (!ConfigProviderBindings.get(mContext)
-        .getBoolean("camera_permission_dialog_allowed", true)) {
-      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
-      return false;
-    }
-    return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
-  }
-
-  @Override
-  public void onCameraPermissionDialogShown() {
-    if (mPrimaryCall != null) {
-      mPrimaryCall.setDidShowCameraPermission(true);
-    }
-  }
-
   /** The call the video surfaces are currently related to */
   private DialerCall mPrimaryCall;
   /**
@@ -231,49 +204,49 @@
     // this function should never be called with null call object, however if it happens we
     // should handle it gracefully.
     if (call == null) {
-      cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+      cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
       LogUtil.e(
           "VideoCallPresenter.updateCameraSelection",
           "call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
     }
 
     // Clear camera direction if this is not a video call.
-    else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
-      cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
-      call.getVideoSettings().setCameraDir(cameraDir);
+    else if (isAudioCall(call) && !isVideoUpgrade(call)) {
+      cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+      call.setCameraDir(cameraDir);
     }
 
     // If this is a waiting video call, default to active call's camera,
     // since we don't want to change the current camera for waiting call
     // without user's permission.
-    else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
-      cameraDir = activeCall.getVideoSettings().getCameraDir();
+    else if (isVideoCall(activeCall) && isIncomingVideoCall(call)) {
+      cameraDir = activeCall.getCameraDir();
     }
 
     // Infer the camera direction from the video state and store it,
     // if this is an outgoing video call.
-    else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+    else if (isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
       cameraDir = toCameraDirection(call.getVideoState());
-      call.getVideoSettings().setCameraDir(cameraDir);
+      call.setCameraDir(cameraDir);
     }
 
     // Use the stored camera dir if this is an outgoing video call for which camera direction
     // is set.
-    else if (VideoUtils.isOutgoingVideoCall(call)) {
-      cameraDir = call.getVideoSettings().getCameraDir();
+    else if (isOutgoingVideoCall(call)) {
+      cameraDir = call.getCameraDir();
     }
 
     // Infer the camera direction from the video state and store it,
     // if this is an active video call and camera direction is not set.
-    else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+    else if (isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
       cameraDir = toCameraDirection(call.getVideoState());
-      call.getVideoSettings().setCameraDir(cameraDir);
+      call.setCameraDir(cameraDir);
     }
 
     // Use the stored camera dir if this is an active video call for which camera direction
     // is set.
-    else if (VideoUtils.isActiveVideoCall(call)) {
-      cameraDir = call.getVideoSettings().getCameraDir();
+    else if (isActiveVideoCall(call)) {
+      cameraDir = call.getCameraDir();
     }
 
     // For all other cases infer the camera direction but don't store it in the call object.
@@ -289,20 +262,18 @@
     final InCallCameraManager cameraManager =
         InCallPresenter.getInstance().getInCallCameraManager();
     cameraManager.setUseFrontFacingCamera(
-        cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+        cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING);
   }
 
   private static int toCameraDirection(int videoState) {
     return VideoProfile.isTransmissionEnabled(videoState)
             && !VideoProfile.isBidirectional(videoState)
-        ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
-        : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+        ? CameraDirection.CAMERA_DIRECTION_BACK_FACING
+        : CameraDirection.CAMERA_DIRECTION_FRONT_FACING;
   }
 
   private static boolean isCameraDirectionSet(DialerCall call) {
-    return VideoUtils.isVideoCall(call)
-        && call.getVideoSettings().getCameraDir()
-            != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+    return isVideoCall(call) && call.getCameraDir() != CameraDirection.CAMERA_DIRECTION_UNKNOWN;
   }
 
   private static String toSimpleString(DialerCall call) {
@@ -350,7 +321,6 @@
 
     // Register for surface and video events from {@link InCallVideoCallListener}s.
     InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
-    InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
     mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
     mCurrentCallState = DialerCall.State.INVALID;
 
@@ -379,7 +349,6 @@
     InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
 
     InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
-    InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
 
     // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
     // happens after any call state changes but we're unregistering from InCallPresenter above so
@@ -447,7 +416,7 @@
     showVideoUi(
         mPrimaryCall.getVideoState(),
         mPrimaryCall.getState(),
-        mPrimaryCall.getSessionModificationState(),
+        mPrimaryCall.getVideoTech().getSessionModificationState(),
         mPrimaryCall.isRemotelyHeld());
     InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
   }
@@ -521,7 +490,7 @@
       // change the camera or UI unless the waiting VT call becomes active.
       primary = callList.getActiveCall();
       currentCall = callList.getIncomingCall();
-      if (!VideoUtils.isActiveVideoCall(primary)) {
+      if (!isActiveVideoCall(primary)) {
         primary = callList.getIncomingCall();
       }
     } else if (newState == InCallPresenter.InCallState.OUTGOING) {
@@ -564,10 +533,10 @@
     cancelAutoFullScreen();
     if (mPrimaryCall != null) {
       updateFullscreenAndGreenScreenMode(
-          mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+          mPrimaryCall.getState(), mPrimaryCall.getVideoTech().getSessionModificationState());
     } else {
       updateFullscreenAndGreenScreenMode(
-          State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+          State.INVALID, VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
     }
   }
 
@@ -622,7 +591,7 @@
       updateCameraSelection(call);
       String newCameraId = cameraManager.getActiveCameraId();
 
-      if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+      if (!Objects.equals(prevCameraId, newCameraId) && isActiveVideoCall(call)) {
         enableCamera(call.getVideoCall(), true);
       }
     }
@@ -631,7 +600,7 @@
     showVideoUi(
         call.getVideoState(),
         call.getState(),
-        call.getSessionModificationState(),
+        call.getVideoTech().getSessionModificationState(),
         call.isRemotelyHeld());
   }
 
@@ -711,12 +680,13 @@
     checkForVideoStateChange(call);
     checkForCallStateChange(call);
     checkForOrientationAllowedChange(call);
-    updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+    updateFullscreenAndGreenScreenMode(
+        call.getState(), call.getVideoTech().getSessionModificationState());
   }
 
   private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
     InCallPresenter.getInstance()
-        .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+        .setInCallAllowsOrientationChange(isVideoCall(call) || isVideoUpgrade(call));
   }
 
   private void updateFullscreenAndGreenScreenMode(
@@ -775,7 +745,8 @@
   private boolean isCameraRequired() {
     return mPrimaryCall != null
         && isCameraRequired(
-            mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+            mPrimaryCall.getVideoState(),
+            mPrimaryCall.getVideoTech().getSessionModificationState());
   }
 
   /**
@@ -799,7 +770,10 @@
     }
 
     showVideoUi(
-        newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+        newVideoState,
+        call.getState(),
+        call.getVideoTech().getSessionModificationState(),
+        call.isRemotelyHeld());
 
     // Communicate the current camera to telephony and make a request for the camera
     // capabilities.
@@ -814,7 +788,9 @@
       Assert.checkState(
           mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
       videoCall.setDeviceOrientation(mDeviceOrientation);
-      enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+      enableCamera(
+          videoCall,
+          isCameraRequired(newVideoState, call.getVideoTech().getSessionModificationState()));
     }
     int previousVideoState = mCurrentVideoState;
     mCurrentVideoState = newVideoState;
@@ -822,7 +798,7 @@
 
     // adjustVideoMode may be called if we are already in a 1-way video state.  In this case
     // we do not want to trigger auto-fullscreen mode.
-    if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+    if (!isVideoCall(previousVideoState) && isVideoCall(newVideoState)) {
       maybeAutoEnterFullscreen(call);
     }
   }
@@ -832,7 +808,7 @@
       return false;
     }
 
-    if (VideoUtils.isVideoCall(call)) {
+    if (isVideoCall(call)) {
       return true;
     }
 
@@ -877,7 +853,7 @@
     showVideoUi(
         VideoProfile.STATE_AUDIO_ONLY,
         DialerCall.State.ACTIVE,
-        DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+        VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
         false /* isRemotelyHeld */);
     enableCamera(mVideoCall, false);
     InCallPresenter.getInstance().setFullScreen(false);
@@ -918,20 +894,6 @@
   }
 
   /**
-   * Handles peer video pause state changes.
-   *
-   * @param call The call which paused or un-pausedvideo transmission.
-   * @param paused {@code True} when the video transmission is paused, {@code false} when video
-   *     transmission resumes.
-   */
-  @Override
-  public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
-    if (!call.equals(mPrimaryCall)) {
-      return;
-    }
-  }
-
-  /**
    * Handles peer video dimension changes.
    *
    * @param call The call which experienced a peer video dimension change.
@@ -959,17 +921,6 @@
   }
 
   /**
-   * Handles any video quality changes in the call.
-   *
-   * @param call The call which experienced a video quality change.
-   * @param videoQuality The new video call quality.
-   */
-  @Override
-  public void onVideoQualityChanged(DialerCall call, int videoQuality) {
-    // No-op
-  }
-
-  /**
    * Handles a change to the dimensions of the local camera. Receiving the camera capabilities
    * triggers the creation of the video
    *
@@ -1024,42 +975,6 @@
   }
 
   /**
-   * Called when call session event is raised.
-   *
-   * @param event The call session event.
-   */
-  @Override
-  public void onCallSessionEvent(int event) {
-    switch (event) {
-      case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
-        break;
-      case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
-        break;
-      default:
-        LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
-        break;
-    }
-  }
-
-  /**
-   * Handles a change to the call data usage
-   *
-   * @param dataUsage call data usage value
-   */
-  @Override
-  public void onCallDataUsageChange(long dataUsage) {
-    LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
-  }
-
-  /**
    * Handles changes to the device orientation.
    *
    * @param orientation The screen orientation of the device (one of: {@link
@@ -1106,7 +1021,7 @@
       return;
     }
 
-    if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+    if (!isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
       LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
       InCallPresenter.getInstance().setFullScreen(false);
     }
@@ -1126,7 +1041,7 @@
 
     if (call == null
         || call.getState() != DialerCall.State.ACTIVE
-        || !VideoUtils.isBidirectionalVideoCall(call)
+        || !isBidirectionalVideoCall(call)
         || InCallPresenter.getInstance().isFullscreen()
         || (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
       // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
@@ -1156,6 +1071,32 @@
     mHandler.removeCallbacks(mAutoFullscreenRunnable);
   }
 
+  @Override
+  public boolean shouldShowCameraPermissionDialog() {
+    if (mPrimaryCall == null) {
+      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+      return false;
+    }
+    if (mPrimaryCall.didShowCameraPermission()) {
+      LogUtil.i(
+          "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+      return false;
+    }
+    if (!ConfigProviderBindings.get(mContext)
+        .getBoolean("camera_permission_dialog_allowed", true)) {
+      LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+      return false;
+    }
+    return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+  }
+
+  @Override
+  public void onCameraPermissionDialogShown() {
+    if (mPrimaryCall != null) {
+      mPrimaryCall.setDidShowCameraPermission(true);
+    }
+  }
+
   private void updateRemoteVideoSurfaceDimensions() {
     Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
     if (activity != null) {
@@ -1166,8 +1107,8 @@
   }
 
   private static boolean isVideoUpgrade(DialerCall call) {
-    return VideoUtils.hasSentVideoUpgradeRequest(call)
-        || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+    return call != null
+        && (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest());
   }
 
   private static boolean isVideoUpgrade(@SessionModificationState int state) {
@@ -1286,4 +1227,48 @@
     /** The surface has been set on the {@link VideoCall}. */
     private static final int SURFACE_SET = 3;
   }
+
+  private static boolean isBidirectionalVideoCall(DialerCall call) {
+    return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+  }
+
+  private static boolean isIncomingVideoCall(DialerCall call) {
+    if (!isVideoCall(call)) {
+      return false;
+    }
+    final int state = call.getState();
+    return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+  }
+
+  private static boolean isActiveVideoCall(DialerCall call) {
+    return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+  }
+
+  private static boolean isOutgoingVideoCall(DialerCall call) {
+    if (!isVideoCall(call)) {
+      return false;
+    }
+    final int state = call.getState();
+    return DialerCall.State.isDialing(state)
+        || state == DialerCall.State.CONNECTING
+        || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+  }
+
+  private static boolean isAudioCall(DialerCall call) {
+    if (!CompatUtils.isVideoCompatible()) {
+      return true;
+    }
+
+    return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+  }
+
+  private static boolean isVideoCall(@Nullable DialerCall call) {
+    return call != null && call.isVideoCall();
+  }
+
+  private static boolean isVideoCall(int videoState) {
+    return CompatUtils.isVideoCompatible()
+        && (VideoProfile.isTransmissionEnabled(videoState)
+            || VideoProfile.isReceptionEnabled(videoState));
+  }
 }
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
index 2b43577..2595e2f 100644
--- a/java/com/android/incallui/VideoPauseController.java
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -17,14 +17,14 @@
 package com.android.incallui;
 
 import android.support.annotation.NonNull;
-import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
 import com.android.incallui.InCallPresenter.IncomingCallListener;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
-import com.android.incallui.call.VideoUtils;
 import java.util.Objects;
 
 /**
@@ -32,12 +32,21 @@
  * to the background and subsequently brought back to the foreground.
  */
 class VideoPauseController implements InCallStateListener, IncomingCallListener {
-
-  private static final String TAG = "VideoPauseController";
   private static VideoPauseController sVideoPauseController;
   private InCallPresenter mInCallPresenter;
-  /** The current call context, if applicable. */
-  private CallContext mPrimaryCallContext = null;
+
+  /** The current call, if applicable. */
+  private DialerCall mPrimaryCall = null;
+
+  /**
+   * The cached state of primary call, updated after onStateChange has processed.
+   *
+   * <p>These values are stored to detect specific changes in state between onStateChange calls.
+   */
+  private int mPrevCallState = State.INVALID;
+
+  private boolean mWasVideoCall = false;
+
   /**
    * Tracks whether the application is in the background. {@code True} if the application is in the
    * background, {@code false} otherwise.
@@ -57,51 +66,9 @@
     return sVideoPauseController;
   }
 
-  /**
-   * Determines if a given call is the same one stored in a {@link CallContext}.
-   *
-   * @param call The call.
-   * @param callContext The call context.
-   * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
-   *     CallContext}.
-   */
-  private static boolean areSame(DialerCall call, CallContext callContext) {
-    if (call == null && callContext == null) {
-      return true;
-    } else if (call == null || callContext == null) {
-      return false;
-    }
-    return call.equals(callContext.getCall());
-  }
-
-  /**
-   * Determines if a video call can be paused. Only a video call which is active can be paused.
-   *
-   * @param callContext The call context to check.
-   * @return {@code true} if the call is an active video call.
-   */
-  private static boolean canVideoPause(CallContext callContext) {
-    return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
-  }
-
-  /**
-   * Determines if a call referenced by a {@link CallContext} is a video call.
-   *
-   * @param callContext The call context.
-   * @return {@code true} if the call is a video call, {@code false} otherwise.
-   */
-  private static boolean isVideoCall(CallContext callContext) {
-    return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
-  }
-
-  /**
-   * Determines if call is in incoming/waiting state.
-   *
-   * @param call The call context.
-   * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
-   */
-  private static boolean isIncomingCall(CallContext call) {
-    return call != null && isIncomingCall(call.getCall());
+  private boolean wasIncomingCall() {
+    return (mPrevCallState == DialerCall.State.CALL_WAITING
+        || mPrevCallState == DialerCall.State.INCOMING);
   }
 
   /**
@@ -119,11 +86,10 @@
   /**
    * Determines if a call is dialing.
    *
-   * @param call The call context.
    * @return {@code true} if the call is dialing, {@code false} otherwise.
    */
-  private static boolean isDialing(CallContext call) {
-    return call != null && DialerCall.State.isDialing(call.getState());
+  private boolean wasDialing() {
+    return DialerCall.State.isDialing(mPrevCallState);
   }
 
   /**
@@ -133,8 +99,8 @@
    * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
    */
   public void setUp(@NonNull InCallPresenter inCallPresenter) {
-    log("setUp");
-    mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+    LogUtil.enterBlock("VideoPauseController.setUp");
+    mInCallPresenter = Assert.isNotNull(inCallPresenter);
     mInCallPresenter.addListener(this);
     mInCallPresenter.addIncomingCallListener(this);
   }
@@ -144,7 +110,7 @@
    * state. Called from {@link com.android.incallui.InCallPresenter}.
    */
   public void tearDown() {
-    log("tearDown...");
+    LogUtil.enterBlock("VideoPauseController.tearDown");
     mInCallPresenter.removeListener(this);
     mInCallPresenter.removeIncomingCallListener(this);
     clear();
@@ -153,7 +119,9 @@
   /** Clears the internal state for the {@link VideoPauseController}. */
   private void clear() {
     mInCallPresenter = null;
-    mPrimaryCallContext = null;
+    mPrimaryCall = null;
+    mPrevCallState = State.INVALID;
+    mWasVideoCall = false;
     mIsInBackground = false;
   }
 
@@ -167,8 +135,6 @@
    */
   @Override
   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
-    log("onStateChange, OldState=" + oldState + " NewState=" + newState);
-
     DialerCall call;
     if (newState == InCallState.INCOMING) {
       call = callList.getIncomingCall();
@@ -182,22 +148,26 @@
       call = callList.getActiveCall();
     }
 
-    boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
-    boolean canVideoPause = VideoUtils.canVideoPause(call);
-    log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
-    log("onStateChange, canVideoPause=" + canVideoPause);
-    log("onStateChange, IsInBackground=" + mIsInBackground);
+    boolean hasPrimaryCallChanged = !Objects.equals(call, mPrimaryCall);
+    boolean canVideoPause = videoCanPause(call);
+
+    LogUtil.i(
+        "VideoPauseController.onStateChange",
+        "hasPrimaryCallChanged: %b, videoCanPause: %b, isInBackground: %b",
+        hasPrimaryCallChanged,
+        canVideoPause,
+        mIsInBackground);
 
     if (hasPrimaryCallChanged) {
       onPrimaryCallChanged(call);
       return;
     }
 
-    if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+    if (wasDialing() && canVideoPause && mIsInBackground) {
       // Bring UI to foreground if outgoing request becomes active while UI is in
       // background.
       bringToForeground();
-    } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+    } else if (!mWasVideoCall && canVideoPause && mIsInBackground) {
       // Bring UI to foreground if VoLTE call becomes active while UI is in
       // background.
       bringToForeground();
@@ -216,27 +186,26 @@
    * @param call The new primary call.
    */
   private void onPrimaryCallChanged(DialerCall call) {
-    log("onPrimaryCallChanged: New call = " + call);
-    log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
-    log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
+    LogUtil.i(
+        "VideoPauseController.onPrimaryCallChanged",
+        "new call: %s, old call: %s, mIsInBackground: %b",
+        call,
+        mPrimaryCall,
+        mIsInBackground);
 
-    if (areSame(call, mPrimaryCallContext)) {
+    if (Objects.equals(call, mPrimaryCall)) {
       throw new IllegalStateException();
     }
-    final boolean canVideoPause = VideoUtils.canVideoPause(call);
+    final boolean canVideoPause = videoCanPause(call);
 
-    if ((isIncomingCall(mPrimaryCallContext)
-            || isDialing(mPrimaryCallContext)
-            || (call != null && VideoProfile.isPaused(call.getVideoState())))
-        && canVideoPause
-        && !mIsInBackground) {
+    if ((wasIncomingCall() || wasDialing()) && canVideoPause && !mIsInBackground) {
       // Send resume request for the active call, if user rejects incoming call, ends dialing
       // call, or the call was previously in a paused state and UI is in the foreground.
       sendRequest(call, true);
-    } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+    } else if (isIncomingCall(call) && videoCanPause(mPrimaryCall)) {
       // Send pause request if there is an active video call, and we just received a new
       // incoming call.
-      sendRequest(mPrimaryCallContext.getCall(), false);
+      sendRequest(mPrimaryCall, false);
     }
 
     updatePrimaryCallContext(call);
@@ -251,9 +220,14 @@
    */
   @Override
   public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
-    log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
+    LogUtil.i(
+        "VideoPauseController.onIncomingCall",
+        "oldState: %s, newState: %s, call: %s",
+        oldState,
+        newState,
+        call);
 
-    if (areSame(call, mPrimaryCallContext)) {
+    if (Objects.equals(call, mPrimaryCall)) {
       return;
     }
 
@@ -267,11 +241,13 @@
    */
   private void updatePrimaryCallContext(DialerCall call) {
     if (call == null) {
-      mPrimaryCallContext = null;
-    } else if (mPrimaryCallContext != null) {
-      mPrimaryCallContext.update(call);
+      mPrimaryCall = null;
+      mPrevCallState = State.INVALID;
+      mWasVideoCall = false;
     } else {
-      mPrimaryCallContext = new CallContext(call);
+      mPrimaryCall = call;
+      mPrevCallState = call.getState();
+      mWasVideoCall = call.isVideoCall();
     }
   }
 
@@ -301,13 +277,9 @@
    *     video provider if we are in a call.
    */
   private void onResume(boolean isInCall) {
-    log("onResume");
-
     mIsInBackground = false;
-    if (canVideoPause(mPrimaryCallContext) && isInCall) {
-      sendRequest(mPrimaryCallContext.getCall(), true);
-    } else {
-      log("onResume. Ignoring...");
+    if (isInCall) {
+      sendRequest(mPrimaryCall, true);
     }
   }
 
@@ -319,22 +291,20 @@
    *     video provider if we are in a call.
    */
   private void onPause(boolean isInCall) {
-    log("onPause");
-
     mIsInBackground = true;
-    if (canVideoPause(mPrimaryCallContext) && isInCall) {
-      sendRequest(mPrimaryCallContext.getCall(), false);
-    } else {
-      log("onPause, Ignoring...");
+    if (isInCall) {
+      sendRequest(mPrimaryCall, false);
     }
   }
 
   private void bringToForeground() {
+    LogUtil.enterBlock("VideoPauseController.bringToForeground");
     if (mInCallPresenter != null) {
-      log("Bringing UI to foreground");
       mInCallPresenter.bringToForeground(false);
     } else {
-      loge("InCallPresenter is null. Cannot bring UI to foreground");
+      LogUtil.e(
+          "VideoPauseController.bringToForeground",
+          "InCallPresenter is null. Cannot bring UI to foreground");
     }
   }
 
@@ -345,72 +315,18 @@
    * @param resume If true resume request will be sent, otherwise pause request.
    */
   private void sendRequest(DialerCall call, boolean resume) {
-    // Check if this call supports pause/un-pause.
-    if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+    if (call == null) {
       return;
     }
 
     if (resume) {
-      log("sending resume request, call=" + call);
-      call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+      call.getVideoTech().unpause();
     } else {
-      log("sending pause request, call=" + call);
-      call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+      call.getVideoTech().pause();
     }
   }
 
-  /**
-   * Logs a debug message.
-   *
-   * @param msg The message.
-   */
-  private void log(String msg) {
-    Log.d(this, TAG + msg);
-  }
-
-  /**
-   * Logs an error message.
-   *
-   * @param msg The message.
-   */
-  private void loge(String msg) {
-    Log.e(this, TAG + msg);
-  }
-
-  /** Keeps track of the current active/foreground call. */
-  private static class CallContext {
-
-    private int mState = State.INVALID;
-    private int mVideoState;
-    private DialerCall mCall;
-
-    public CallContext(@NonNull DialerCall call) {
-      Objects.requireNonNull(call);
-      update(call);
-    }
-
-    public void update(@NonNull DialerCall call) {
-      mCall = Objects.requireNonNull(call);
-      mState = call.getState();
-      mVideoState = call.getVideoState();
-    }
-
-    public int getState() {
-      return mState;
-    }
-
-    public int getVideoState() {
-      return mVideoState;
-    }
-
-    @Override
-    public String toString() {
-      return String.format(
-          "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
-    }
-
-    public DialerCall getCall() {
-      return mCall;
-    }
+  private static boolean videoCanPause(DialerCall call) {
+    return call != null && call.isVideoCall() && call.getState() == DialerCall.State.ACTIVE;
   }
 }
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
index f7a7a0a..442e207 100644
--- a/java/com/android/incallui/answer/bindings/AnswerBindings.java
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.java
@@ -23,7 +23,7 @@
 public class AnswerBindings {
 
   public static AnswerScreen createAnswerScreen(
-      String callId, int videoState, boolean isVideoUpgradeRequest) {
-    return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+      String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
+    return AnswerFragment.newInstance(callId, isVideoCall, isVideoUpgradeRequest);
   }
 }
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
index 98439ee..6874dae 100644
--- a/java/com/android/incallui/answer/impl/AnswerFragment.java
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -37,7 +37,6 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.transition.TransitionManager;
 import android.support.v4.app.Fragment;
-import android.telecom.VideoProfile;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -79,10 +78,11 @@
 import com.android.incallui.incall.protocol.PrimaryCallState;
 import com.android.incallui.incall.protocol.PrimaryInfo;
 import com.android.incallui.incall.protocol.SecondaryInfo;
-import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.MapsComponent;
 import com.android.incallui.sessiondata.AvatarPresenter;
 import com.android.incallui.sessiondata.MultimediaFragment;
 import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -101,7 +101,7 @@
   static final String ARG_CALL_ID = "call_id";
 
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String ARG_VIDEO_STATE = "video_state";
+  static final String ARG_IS_VIDEO_CALL = "is_video_call";
 
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
   static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
@@ -143,7 +143,7 @@
   private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
   private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
   private ContactGridManager contactGridManager;
-  private AnswerVideoCallScreen answerVideoCallScreen;
+  private VideoCallScreen answerVideoCallScreen;
   private Handler handler = new Handler(Looper.getMainLooper());
 
   private enum SecondaryBehavior {
@@ -288,10 +288,10 @@
   }
 
   public static AnswerFragment newInstance(
-      String callId, int videoState, boolean isVideoUpgradeRequest) {
+      String callId, boolean isVideoCall, boolean isVideoUpgradeRequest) {
     Bundle bundle = new Bundle();
     bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
-    bundle.putInt(ARG_VIDEO_STATE, videoState);
+    bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
     bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
 
     AnswerFragment instance = new AnswerFragment();
@@ -306,18 +306,13 @@
   }
 
   @Override
-  public int getVideoState() {
-    return getArguments().getInt(ARG_VIDEO_STATE);
-  }
-
-  @Override
   public boolean isVideoUpgradeRequest() {
     return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
   }
 
   @Override
   public void setTextResponses(List<String> textResponses) {
-    if (isVideoCall()) {
+    if (isVideoCall() || isVideoUpgradeRequest()) {
       LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
     } else if (textResponses == null) {
       LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
@@ -336,7 +331,9 @@
 
   private void initSecondaryButton() {
     secondaryBehavior =
-        isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+        isVideoCall() || isVideoUpgradeRequest()
+            ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO
+            : SecondaryBehavior.REJECT_WITH_SMS;
     secondaryBehavior.applyToView(secondaryButton);
 
     secondaryButton.setOnClickListener(
@@ -351,12 +348,9 @@
     secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
 
     if (isVideoCall()) {
-      //noinspection WrongConstant
-      if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
-        secondaryButton.setVisibility(View.VISIBLE);
-      } else {
-        secondaryButton.setVisibility(View.INVISIBLE);
-      }
+      secondaryButton.setVisibility(View.VISIBLE);
+    } else {
+      secondaryButton.setVisibility(View.INVISIBLE);
     }
   }
 
@@ -448,11 +442,11 @@
 
     MultimediaData multimediaData = getSessionData();
     if (multimediaData != null
-        && (!TextUtils.isEmpty(multimediaData.getSubject())
+        && (!TextUtils.isEmpty(multimediaData.getText())
             || (multimediaData.getImageUri() != null)
             || (multimediaData.getLocation() != null && canShowMap()))) {
       // Need message fragment
-      String subject = multimediaData.getSubject();
+      String subject = multimediaData.getText();
       Uri imageUri = multimediaData.getImageUri();
       Location location = multimediaData.getLocation();
       if (!(current instanceof MultimediaFragment)
@@ -487,11 +481,11 @@
   }
 
   private boolean shouldShowAvatar() {
-    return !isVideoCall();
+    return !isVideoCall() && !isVideoUpgradeRequest();
   }
 
   private boolean canShowMap() {
-    return StaticMapBinding.get(getActivity().getApplication()) != null;
+    return MapsComponent.get(getContext()).getMaps().isAvailable();
   }
 
   @Override
@@ -564,7 +558,7 @@
       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
     Bundle arguments = getArguments();
     Assert.checkState(arguments.containsKey(ARG_CALL_ID));
-    Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+    Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL));
     Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
 
     buttonAcceptClicked = false;
@@ -596,7 +590,6 @@
             });
     updateImportanceBadgeVisibility();
 
-    boolean isVideoCall = isVideoCall();
     contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
 
     Fragment answerMethod =
@@ -625,9 +618,9 @@
       flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
     }
     view.setSystemUiVisibility(flags);
-    if (isVideoCall) {
+    if (isVideoCall() || isVideoUpgradeRequest()) {
       if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
-        answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+        answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view);
       } else {
         view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
       }
@@ -649,7 +642,7 @@
     updateUI();
 
     if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
-      ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+      ViewUtil.doOnGlobalLayout(view, this::animateEntry);
     }
   }
 
@@ -667,7 +660,7 @@
 
     updateUI();
     if (answerVideoCallScreen != null) {
-      answerVideoCallScreen.onStart();
+      answerVideoCallScreen.onVideoScreenStart();
     }
   }
 
@@ -678,7 +671,7 @@
 
     handler.removeCallbacks(swipeHintRestoreTimer);
     if (answerVideoCallScreen != null) {
-      answerVideoCallScreen.onStop();
+      answerVideoCallScreen.onVideoScreenStop();
     }
   }
 
@@ -722,7 +715,7 @@
 
   @Override
   public boolean isVideoCall() {
-    return VideoUtils.isVideoCall(getVideoState());
+    return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
   }
 
   @Override
@@ -775,14 +768,12 @@
     Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
 
     AnimatorSet animatorSet = new AnimatorSet();
-    animatorSet
-        .play(alpha)
-        .with(topRow)
-        .with(contactName)
-        .with(bottomRow)
-        .with(important)
-        .with(dataContainer);
-    animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+    AnimatorSet.Builder builder = animatorSet.play(alpha);
+    builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer);
+    if (isShowingLocationUi()) {
+      builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder)));
+    }
+    animatorSet.setDuration(getResources().getInteger(R.integer.answer_animate_entry_millis));
     animatorSet.addListener(
         new AnimatorListenerAdapter() {
           @Override
@@ -803,14 +794,7 @@
   private void acceptCallByUser(boolean answerVideoAsAudio) {
     LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
     if (!buttonAcceptClicked) {
-      int desiredVideoState = getVideoState();
-      if (answerVideoAsAudio) {
-        desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
-      }
-
-      // Notify the lower layer first to start signaling ASAP.
-      answerScreenDelegate.onAnswer(desiredVideoState);
-
+      answerScreenDelegate.onAnswer(answerVideoAsAudio);
       buttonAcceptClicked = true;
     }
   }
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
index 0316a5f..06502da 100644
--- a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -32,12 +32,15 @@
 
 /** Shows a video preview for an incoming call. */
 public class AnswerVideoCallScreen implements VideoCallScreen {
+  @NonNull private final String callId;
   @NonNull private final Fragment fragment;
   @NonNull private final TextureView textureView;
   @NonNull private final VideoCallScreenDelegate delegate;
 
-  public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
-    this.fragment = fragment;
+  public AnswerVideoCallScreen(
+      @NonNull String callId, @NonNull Fragment fragment, @NonNull View view) {
+    this.callId = Assert.isNotNull(callId);
+    this.fragment = Assert.isNotNull(fragment);
 
     textureView =
         Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
@@ -53,13 +56,15 @@
     overlayView.setVisibility(View.VISIBLE);
   }
 
-  public void onStart() {
+  @Override
+  public void onVideoScreenStart() {
     LogUtil.i("AnswerVideoCallScreen.onStart", null);
     delegate.onVideoCallScreenUiReady();
     delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
   }
 
-  public void onStop() {
+  @Override
+  public void onVideoScreenStop() {
     LogUtil.i("AnswerVideoCallScreen.onStop", null);
     delegate.onVideoCallScreenUiUnready();
   }
@@ -98,6 +103,12 @@
     return fragment;
   }
 
+  @NonNull
+  @Override
+  public String getCallId() {
+    return callId;
+  }
+
   private void updatePreviewVideoScaling() {
     if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
       LogUtil.i(
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
index 62845b7..ff20d3a 100644
--- a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -190,7 +190,7 @@
 
       case MotionEvent.ACTION_UP:
         isUp = true;
-        //fallthrough_intended
+        // fall through
       case MotionEvent.ACTION_CANCEL:
         boolean hintOnTheRight = targetedView == rightIcon;
         trackMovement(event);
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
index 4052281..afa194f 100644
--- a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
@@ -44,4 +44,6 @@
    * @return true iff the current call is a video call.
    */
   boolean isVideoCall();
+
+  boolean isVideoUpgradeRequest();
 }
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
index 0bc6581..6e8e1f7 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -60,7 +60,7 @@
 import com.android.incallui.answer.impl.classifier.FalsingManager;
 import com.android.incallui.answer.impl.hint.AnswerHint;
 import com.android.incallui.answer.impl.hint.AnswerHintFactory;
-import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import com.android.incallui.answer.impl.hint.PawImageLoaderImpl;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -228,7 +228,7 @@
     touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
 
     answerHint =
-        new AnswerHintFactory(new EventPayloadLoaderImpl())
+        new AnswerHintFactory(new PawImageLoaderImpl())
             .create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
     answerHint.onCreateView(
         layoutInflater,
@@ -319,7 +319,7 @@
     if (contactPuckIcon == null) {
       return;
     }
-    if (getParent().isVideoCall()) {
+    if (getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
       contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
     } else {
       contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
@@ -348,7 +348,8 @@
   }
 
   private boolean shouldShowPhotoInPuck() {
-    return getParent().isVideoCall() && contactPhoto != null;
+    return (getParent().isVideoCall() || getParent().isVideoUpgradeRequest())
+        && contactPhoto != null;
   }
 
   @Override
@@ -387,6 +388,10 @@
     // Since the animation progression is controlled by user gesture instead of real timeline, the
     // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
     // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+    //
+    // See specs -
+    // Accept: https://direct.googleplex.com/#/spec/8510001
+    // Decline: https://direct.googleplex.com/#/spec/3850001
     final float progressSlots = 9;
 
     // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
@@ -414,7 +419,7 @@
     contactPuckBackground.setColorFilter(destPuckColor);
 
     // Animate decline icon
-    if (isAcceptingFlow || getParent().isVideoCall()) {
+    if (isAcceptingFlow || getParent().isVideoCall() || getParent().isVideoUpgradeRequest()) {
       rotateToward(contactPuckIcon, 0f);
     } else {
       rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
index b5fa6da..dfbba1c 100644
--- a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -3,7 +3,7 @@
   xmlns:android="http://schemas.android.com/apk/res/android">
 
   <application>
-    <receiver android:name=".EventSecretCodeListener">
+    <receiver android:name=".PawSecretCodeListener">
       <intent-filter>
         <action android:name="android.provider.Telephony.SECRET_CODE" />
         <data android:scheme="android_secret_code" />
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
index 45395a7..eaf5b74 100644
--- a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -28,7 +28,6 @@
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.util.AccessibilityUtil;
-import java.util.Calendar;
 
 /**
  * Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
@@ -51,10 +50,10 @@
   @VisibleForTesting
   static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
 
-  private final EventPayloadLoader eventPayloadLoader;
+  private final PawImageLoader pawImageLoader;
 
-  public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
-    this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+  public AnswerHintFactory(@NonNull PawImageLoader pawImageLoader) {
+    this.pawImageLoader = Assert.isNotNull(pawImageLoader);
   }
 
   @NonNull
@@ -69,11 +68,9 @@
     }
 
     // Display the event answer hint if the payload is available.
-    Drawable eventPayload =
-        eventPayloadLoader.loadPayload(
-            context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+    Drawable eventPayload = pawImageLoader.loadPayload(context);
     if (eventPayload != null) {
-      return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+      return new PawAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
     }
 
     return new EmptyAnswerHint();
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
deleted file mode 100644
index bd8d736..0000000
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.answer.impl.hint;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build.VERSION_CODES;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import com.android.dialer.common.Assert;
-import com.android.dialer.common.ConfigProvider;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import java.io.InputStream;
-import java.util.TimeZone;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
-@TargetApi(VERSION_CODES.M)
-public final class EventPayloadLoaderImpl implements EventPayloadLoader {
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_KEY = "event_key";
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_BINARY = "event_binary";
-
-  // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
-  // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
-
-  @Override
-  @Nullable
-  public Drawable loadPayload(
-      @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
-    Assert.isNotNull(context);
-    Assert.isNotNull(timeZone);
-    ConfigProvider configProvider = ConfigProviderBindings.get(context);
-
-    String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
-    if (pbeKey == null) {
-      return null;
-    }
-    long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
-    long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
-
-    String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
-    if (eventBinary == null) {
-      return null;
-    }
-
-    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-    if (!preferences.getBoolean(
-        EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
-      long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
-
-      if (localTimestamp < timeRangeStart) {
-        return null;
-      }
-
-      if (localTimestamp > timeRangeEnd) {
-        return null;
-      }
-    }
-
-    // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
-    try (InputStream input = context.getAssets().open(eventBinary)) {
-      byte[] encryptedFile = new byte[input.available()];
-      input.read(encryptedFile);
-
-      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
-
-      byte[] salt = new byte[8];
-      System.arraycopy(encryptedFile, 8, salt, 0, 8);
-      SecretKey key =
-          SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
-              .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
-      cipher.init(Cipher.DECRYPT_MODE, key);
-
-      byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
-
-      return new BitmapDrawable(
-          context.getResources(),
-          BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
-    } catch (Exception e) {
-      // Avoid crashing dialer for any reason.
-      LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
-      return null;
-    }
-  }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
deleted file mode 100644
index 7cf4054..0000000
--- a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
+++ /dev/null
@@ -1,67 +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.incallui.answer.impl.hint;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.widget.Toast;
-import com.android.dialer.common.ConfigProviderBindings;
-import com.android.dialer.common.LogUtil;
-import com.android.dialer.logging.Logger;
-import com.android.dialer.logging.nano.DialerImpression;
-
-/**
- * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
- */
-public class EventSecretCodeListener extends BroadcastReceiver {
-
-  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-  static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
-
-  public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
-
-  @Override
-  public void onReceive(Context context, Intent intent) {
-    String host = intent.getData().getHost();
-    String secretCode =
-        ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
-    if (secretCode == null) {
-      return;
-    }
-    if (!TextUtils.equals(secretCode, host)) {
-      return;
-    }
-    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-    boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
-    if (wasEnabled) {
-      preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
-      Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
-      Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
-      LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
-    } else {
-      preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
-      Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
-      Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
-      LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
-    }
-  }
-}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
similarity index 95%
rename from java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
rename to java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
index 7ee327d..36b761f 100644
--- a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
+++ b/java/com/android/incallui/answer/impl/hint/PawAnswerHint.java
@@ -39,7 +39,7 @@
  * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
  * DotAnswerHint}.
  */
-public final class EventAnswerHint implements AnswerHint {
+public final class PawAnswerHint implements AnswerHint {
 
   private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
   private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
@@ -53,7 +53,8 @@
   private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
   private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
 
-  private static final float FADE_SCALE = 1.2f;
+  private static final float IMAGE_SCALE = 1.5f;
+  private static final float FADE_SCALE = 2.0f;
 
   private final Context context;
   private final Drawable payload;
@@ -65,7 +66,7 @@
   private View answerHintContainer;
   private AnimatorSet answerGestureHintAnim;
 
-  public EventAnswerHint(
+  public PawAnswerHint(
       @NonNull Context context,
       @NonNull Drawable payload,
       long puckUpDurationMillis,
@@ -80,9 +81,9 @@
   public void onCreateView(
       LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
     this.puck = puck;
-    View view = inflater.inflate(R.layout.event_hint, container, true);
+    View view = inflater.inflate(R.layout.paw_hint, container, true);
     answerHintContainer = view.findViewById(R.id.answer_hint_container);
-    payloadView = view.findViewById(R.id.payload);
+    payloadView = view.findViewById(R.id.paw_image);
     hintText.setTextSize(
         TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
     ((ImageView) payloadView).setImageDrawable(payload);
@@ -143,7 +144,7 @@
         createUniformScaleAnimator(
             target,
             FADE_SCALE,
-            1.0f,
+            IMAGE_SCALE,
             FADE_IN_DURATION_SCALE_MILLIS,
             FADE_IN_DELAY_SCALE_MILLIS,
             new LinearInterpolator());
@@ -170,7 +171,7 @@
     Animator scale =
         createUniformScaleAnimator(
             target,
-            1.0f,
+            IMAGE_SCALE,
             FADE_SCALE,
             FADE_OUT_DURATION_SCALE_MILLIS,
             scaleDelay,
@@ -178,7 +179,7 @@
     Animator alpha =
         createAlphaAnimator(
             target,
-            01.0f,
+            1.0f,
             0.0f,
             FADE_OUT_DURATION_ALPHA_MILLIS,
             FADE_OUT_DELAY_ALPHA_MILLIS,
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
similarity index 75%
rename from java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
rename to java/com/android/incallui/answer/impl/hint/PawImageLoader.java
index 09e3bed..09e700f 100644
--- a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoader.java
@@ -20,11 +20,9 @@
 import android.graphics.drawable.Drawable;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import java.util.TimeZone;
 
-/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
-public interface EventPayloadLoader {
+/** Loads a {@link Drawable} payload for the {@link PawAnswerHint} if it should be displayed. */
+public interface PawImageLoader {
   @Nullable
-  Drawable loadPayload(
-      @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+  Drawable loadPayload(@NonNull Context context);
 }
diff --git a/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
new file mode 100644
index 0000000..485a9ae
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawImageLoaderImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class PawImageLoaderImpl implements PawImageLoader {
+
+  @Override
+  @Nullable
+  public Drawable loadPayload(@NonNull Context context) {
+    Assert.isNotNull(context);
+
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+    if (!preferences.getBoolean(PawSecretCodeListener.PAW_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+      return null;
+    }
+    int drawableId = preferences.getInt(PawSecretCodeListener.PAW_DRAWABLE_ID_KEY, 0);
+    if (drawableId == 0) {
+      return null;
+    }
+    return context.getDrawable(drawableId);
+  }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
new file mode 100644
index 0000000..b4fc19c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/PawSecretCodeListener.java
@@ -0,0 +1,81 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression.Type;
+import java.util.Random;
+
+/**
+ * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
+ */
+public class PawSecretCodeListener extends BroadcastReceiver {
+
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String CONFIG_PAW_SECRET_CODE = "paw_secret_code";
+
+  public static final String PAW_ENABLED_WITH_SECRET_CODE_KEY = "paw_enabled_with_secret_code";
+  public static final String PAW_DRAWABLE_ID_KEY = "paw_drawable_id";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    String host = intent.getData().getHost();
+    Assert.checkState(!TextUtils.isEmpty(host));
+    String secretCode =
+        ConfigProviderBindings.get(context).getString(CONFIG_PAW_SECRET_CODE, "729");
+    if (secretCode == null) {
+      return;
+    }
+    if (!TextUtils.equals(secretCode, host)) {
+      return;
+    }
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+    boolean wasEnabled = preferences.getBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false);
+    if (wasEnabled) {
+      preferences.edit().putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+      Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
+      Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_DEACTIVATED);
+      LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint disabled");
+    } else {
+      int drawableId;
+      if (new Random().nextBoolean()) {
+        drawableId = R.drawable.cat_paw;
+      } else {
+        drawableId = R.drawable.dog_paw;
+      }
+      preferences
+          .edit()
+          .putBoolean(PAW_ENABLED_WITH_SECRET_CODE_KEY, true)
+          .putInt(PAW_DRAWABLE_ID_KEY, drawableId)
+          .apply();
+      Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
+      Logger.get(context).logImpression(Type.EVENT_ANSWER_HINT_ACTIVATED);
+      LogUtil.i("PawSecretCodeListener.onReceive", "PawAnswerHint enabled");
+    }
+  }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
new file mode 100644
index 0000000..f7ff6eb
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/cat_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
new file mode 100644
index 0000000..3a23254
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable-xxhdpi/dog_paw.webp
Binary files differ
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
similarity index 89%
rename from java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
rename to java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
index d505014..c3b12a0 100644
--- a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/paw_hint.xml
@@ -24,9 +24,10 @@
   android:clipToPadding="false"
   android:visibility="gone">
   <ImageView
-    android:id="@+id/payload"
-    android:layout_width="191dp"
-    android:layout_height="773dp"
+    android:id="@+id/paw_image"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:src="@drawable/cat_paw"
     android:layout_gravity="center"
     android:alpha="0"
     android:rotation="-30"
diff --git a/java/com/android/incallui/answer/impl/proguard.flags b/java/com/android/incallui/answer/impl/proguard.flags
new file mode 100644
index 0000000..0163528
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/proguard.flags
@@ -0,0 +1,5 @@
+# Used in com.android.dialer.answer.impl.SmsBottomSheetFragment
+-keep class android.support.design.widget.BottomSheetBehavior {
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>();
+}
\ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
index c48b68f..8329707 100644
--- a/java/com/android/incallui/answer/impl/res/values/dimens.xml
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -22,4 +22,5 @@
   <dimen name="answer_avatar_size">0dp</dimen>
   <dimen name="answer_importance_margin_bottom">0dp</dimen>
   <bool name="answer_important_call_allowed">false</bool>
+  <integer name="answer_animate_entry_millis">1000</integer>
 </resources>
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
index 0c374eb..f03efef 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreen.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -24,7 +24,7 @@
 
   String getCallId();
 
-  int getVideoState();
+  boolean isVideoCall();
 
   boolean isVideoUpgradeRequest();
 
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
index 9934497..36b4e3a 100644
--- a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -27,7 +27,7 @@
 
   void onRejectCallWithMessage(String message);
 
-  void onAnswer(int videoState);
+  void onAnswer(boolean answerVideoAsAudio);
 
   void onReject();
 
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index edc3db3..6a2c4b4 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -23,7 +23,6 @@
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.DialerCallListener;
 
@@ -141,7 +140,7 @@
   public void onHandoverToWifiFailure() {}
 
   @Override
-  public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+  public void onDialerCallSessionModificationStateChange() {}
 
   @Override
   public void onScreenOn() {
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 862c71c..c88802f 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -38,10 +38,10 @@
 import com.android.dialer.shortcuts.ShortcutUsageReporter;
 import com.android.dialer.spam.Spam;
 import com.android.dialer.spam.SpamBindings;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.Map;
@@ -110,6 +110,8 @@
     Trace.beginSection("onCallAdded");
     final DialerCall call =
         new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+    logSecondIncomingCall(context, call);
+
     final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
     call.addListener(dialerCallListener);
     LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
@@ -184,6 +186,30 @@
     Trace.endSection();
   }
 
+  private void logSecondIncomingCall(@NonNull Context context, @NonNull DialerCall incomingCall) {
+    DialerCall firstCall = getFirstCall();
+    if (firstCall != null) {
+      int impression = 0;
+      if (firstCall.isVideoCall()) {
+        if (incomingCall.isVideoCall()) {
+          impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VIDEO_CALL;
+        } else {
+          impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VOICE_CALL;
+        }
+      } else {
+        if (incomingCall.isVideoCall()) {
+          impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VIDEO_CALL;
+        } else {
+          impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VOICE_CALL;
+        }
+      }
+      Assert.checkArgument(impression != 0);
+      Logger.get(context)
+          .logCallImpression(
+              impression, incomingCall.getUniqueCallId(), incomingCall.getTimeAddedMs());
+    }
+  }
+
   private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
     if (BuildCompat.isAtLeastO()) {
       return call.isPotentialEmergencyCallback();
@@ -440,8 +466,8 @@
    */
   public DialerCall getVideoUpgradeRequestCall() {
     for (DialerCall call : mCallById.values()) {
-      if (call.getSessionModificationState()
-          == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      if (call.getVideoTech().getSessionModificationState()
+          == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
         return call;
       }
     }
@@ -637,17 +663,7 @@
    */
   public void notifyCallsOfDeviceRotation(int rotation) {
     for (DialerCall call : mCallById.values()) {
-      // First, ensure that the call videoState has video enabled (there is no need to set
-      // device orientation on a voice call which has not yet been upgraded to video).
-      // Second, ensure a VideoCall is set on the call so that the change can be sent to the
-      // provider (a VideoCall can be present for a call that does not currently have video,
-      // but can be upgraded to video).
-
-      // NOTE: is it necessary to use this order because getVideoCall references the class
-      // VideoProfile which is not available on APIs <23 (M).
-      if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
-        call.getVideoCall().setDeviceOrientation(rotation);
-      }
+      call.getVideoTech().setDeviceOrientation(rotation);
     }
   }
 
@@ -675,7 +691,7 @@
     void onUpgradeToVideo(DialerCall call);
 
     /** Called when the session modification state of a call changes. */
-    void onSessionModificationStateChange(@SessionModificationState int newState);
+    void onSessionModificationStateChange(DialerCall call);
 
     /**
      * Called anytime there are changes to the call list. The change can be switching call states,
@@ -754,9 +770,9 @@
     }
 
     @Override
-    public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+    public void onDialerCallSessionModificationStateChange() {
       for (Listener listener : mListeners) {
-        listener.onSessionModificationStateChange(state);
+        listener.onSessionModificationStateChange(mCall);
       }
     }
   }
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index bd8f006..15a0233 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.Trace;
 import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.Call;
 import android.telecom.Call.Details;
@@ -47,10 +48,15 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.ConfigProviderBindings;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
 import com.android.dialer.logging.nano.ContactLookupResult;
-import com.android.dialer.util.CallUtil;
 import com.android.incallui.latencyreport.LatencyReport;
 import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+import com.android.incallui.videotech.empty.EmptyVideoTech;
+import com.android.incallui.videotech.ims.ImsVideoTech;
+import com.android.incallui.videotech.rcs.RcsVideoShare;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -62,11 +68,16 @@
 import java.util.concurrent.TimeUnit;
 
 /** Describes a single call and its state. */
-public class DialerCall {
+public class DialerCall implements VideoTechListener {
 
   public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
   public static final int CALL_HISTORY_STATUS_PRESENT = 1;
   public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+
+  // Hard coded property for {@code Call}. Upstreamed change from Motorola.
+  // TODO(b/35359461): Move it to Telecom in framework.
+  public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
+
   private static final String ID_PREFIX = "DialerCall_";
   private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
       "emergency_callback_window_millis";
@@ -82,13 +93,13 @@
   private final LatencyReport mLatencyReport;
   private final String mId;
   private final List<String> mChildCallIds = new ArrayList<>();
-  private final VideoSettings mVideoSettings = new VideoSettings();
   private final LogState mLogState = new LogState();
   private final Context mContext;
   private final DialerCallDelegate mDialerCallDelegate;
   private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
   private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
       new CopyOnWriteArrayList<>();
+  private final VideoTechManager mVideoTechManager;
 
   private boolean mIsEmergencyCall;
   private Uri mHandle;
@@ -98,13 +109,6 @@
   private boolean hasShownWiFiToLteHandoverToast;
   private boolean doNotShowDialogForHandoffToWifiFailure;
 
-  @SessionModificationState private int mSessionModificationState;
-  private int mVideoState;
-  /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
-  private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
-
-  private InCallVideoCallCallback mVideoCallCallback;
-  private boolean mIsVideoCallCallbackRegistered;
   private String mChildNumber;
   private String mLastForwardedNumber;
   private String mCallSubject;
@@ -118,6 +122,7 @@
   private boolean didShowCameraPermission;
   private String callProviderLabel;
   private String callbackNumber;
+  private int mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
 
   public static String getNumberFromHandle(Uri handle) {
     return handle == null ? "" : handle.getSchemeSpecificPart();
@@ -125,7 +130,7 @@
 
   /**
    * Whether the call is put on hold by remote party. This is different than the {@link
-   * State.ONHOLD} state which indicates that the call is being held locally on the device.
+   * State#ONHOLD} state which indicates that the call is being held locally on the device.
    */
   private boolean isRemotelyHeld;
 
@@ -189,7 +194,7 @@
         @Override
         public void onCallDestroyed(Call call) {
           LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
-          call.unregisterCallback(this);
+          unregisterCallback();
         }
 
         @Override
@@ -248,7 +253,10 @@
     mLatencyReport = latencyReport;
     mId = ID_PREFIX + Integer.toString(sIdCounter++);
 
-    updateFromTelecomCall(registerCallback);
+    // Must be after assigning mTelecomCall
+    mVideoTechManager = new VideoTechManager(this);
+
+    updateFromTelecomCall();
 
     if (registerCallback) {
       mTelecomCall.registerCallback(mTelecomCallCallback);
@@ -348,19 +356,24 @@
     return mTelecomCall.getDetails().getStatusHints();
   }
 
-  /**
-   * @return video settings of the call, null if the call is not a video call.
-   * @see VideoProfile
-   */
-  public VideoSettings getVideoSettings() {
-    return mVideoSettings;
+  public int getCameraDir() {
+    return mCameraDirection;
+  }
+
+  public void setCameraDir(int cameraDir) {
+    if (cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING
+        || cameraDir == CameraDirection.CAMERA_DIRECTION_BACK_FACING) {
+      mCameraDirection = cameraDir;
+    } else {
+      mCameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
+    }
   }
 
   private void update() {
     Trace.beginSection("Update");
     int oldState = getState();
     // We want to potentially register a video call callback here.
-    updateFromTelecomCall(true /* registerCallback */);
+    updateFromTelecomCall();
     if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
       for (DialerCallListener listener : mListeners) {
         listener.onDialerCallDisconnect();
@@ -373,21 +386,15 @@
     Trace.endSection();
   }
 
-  private void updateFromTelecomCall(boolean registerCallback) {
+  private void updateFromTelecomCall() {
     LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+
+    mVideoTechManager.dispatchCallStateChanged(mTelecomCall.getState());
+
     final int translatedState = translateState(mTelecomCall.getState());
     if (mState != State.BLOCKED) {
       setState(translatedState);
       setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
-      maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
-    }
-
-    if (registerCallback && mTelecomCall.getVideoCall() != null) {
-      if (mVideoCallCallback == null) {
-        mVideoCallCallback = new InCallVideoCallCallback(this);
-      }
-      mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
-      mIsVideoCallCallbackRegistered = true;
     }
 
     mChildCallIds.clear();
@@ -428,19 +435,6 @@
         }
       }
     }
-
-    if (mSessionModificationState
-            == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
-        && isVideoCall()) {
-      // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
-      // whether the video upgrade request was accepted. We don't clear the session modification
-      // state right away though to avoid having the UI switch from video to voice to video.
-      // Once the underlying telecom call updates to video mode it's safe to clear the state.
-      LogUtil.i(
-          "DialerCall.updateFromTelecomCall",
-          "upgraded to video, clearing session modification state");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
   }
 
   /**
@@ -518,25 +512,6 @@
     }
   }
 
-  /**
-   * Determines if a received upgrade to video request should be cancelled. This can happen if
-   * another InCall UI responds to the upgrade to video request.
-   *
-   * @param newVideoState The new video state.
-   */
-  private void maybeCancelVideoUpgrade(int newVideoState) {
-    boolean isVideoStateChanged = mVideoState != newVideoState;
-
-    if (mSessionModificationState
-            == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
-        && isVideoStateChanged) {
-
-      LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    }
-    mVideoState = newVideoState;
-  }
-
   public String getId() {
     return mId;
   }
@@ -710,6 +685,7 @@
     return mTelecomCall.getDetails().hasProperty(property);
   }
 
+  @NonNull
   public String getUniqueCallId() {
     return uniqueCallId;
   }
@@ -733,15 +709,9 @@
     return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
   }
 
-  /**
-   * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
-   *     null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
-   *     {@link VideoCall}.
-   */
+  /** @return The {@link VideoCall} instance associated with the {@link Call}. */
   public VideoCall getVideoCall() {
-    return mTelecomCall == null || !mIsVideoCallCallbackRegistered
-        ? null
-        : mTelecomCall.getVideoCall();
+    return mTelecomCall == null ? null : mTelecomCall.getVideoCall();
   }
 
   public List<String> getChildCallIds() {
@@ -761,7 +731,15 @@
   }
 
   public boolean isVideoCall() {
-    return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+    return getVideoTech().isTransmittingOrReceiving();
+  }
+
+  public boolean hasReceivedVideoUpgradeRequest() {
+    return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
+  }
+
+  public boolean hasSentVideoUpgradeRequest() {
+    return VideoUtils.hasSentVideoUpgradeRequest(getVideoTech().getSessionModificationState());
   }
 
   /**
@@ -772,76 +750,6 @@
     mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
   }
 
-  /**
-   * Gets the video state which was requested via a session modification request.
-   *
-   * @return The video state.
-   */
-  public int getRequestedVideoState() {
-    return mRequestedVideoState;
-  }
-
-  /**
-   * Handles incoming session modification requests. Stores the pending video request and sets the
-   * session modification state to {@link
-   * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
-   * track of the fact the request was received. Only upgrade requests require user confirmation and
-   * will be handled by this method. The remote user can turn off their own camera without
-   * confirmation.
-   *
-   * @param videoState The requested video state.
-   */
-  public void setRequestedVideoState(int videoState) {
-    LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
-    if (videoState == getVideoState()) {
-      LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
-      setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-      return;
-    }
-
-    mRequestedVideoState = videoState;
-    setSessionModificationState(
-        DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
-    for (DialerCallListener listener : mListeners) {
-      listener.onDialerCallUpgradeToVideo();
-    }
-
-    LogUtil.i(
-        "DialerCall.setRequestedVideoState",
-        "mSessionModificationState: %d, videoState: %d",
-        mSessionModificationState,
-        videoState);
-    update();
-  }
-
-  /**
-   * Gets the current video session modification state.
-   *
-   * @return The session modification state.
-   */
-  @SessionModificationState
-  public int getSessionModificationState() {
-    return mSessionModificationState;
-  }
-
-  /**
-   * Set the session modification state. Used to keep track of pending video session modification
-   * operations and to inform listeners of these changes.
-   *
-   * @param state the new session modification state.
-   */
-  public void setSessionModificationState(@SessionModificationState int state) {
-    boolean hasChanged = mSessionModificationState != state;
-    if (hasChanged) {
-      LogUtil.i(
-          "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
-      mSessionModificationState = state;
-      for (DialerCallListener listener : mListeners) {
-        listener.onDialerCallSessionModificationStateChange(state);
-      }
-    }
-  }
-
   public LogState getLogState() {
     return mLogState;
   }
@@ -862,24 +770,6 @@
   }
 
   /**
-   * Determines if the external call is pullable.
-   *
-   * <p>An external call is one which does not exist locally for the {@link
-   * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
-   * which means that the user can request it be transferred to the current device.
-   *
-   * <p>External calls are only supported in N and higher.
-   *
-   * @return {@code true} if the call is an external call, {@code false} otherwise.
-   */
-  public boolean isPullableExternalCall() {
-    return VERSION.SDK_INT >= VERSION_CODES.N
-        && (mTelecomCall.getDetails().getCallCapabilities()
-                & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
-            == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
-  }
-
-  /**
    * Determines if answering this call will cause an ongoing video call to be dropped.
    *
    * @return {@code true} if answering this call will drop an ongoing video call, {@code false}
@@ -922,7 +812,7 @@
     return String.format(
         Locale.US,
         "[%s, %s, %s, %s, children:%s, parent:%s, "
-            + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+            + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, CameraDir:%s]",
         mId,
         State.toString(getState()),
         Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
@@ -931,8 +821,8 @@
         getParentId(),
         this.mTelecomCall.getConferenceableCalls(),
         VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
-        mSessionModificationState,
-        getVideoSettings());
+        getVideoTech().getSessionModificationState(),
+        getCameraDir());
   }
 
   public String toSimpleString() {
@@ -1012,20 +902,6 @@
     mTelecomCall.unregisterCallback(mTelecomCallCallback);
   }
 
-  public void acceptUpgradeRequest(int videoState) {
-    LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
-    VideoProfile videoProfile = new VideoProfile(videoState);
-    getVideoCall().sendSessionModifyResponse(videoProfile);
-    setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-  }
-
-  public void declineUpgradeRequest() {
-    LogUtil.i("DialerCall.declineUpgradeRequest", "");
-    VideoProfile videoProfile = new VideoProfile(getVideoState());
-    getVideoCall().sendSessionModifyResponse(videoProfile);
-    setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-  }
-
   public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
     LogUtil.i(
         "DialerCall.phoneAccountSelected",
@@ -1064,6 +940,10 @@
     mTelecomCall.answer(videoState);
   }
 
+  public void answer() {
+    answer(mTelecomCall.getDetails().getVideoState());
+  }
+
   public void reject(boolean rejectWithMessage, String message) {
     LogUtil.i("DialerCall.reject", "");
     mTelecomCall.reject(rejectWithMessage, message);
@@ -1095,6 +975,10 @@
     return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
   }
 
+  public VideoTech getVideoTech() {
+    return mVideoTechManager.getVideoTech();
+  }
+
   public String getCallbackNumber() {
     if (callbackNumber == null) {
       // Show the emergency callback number if either:
@@ -1146,6 +1030,39 @@
     return null;
   }
 
+  @Override
+  public void onVideoTechStateChanged() {
+    update();
+  }
+
+  @Override
+  public void onSessionModificationStateChanged() {
+    for (DialerCallListener listener : mListeners) {
+      listener.onDialerCallSessionModificationStateChange();
+    }
+  }
+
+  @Override
+  public void onCameraDimensionsChanged(int width, int height) {
+    InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged(this, width, height);
+  }
+
+  @Override
+  public void onPeerDimensionsChanged(int width, int height) {
+    InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
+  }
+
+  @Override
+  public void onVideoUpgradeRequestReceived() {
+    LogUtil.enterBlock("DialerCall.onVideoUpgradeRequestReceived");
+
+    for (DialerCallListener listener : mListeners) {
+      listener.onDialerCallUpgradeToVideo();
+    }
+
+    update();
+  }
+
   /**
    * Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
    * means there is no result.
@@ -1191,8 +1108,8 @@
         case CONFERENCED:
           return true;
         default:
+          return false;
       }
-      return false;
     }
 
     public static boolean isDialing(int state) {
@@ -1239,71 +1156,11 @@
     }
   }
 
-  /**
-   * Defines different states of session modify requests, which are used to upgrade to video, or
-   * downgrade to audio.
-   */
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({
-    SESSION_MODIFICATION_STATE_NO_REQUEST,
-    SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
-    SESSION_MODIFICATION_STATE_REQUEST_FAILED,
-    SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
-    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
-    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
-    SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
-    SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
-  })
-  public @interface SessionModificationState {}
-
-  public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
-  public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
-  public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
-  public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
-  public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
-  public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
-  public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
-  public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
-
-  public static class VideoSettings {
-
+  /** Camera direction constants */
+  public static class CameraDirection {
     public static final int CAMERA_DIRECTION_UNKNOWN = -1;
     public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
     public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
-
-    private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-
-    /**
-     * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
-     * state of the call should be used to infer the camera direction.
-     *
-     * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
-     * @see {@link CameraCharacteristics#LENS_FACING_BACK}
-     */
-    public int getCameraDir() {
-      return mCameraDirection;
-    }
-
-    /**
-     * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
-     * state of the call should be used to infer the camera direction.
-     *
-     * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
-     * @see {@link CameraCharacteristics#LENS_FACING_BACK}
-     */
-    public void setCameraDir(int cameraDirection) {
-      if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
-          || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
-        mCameraDirection = cameraDirection;
-      } else {
-        mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
-      }
-    }
-
-    @Override
-    public String toString() {
-      return "(CameraDir:" + getCameraDir() + ")";
-    }
   }
 
   /**
@@ -1394,6 +1251,48 @@
     }
   }
 
+  private static class VideoTechManager {
+    private final EmptyVideoTech emptyVideoTech = new EmptyVideoTech();
+    private final VideoTech[] videoTechs;
+    private VideoTech savedTech;
+
+    VideoTechManager(DialerCall call) {
+      String phoneNumber = call.getNumber();
+
+      // Insert order here determines the priority of that video tech option
+      videoTechs =
+          new VideoTech[] {
+            new ImsVideoTech(call, call.mTelecomCall),
+            new RcsVideoShare(
+                EnrichedCallComponent.get(call.mContext).getEnrichedCallManager(),
+                call,
+                phoneNumber != null ? phoneNumber : "")
+          };
+    }
+
+    VideoTech getVideoTech() {
+      if (savedTech != null) {
+        return savedTech;
+      }
+
+      for (VideoTech tech : videoTechs) {
+        if (tech.isAvailable()) {
+          // Remember the first VideoTech that becomes available and always use it
+          savedTech = tech;
+          return savedTech;
+        }
+      }
+
+      return emptyVideoTech;
+    }
+
+    void dispatchCallStateChanged(int newState) {
+      for (VideoTech videoTech : videoTechs) {
+        videoTech.onCallStateChanged(newState);
+      }
+    }
+  }
+
   /** Called when canned text responses have been loaded. */
   public interface CannedTextResponsesLoadedListener {
     void onCannedTextResponsesLoaded(DialerCall call);
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
index b426cd7..fece103 100644
--- a/java/com/android/incallui/call/DialerCallListener.java
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -16,8 +16,6 @@
 
 package com.android.incallui.call;
 
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
 /** Used to monitor state changes in a dialer call. */
 public interface DialerCallListener {
 
@@ -31,7 +29,7 @@
 
   void onDialerCallUpgradeToVideo();
 
-  void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+  void onDialerCallSessionModificationStateChange();
 
   void onWiFiToLteHandover();
 
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
deleted file mode 100644
index f897ac9..0000000
--- a/java/com/android/incallui/call/InCallVideoCallCallback.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.incallui.call;
-
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.telecom.Connection;
-import android.telecom.Connection.VideoProvider;
-import android.telecom.InCallService.VideoCall;
-import android.telecom.VideoProfile;
-import android.telecom.VideoProfile.CameraCapabilities;
-import com.android.dialer.common.LogUtil;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-
-/** Implements the InCallUI VideoCall Callback. */
-public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
-
-  private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
-
-  private final DialerCall call;
-  @Nullable private Handler handler;
-  @SessionModificationState private int newSessionModificationState;
-
-  public InCallVideoCallCallback(DialerCall call) {
-    this.call = call;
-  }
-
-  @Override
-  public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
-    LogUtil.i(
-        "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
-    int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
-    int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
-
-    boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
-    boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
-
-    if (wasVideoCall && !isVideoCall) {
-      LogUtil.v(
-          "InCallVideoCallCallback.onSessionModifyRequestReceived",
-          "call downgraded to " + newVideoState);
-    } else if (previousVideoState != newVideoState) {
-      InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
-    }
-  }
-
-  /**
-   * @param status Status of the session modify request. Valid values are {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
-   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
-   * @param responseProfile The actual profile changes made by the peer device.
-   */
-  @Override
-  public void onSessionModifyResponseReceived(
-      int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
-    LogUtil.i(
-        "InCallVideoCallCallback.onSessionModifyResponseReceived",
-        "status: %d, "
-            + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
-        status,
-        requestedProfile,
-        responseProfile,
-        call.getSessionModificationState());
-
-    if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
-      if (handler == null) {
-        handler = new Handler();
-      } else {
-        handler.removeCallbacks(this);
-      }
-
-      newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
-      if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
-        // This will update the video UI to display the error message.
-        call.setSessionModificationState(newSessionModificationState);
-      }
-
-      // Wait for 4 seconds and then clean the session modification state. This allows the video UI
-      // to stay up so that the user can read the error message.
-      //
-      // If the other person accepted the upgrade request then this will keep the video UI up until
-      // the call's video state change. Without this we would switch to the voice call and then
-      // switch back to video UI.
-      handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    } else if (call.getSessionModificationState()
-        == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
-      call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
-    } else {
-      LogUtil.i(
-          "InCallVideoCallCallback.onSessionModifyResponseReceived",
-          "call is not waiting for " + "response, doing nothing");
-    }
-  }
-
-  @SessionModificationState
-  private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
-    switch (telecomStatus) {
-      case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
-        return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
-      case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
-      case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
-        // Check if it's already video call, which means the request is not video upgrade request.
-        if (VideoUtils.isVideoCall(call.getVideoState())) {
-          return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
-        } else {
-          return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
-        }
-      case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
-        return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
-      case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
-        return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
-      default:
-        LogUtil.e(
-            "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
-            "unknown status: %d",
-            telecomStatus);
-        return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
-    }
-  }
-
-  @Override
-  public void onCallSessionEvent(int event) {
-    InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
-  }
-
-  @Override
-  public void onPeerDimensionsChanged(int width, int height) {
-    InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
-  }
-
-  @Override
-  public void onVideoQualityChanged(int videoQuality) {
-    InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
-  }
-
-  /**
-   * Handles a change to the call data usage. No implementation as the in-call UI does not display
-   * data usage.
-   *
-   * @param dataUsage The updated data usage.
-   */
-  @Override
-  public void onCallDataUsageChanged(long dataUsage) {
-    LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
-    InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
-  }
-
-  /**
-   * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
-   * use of camera capabilities.
-   *
-   * @param cameraCapabilities The changed camera capabilities.
-   */
-  @Override
-  public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
-    if (cameraCapabilities != null) {
-      InCallVideoCallCallbackNotifier.getInstance()
-          .cameraDimensionsChanged(
-              call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
-    }
-  }
-
-  /**
-   * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
-   * clear the session modify state.
-   */
-  @Override
-  public void run() {
-    if (call.getSessionModificationState() == newSessionModificationState) {
-      LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
-      call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
-    } else {
-      LogUtil.i(
-          "InCallVideoCallCallback.onSessionModifyResponseReceived",
-          "session modification state has changed, not clearing state");
-    }
-  }
-}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
index 4a94926..1cb9f74 100644
--- a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -18,16 +18,12 @@
 
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import com.android.dialer.common.LogUtil;
 import java.util.Collections;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-/**
- * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
- * events.
- */
+/** Class used to notify interested parties of incoming video related events. */
 public class InCallVideoCallCallbackNotifier {
 
   /** Singleton instance of this class. */
@@ -37,12 +33,6 @@
    * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
    * resizing, 1 means we only expect a single thread to access the map so make only a single shard
    */
-  private final Set<SessionModificationListener> mSessionModificationListeners =
-      Collections.newSetFromMap(
-          new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
-
-  private final Set<VideoEventListener> mVideoEventListeners =
-      Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
   private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
       Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
 
@@ -55,48 +45,6 @@
   }
 
   /**
-   * Adds a new {@link SessionModificationListener}.
-   *
-   * @param listener The listener.
-   */
-  public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
-    Objects.requireNonNull(listener);
-    mSessionModificationListeners.add(listener);
-  }
-
-  /**
-   * Remove a {@link SessionModificationListener}.
-   *
-   * @param listener The listener.
-   */
-  public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
-    if (listener != null) {
-      mSessionModificationListeners.remove(listener);
-    }
-  }
-
-  /**
-   * Adds a new {@link VideoEventListener}.
-   *
-   * @param listener The listener.
-   */
-  public void addVideoEventListener(@NonNull VideoEventListener listener) {
-    Objects.requireNonNull(listener);
-    mVideoEventListeners.add(listener);
-  }
-
-  /**
-   * Remove a {@link VideoEventListener}.
-   *
-   * @param listener The listener.
-   */
-  public void removeVideoEventListener(@Nullable VideoEventListener listener) {
-    if (listener != null) {
-      mVideoEventListeners.remove(listener);
-    }
-  }
-
-  /**
    * Adds a new {@link SurfaceChangeListener}.
    *
    * @param listener The listener.
@@ -118,56 +66,6 @@
   }
 
   /**
-   * Inform listeners of an upgrade to video request for a call.
-   *
-   * @param call The call.
-   * @param videoState The video state we want to upgrade to.
-   */
-  public void upgradeToVideoRequest(DialerCall call, int videoState) {
-    LogUtil.v(
-        "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
-        "call = " + call + " new video state = " + videoState);
-    for (SessionModificationListener listener : mSessionModificationListeners) {
-      listener.onUpgradeToVideoRequest(call, videoState);
-    }
-  }
-
-  /**
-   * Inform listeners of a call session event.
-   *
-   * @param event The call session event.
-   */
-  public void callSessionEvent(int event) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onCallSessionEvent(event);
-    }
-  }
-
-  /**
-   * Inform listeners of a downgrade to audio.
-   *
-   * @param call The call.
-   * @param paused The paused state.
-   */
-  public void peerPausedStateChanged(DialerCall call, boolean paused) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onPeerPauseStateChanged(call, paused);
-    }
-  }
-
-  /**
-   * Inform listeners of any change in the video quality of the call
-   *
-   * @param call The call.
-   * @param videoQuality The updated video quality of the call.
-   */
-  public void videoQualityChanged(DialerCall call, int videoQuality) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onVideoQualityChanged(call, videoQuality);
-    }
-  }
-
-  /**
    * Inform listeners of a change to peer dimensions.
    *
    * @param call The call.
@@ -194,67 +92,6 @@
   }
 
   /**
-   * Inform listeners of a change to call data usage.
-   *
-   * @param dataUsage data usage value
-   */
-  public void callDataUsageChanged(long dataUsage) {
-    for (VideoEventListener listener : mVideoEventListeners) {
-      listener.onCallDataUsageChange(dataUsage);
-    }
-  }
-
-  /** Listener interface for any class that wants to be notified of upgrade to video request. */
-  public interface SessionModificationListener {
-
-    /**
-     * Called when a peer request is received to upgrade an audio-only call to a video call.
-     *
-     * @param call The call the request was received for.
-     * @param videoState The requested video state.
-     */
-    void onUpgradeToVideoRequest(DialerCall call, int videoState);
-  }
-
-  /**
-   * Listener interface for any class that wants to be notified of video events, including pause and
-   * un-pause of peer video, video quality changes.
-   */
-  public interface VideoEventListener {
-
-    /**
-     * Called when the peer pauses or un-pauses video transmission.
-     *
-     * @param call The call which paused or un-paused video transmission.
-     * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
-     */
-    void onPeerPauseStateChanged(DialerCall call, boolean paused);
-
-    /**
-     * Called when the video quality changes.
-     *
-     * @param call The call whose video quality changes.
-     * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
-     */
-    void onVideoQualityChanged(DialerCall call, int videoCallQuality);
-
-    /*
-     * Called when call data usage value is requested or when call data usage value is updated
-     * because of a call state change
-     *
-     * @param dataUsage call data usage value
-     */
-    void onCallDataUsageChange(long dataUsage);
-
-    /**
-     * Called when call session event is raised.
-     *
-     * @param event The call session event.
-     */
-    void onCallSessionEvent(int event);
-  }
-
-  /**
    * Listener interface for any class that wants to be notified of changes to the video surfaces.
    */
   public interface SurfaceChangeListener {
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
index 80fbfb1..b99b732 100644
--- a/java/com/android/incallui/call/VideoUtils.java
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -19,113 +19,24 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.content.ContextCompat;
-import android.telecom.VideoProfile;
-import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.util.DialerUtils;
-import com.android.incallui.call.DialerCall.SessionModificationState;
-import java.util.Objects;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 
 public class VideoUtils {
 
   private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
 
-  public static boolean isVideoCall(@Nullable DialerCall call) {
-    return call != null && isVideoCall(call.getVideoState());
-  }
-
-  public static boolean isVideoCall(int videoState) {
-    return CompatUtils.isVideoCompatible()
-        && (VideoProfile.isTransmissionEnabled(videoState)
-            || VideoProfile.isReceptionEnabled(videoState));
-  }
-
-  public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
-    return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
-  }
-
   public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
-    return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
-        || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
-        || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
-        || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
-  }
-
-  public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
-    return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+    return state == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+        || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+        || state == VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+        || state == VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
   }
 
   public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
-    return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
-  }
-
-  public static boolean isBidirectionalVideoCall(DialerCall call) {
-    return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
-  }
-
-  public static boolean isTransmissionEnabled(DialerCall call) {
-    if (!CompatUtils.isVideoCompatible()) {
-      return false;
-    }
-
-    return VideoProfile.isTransmissionEnabled(call.getVideoState());
-  }
-
-  public static boolean isIncomingVideoCall(DialerCall call) {
-    if (!VideoUtils.isVideoCall(call)) {
-      return false;
-    }
-    final int state = call.getState();
-    return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
-  }
-
-  public static boolean isActiveVideoCall(DialerCall call) {
-    return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
-  }
-
-  public static boolean isOutgoingVideoCall(DialerCall call) {
-    if (!VideoUtils.isVideoCall(call)) {
-      return false;
-    }
-    final int state = call.getState();
-    return DialerCall.State.isDialing(state)
-        || state == DialerCall.State.CONNECTING
-        || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
-  }
-
-  public static boolean isAudioCall(DialerCall call) {
-    if (!CompatUtils.isVideoCompatible()) {
-      return true;
-    }
-
-    return call != null && VideoProfile.isAudioOnly(call.getVideoState());
-  }
-
-  // TODO (ims-vt) Check if special handling is needed for CONF calls.
-  public static boolean canVideoPause(DialerCall call) {
-    return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
-  }
-
-  public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
-    Objects.requireNonNull(call);
-    if (VideoProfile.isAudioOnly(call.getVideoState())) {
-      throw new IllegalStateException();
-    }
-    return new VideoProfile(getPausedVideoState(call.getVideoState()));
-  }
-
-  public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
-    Objects.requireNonNull(call);
-    return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
-  }
-
-  public static int getUnPausedVideoState(int videoState) {
-    return videoState & (~VideoProfile.STATE_PAUSED);
-  }
-
-  public static int getPausedVideoState(int videoState) {
-    return videoState | VideoProfile.STATE_PAUSED;
+    return state == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
   }
 
   public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/calllocation/CallLocation.java
similarity index 64%
rename from java/com/android/incallui/maps/StaticMapFactory.java
rename to java/com/android/incallui/calllocation/CallLocation.java
index a350138..15a6a8e 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/calllocation/CallLocation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,15 +14,19 @@
  * limitations under the License
  */
 
-package com.android.incallui.maps;
+package com.android.incallui.calllocation;
 
-import android.location.Location;
+import android.content.Context;
 import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** Used to show the user's location during an emergency call. */
+public interface CallLocation {
+
+  boolean canGetLocation(@NonNull Context context);
 
   @NonNull
-  Fragment getStaticMap(@NonNull Location location);
+  Fragment getLocationFragment(@NonNull Context context);
+
+  void close();
 }
diff --git a/java/com/android/incallui/calllocation/CallLocationComponent.java b/java/com/android/incallui/calllocation/CallLocationComponent.java
new file mode 100644
index 0000000..6b1faf2
--- /dev/null
+++ b/java/com/android/incallui/calllocation/CallLocationComponent.java
@@ -0,0 +1,46 @@
+/*
+ * 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.incallui.calllocation;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.incallui.calllocation.stub.StubCallLocationModule;
+
+/** Subcomponent that can be used to access the call location implementation. */
+public class CallLocationComponent {
+  private static CallLocationComponent instance;
+  private CallLocation callLocation;
+
+  public CallLocation getCallLocation(){
+    if (callLocation == null) {
+        callLocation = new StubCallLocationModule.StubCallLocation();
+    }
+    return callLocation;
+  }
+
+  public static CallLocationComponent get(Context context) {
+    if (instance == null) {
+        instance = new CallLocationComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    CallLocationComponent callLocationComponent();
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
new file mode 100644
index 0000000..550c580
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.incallui.calllocation.impl">
+
+  <application>
+    <meta-data
+      android:name="com.google.android.gms.version"
+      android:value="@integer/google_play_services_version"/>
+  </application>
+</manifest>
diff --git a/java/com/android/incallui/calllocation/impl/AuthException.java b/java/com/android/incallui/calllocation/impl/AuthException.java
new file mode 100644
index 0000000..26def2f
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/AuthException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+/** For detecting backend authorization errors */
+public class AuthException extends Exception {
+
+  public AuthException(String detailMessage) {
+    super(detailMessage);
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
new file mode 100644
index 0000000..20f5ffb
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import javax.inject.Inject;
+
+/** Uses Google Play Services to show the user's location during an emergency call. */
+public class CallLocationImpl implements CallLocation {
+
+  private LocationHelper locationHelper;
+  private LocationFragment locationFragment;
+
+  @Inject
+  public CallLocationImpl() {}
+
+  @MainThread
+  @Override
+  public boolean canGetLocation(@NonNull Context context) {
+    Assert.isMainThread();
+    return LocationHelper.canGetLocation(context);
+  }
+
+  @MainThread
+  @NonNull
+  @Override
+  public Fragment getLocationFragment(@NonNull Context context) {
+    Assert.isMainThread();
+    if (locationFragment == null) {
+      locationFragment = new LocationFragment();
+      locationHelper = new LocationHelper(context);
+      locationHelper.addLocationListener(locationFragment.getPresenter());
+    }
+    return locationFragment;
+  }
+
+  @MainThread
+  @Override
+  public void close() {
+    Assert.isMainThread();
+    if (locationFragment != null) {
+      locationHelper.removeLocationListener(locationFragment.getPresenter());
+      locationHelper.close();
+      locationFragment = null;
+      locationHelper = null;
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/CallLocationModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
new file mode 100644
index 0000000..73e8555
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java
@@ -0,0 +1,29 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class CallLocationModule {
+
+  @Binds
+  public abstract CallLocation bindCallLocation(CallLocationImpl callLocation);
+}
diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
new file mode 100644
index 0000000..801b0d3
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java
@@ -0,0 +1,77 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+
+class DownloadMapImageTask extends AsyncTask<Location, Void, Drawable> {
+
+  private static final String STATIC_MAP_SRC_NAME = "src";
+
+  private final WeakReference<LocationUi> mUiReference;
+
+  public DownloadMapImageTask(WeakReference<LocationUi> uiReference) {
+    mUiReference = uiReference;
+  }
+
+  @Override
+  protected Drawable doInBackground(Location... locations) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return null;
+    }
+    if (locations == null || locations.length == 0) {
+      LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided");
+      return null;
+    }
+
+    try {
+      URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0]));
+      InputStream content = (InputStream) mapUrl.getContent();
+
+      TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG);
+      return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME);
+    } catch (Exception ex) {
+      LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex);
+      return null;
+    } finally {
+      TrafficStats.clearThreadStatsTag();
+    }
+  }
+
+  @Override
+  protected void onPostExecute(Drawable mapImage) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return;
+    }
+
+    try {
+      ui.setMap(mapImage);
+    } catch (Exception ex) {
+      LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex);
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
new file mode 100644
index 0000000..18a80b8
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java
@@ -0,0 +1,123 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings.Secure;
+import android.provider.Settings.SettingNotFoundException;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Helper class to check if Google Location Services is enabled. This class is based on
+ * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo
+ */
+public class GoogleLocationSettingHelper {
+
+  /** User has disagreed to use location for Google services. */
+  public static final int USE_LOCATION_FOR_SERVICES_OFF = 0;
+  /** User has agreed to use location for Google services. */
+  public static final int USE_LOCATION_FOR_SERVICES_ON = 1;
+  /** The user has neither agreed nor disagreed to use location for Google services yet. */
+  public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2;
+
+  private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings";
+  private static final Uri GOOGLE_SETTINGS_CONTENT_URI =
+      Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner");
+  private static final String NAME = "name";
+  private static final String VALUE = "value";
+  private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
+
+  /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */
+  public static boolean isEnforceable(Context context) {
+    final ResolveInfo ri =
+        context
+            .getPackageManager()
+            .resolveActivity(
+                new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"),
+                PackageManager.MATCH_DEFAULT_ONLY);
+    return ri != null;
+  }
+
+  /**
+   * Get the current value for the 'Use value for location' setting.
+   *
+   * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link
+   *     #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}.
+   */
+  private static int getUseLocationForServices(Context context) {
+    final ContentResolver resolver = context.getContentResolver();
+    Cursor c = null;
+    String stringValue = null;
+    try {
+      c =
+          resolver.query(
+              GOOGLE_SETTINGS_CONTENT_URI,
+              new String[] {VALUE},
+              NAME + "=?",
+              new String[] {USE_LOCATION_FOR_SERVICES},
+              null);
+      if (c != null && c.moveToNext()) {
+        stringValue = c.getString(0);
+      }
+    } catch (final RuntimeException e) {
+      LogUtil.e(
+          "GoogleLocationSettingHelper.getUseLocationForServices",
+          "Failed to get 'Use My Location' setting",
+          e);
+    } finally {
+      if (c != null) {
+        c.close();
+      }
+    }
+    if (stringValue == null) {
+      return USE_LOCATION_FOR_SERVICES_NOT_SET;
+    }
+    int value;
+    try {
+      value = Integer.parseInt(stringValue);
+    } catch (final NumberFormatException nfe) {
+      value = USE_LOCATION_FOR_SERVICES_NOT_SET;
+    }
+    return value;
+  }
+
+  /** Whether or not the system location setting is enable */
+  public static boolean isSystemLocationSettingEnabled(Context context) {
+    try {
+      return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE)
+          != Secure.LOCATION_MODE_OFF;
+    } catch (SettingNotFoundException e) {
+      LogUtil.e(
+          "GoogleLocationSettingHelper.isSystemLocationSettingEnabled",
+          "Failed to get System Location setting",
+          e);
+      return false;
+    }
+  }
+
+  /** Convenience method that returns true is GLS is ON or if it's not enforceable. */
+  public static boolean isGoogleLocationServicesEnabled(Context context) {
+    return !isEnforceable(context)
+        || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON;
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
new file mode 100644
index 0000000..7bfbaa6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java
@@ -0,0 +1,289 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import static com.android.dialer.util.DialerUtils.closeQuietly;
+
+import android.content.Context;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.SystemClock;
+import android.util.Pair;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.MoreStrings;
+import com.google.android.common.http.UrlRules;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility for making http requests. */
+public class HttpFetcher {
+
+  // Phone number
+  public static final String PARAM_ID = "id";
+  // auth token
+  public static final String PARAM_ACCESS_TOKEN = "access_token";
+  private static final String TAG = HttpFetcher.class.getSimpleName();
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @param urlString The url to request.
+   * @return The response body as a byte array. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static byte[] sendRequestAsByteArray(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    Objects.requireNonNull(urlString);
+
+    URL url = reWriteUrl(context, urlString);
+    if (url == null) {
+      return null;
+    }
+
+    HttpURLConnection conn = null;
+    InputStream is = null;
+    boolean isError = false;
+    final long start = SystemClock.uptimeMillis();
+    try {
+      conn = (HttpURLConnection) url.openConnection();
+      setMethodAndHeaders(conn, requestMethod, headers);
+      int responseCode = conn.getResponseCode();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode);
+      // All 2xx codes are successful.
+      if (responseCode / 100 == 2) {
+        is = conn.getInputStream();
+      } else {
+        is = conn.getErrorStream();
+        isError = true;
+      }
+
+      final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      final byte[] buffer = new byte[1024];
+      int bytesRead;
+
+      while ((bytesRead = is.read(buffer)) != -1) {
+        baos.write(buffer, 0, bytesRead);
+      }
+
+      if (isError) {
+        handleBadResponse(url.toString(), baos.toByteArray());
+        if (responseCode == 401) {
+          throw new AuthException("Auth error");
+        }
+        return null;
+      }
+
+      byte[] response = baos.toByteArray();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes");
+      long end = SystemClock.uptimeMillis();
+      LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms");
+      return response;
+    } finally {
+      closeQuietly(is);
+      if (conn != null) {
+        conn.disconnect();
+      }
+    }
+  }
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static InputStream sendRequestAsInputStream(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    Objects.requireNonNull(urlString);
+
+    URL url = reWriteUrl(context, urlString);
+    if (url == null) {
+      return null;
+    }
+
+    HttpURLConnection httpUrlConnection = null;
+    boolean isSuccess = false;
+    try {
+      httpUrlConnection = (HttpURLConnection) url.openConnection();
+      setMethodAndHeaders(httpUrlConnection, requestMethod, headers);
+      int responseCode = httpUrlConnection.getResponseCode();
+      LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode);
+
+      if (responseCode == 401) {
+        throw new AuthException("Auth error");
+      } else if (responseCode / 100 == 2) { // All 2xx codes are successful.
+        InputStream is = httpUrlConnection.getInputStream();
+        if (is != null) {
+          is = new HttpInputStreamWrapper(httpUrlConnection, is);
+          isSuccess = true;
+          return is;
+        }
+      }
+
+      return null;
+    } finally {
+      if (httpUrlConnection != null && !isSuccess) {
+        httpUrlConnection.disconnect();
+      }
+    }
+  }
+
+  /**
+   * Set http method and headers.
+   *
+   * @param conn The connection to add headers to.
+   * @param requestMethod request method
+   * @param headers http headers where the first item in the pair is the key and second item is the
+   *     value.
+   */
+  private static void setMethodAndHeaders(
+      HttpURLConnection conn, String requestMethod, List<Pair<String, String>> headers)
+      throws ProtocolException {
+    conn.setRequestMethod(requestMethod);
+    if (headers != null) {
+      for (Pair<String, String> pair : headers) {
+        conn.setRequestProperty(pair.first, pair.second);
+      }
+    }
+  }
+
+  private static String obfuscateUrl(String urlString) {
+    final Uri uri = Uri.parse(urlString);
+    final Builder builder =
+        new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath());
+    final Set<String> names = uri.getQueryParameterNames();
+    for (String name : names) {
+      if (PARAM_ACCESS_TOKEN.equals(name)) {
+        builder.appendQueryParameter(name, "token");
+      } else {
+        final String value = uri.getQueryParameter(name);
+        if (PARAM_ID.equals(name)) {
+          builder.appendQueryParameter(name, MoreStrings.toSafeString(value));
+        } else {
+          builder.appendQueryParameter(name, value);
+        }
+      }
+    }
+    return builder.toString();
+  }
+
+  /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */
+  public static String getRequestAsString(Context context, String urlString)
+      throws IOException, AuthException {
+    return getRequestAsString(context, urlString, "GET" /* Default to get. */, null);
+  }
+
+  /**
+   * Send a http request to the given url.
+   *
+   * @param context The android context.
+   * @param urlString The url to request.
+   * @param headers Http headers to pass in the request. {@literal null} is allowed.
+   * @return The response body as a String. Or {@literal null} if status code is not 2xx.
+   * @throws java.io.IOException when an error occurs.
+   */
+  public static String getRequestAsString(
+      Context context, String urlString, String requestMethod, List<Pair<String, String>> headers)
+      throws IOException, AuthException {
+    final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers);
+    if (byteArr == null) {
+      // Encountered error response... just return.
+      return null;
+    }
+    final String response = new String(byteArr);
+    LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response);
+    return response;
+  }
+
+  /**
+   * Lookup up url re-write rules from gServices and apply to the given url.
+   *
+   * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules
+   *
+   * @return The new url.
+   */
+  private static URL reWriteUrl(Context context, String url) {
+    final UrlRules rules = UrlRules.getRules(context.getContentResolver());
+    final UrlRules.Rule rule = rules.matchRule(url);
+    final String newUrl = rule.apply(url);
+
+    if (newUrl == null) {
+      if (LogUtil.isDebugEnabled()) {
+        // Url is blocked by re-write.
+        LogUtil.i(
+            "HttpFetcher.reWriteUrl",
+            "url " + obfuscateUrl(url) + " is blocked.  Ignoring request.");
+      }
+      return null;
+    }
+
+    if (LogUtil.isDebugEnabled()) {
+      LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl));
+      if (!newUrl.equals(url)) {
+        LogUtil.i(
+            "HttpFetcher.reWriteUrl",
+            "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl));
+      }
+    }
+
+    URL urlObject = null;
+    try {
+      urlObject = new URL(newUrl);
+    } catch (MalformedURLException e) {
+      LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e);
+    }
+    return urlObject;
+  }
+
+  private static void handleBadResponse(String url, byte[] response) {
+    LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url);
+    LogUtil.i("HttpFetcher.handleBadResponse", new String(response));
+  }
+
+  /** Disconnect {@link HttpURLConnection} when InputStream is closed */
+  private static class HttpInputStreamWrapper extends FilterInputStream {
+
+    final HttpURLConnection mHttpUrlConnection;
+    final long mStartMillis = SystemClock.uptimeMillis();
+
+    public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) {
+      super(in);
+      mHttpUrlConnection = conn;
+    }
+
+    @Override
+    public void close() throws IOException {
+      super.close();
+      mHttpUrlConnection.disconnect();
+      if (LogUtil.isDebugEnabled()) {
+        long endMillis = SystemClock.uptimeMillis();
+        LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms");
+      }
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java
new file mode 100644
index 0000000..b152cd6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java
@@ -0,0 +1,197 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment which shows location during E911 calls, to supplement the user with accurate location
+ * information in case the user is asked for their location by the emergency responder.
+ *
+ * <p>If location data is inaccurate, stale, or unavailable, this should not be shown.
+ */
+public class LocationFragment extends BaseFragment<LocationPresenter, LocationPresenter.LocationUi>
+    implements LocationPresenter.LocationUi {
+
+  private static final String ADDRESS_DELIMITER = ",";
+
+  // Indexes used to animate fading between views
+  private static final int LOADING_VIEW_INDEX = 0;
+  private static final int LOCATION_VIEW_INDEX = 1;
+  private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+
+  private ViewAnimator viewAnimator;
+  private ImageView locationMap;
+  private TextView addressLine1;
+  private TextView addressLine2;
+  private TextView latLongLine;
+  private Location location;
+  private ViewGroup locationLayout;
+
+  private boolean isMapSet;
+  private boolean isAddressSet;
+  private boolean isLocationSet;
+  private boolean hasTimeoutStarted;
+
+  private final Handler handler = new Handler();
+  private final Runnable dataTimeoutRunnable =
+      () -> {
+        LogUtil.i(
+            "LocationFragment.dataTimeoutRunnable",
+            "timed out so animate any future layout changes");
+        locationLayout.setLayoutTransition(new LayoutTransition());
+        showLocationNow();
+      };
+
+  @Override
+  public LocationPresenter createPresenter() {
+    return new LocationPresenter();
+  }
+
+  @Override
+  public LocationPresenter.LocationUi getUi() {
+    return this;
+  }
+
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    final View view = inflater.inflate(R.layout.location_fragment, container, false);
+    viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator);
+    locationMap = (ImageView) view.findViewById(R.id.location_map);
+    addressLine1 = (TextView) view.findViewById(R.id.address_line_one);
+    addressLine2 = (TextView) view.findViewById(R.id.address_line_two);
+    latLongLine = (TextView) view.findViewById(R.id.lat_long_line);
+    locationLayout = (ViewGroup) view.findViewById(R.id.location_layout);
+    view.setOnClickListener(
+        v -> {
+          LogUtil.enterBlock("LocationFragment.onCreateView");
+          launchMap();
+        });
+    return view;
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    handler.removeCallbacks(dataTimeoutRunnable);
+  }
+
+  @Override
+  public void setMap(Drawable mapImage) {
+    LogUtil.enterBlock("LocationFragment.setMap");
+    isMapSet = true;
+    locationMap.setVisibility(View.VISIBLE);
+    locationMap.setImageDrawable(mapImage);
+    displayWhenReady();
+  }
+
+  @Override
+  public void setAddress(String address) {
+    LogUtil.i("LocationFragment.setAddress", address);
+    isAddressSet = true;
+    addressLine1.setVisibility(View.VISIBLE);
+    addressLine2.setVisibility(View.VISIBLE);
+    if (TextUtils.isEmpty(address)) {
+      addressLine1.setText(null);
+      addressLine2.setText(null);
+    } else {
+
+      // Split the address after the first delimiter for display, if present.
+      // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043"
+      //     => "1600 Amphitheatre Parkway"
+      //     => "Mountain View, CA 94043"
+      int splitIndex = address.indexOf(ADDRESS_DELIMITER);
+      if (splitIndex >= 0) {
+        updateText(addressLine1, address.substring(0, splitIndex).trim());
+        updateText(addressLine2, address.substring(splitIndex + 1).trim());
+      } else {
+        updateText(addressLine1, address);
+        updateText(addressLine2, null);
+      }
+    }
+    displayWhenReady();
+  }
+
+  @Override
+  public void setLocation(Location location) {
+    LogUtil.i("LocationFragment.setLocation", String.valueOf(location));
+    isLocationSet = true;
+    this.location = location;
+
+    if (location != null) {
+      latLongLine.setVisibility(View.VISIBLE);
+      latLongLine.setText(
+          getContext()
+              .getString(
+                  R.string.lat_long_format, location.getLatitude(), location.getLongitude()));
+    }
+    displayWhenReady();
+  }
+
+  private void displayWhenReady() {
+    // Show the location if all data has loaded, otherwise prime the timeout
+    if (isMapSet && isAddressSet && isLocationSet) {
+      showLocationNow();
+    } else if (!hasTimeoutStarted) {
+      handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS);
+      hasTimeoutStarted = true;
+    }
+  }
+
+  private void showLocationNow() {
+    handler.removeCallbacks(dataTimeoutRunnable);
+    if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) {
+      viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX);
+    }
+  }
+
+  @Override
+  public Context getContext() {
+    return getActivity();
+  }
+
+  private void launchMap() {
+    if (location != null) {
+      startActivity(
+          LocationUrlBuilder.getShowMapIntent(
+              location, addressLine1.getText(), addressLine2.getText()));
+    }
+  }
+
+  private static void updateText(TextView view, String text) {
+    if (!Objects.equals(text, view.getText())) {
+      view.setText(text);
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java
new file mode 100644
index 0000000..645e9b8
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java
@@ -0,0 +1,219 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.location.Location;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.MainThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
+import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.location.LocationListener;
+import com.google.android.gms.location.LocationRequest;
+import com.google.android.gms.location.LocationServices;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Uses the Fused location service to get location and pass updates on to listeners. */
+public class LocationHelper {
+
+  private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000;
+  private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000;
+  private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100;
+
+  private final LocationHelperInternal locationHelperInternal;
+  private final List<LocationListener> listeners = new ArrayList<>();
+
+  @MainThread
+  LocationHelper(Context context) {
+    Assert.isMainThread();
+    Assert.checkArgument(canGetLocation(context));
+    locationHelperInternal = new LocationHelperInternal(context);
+  }
+
+  static boolean canGetLocation(Context context) {
+    if (!PermissionsUtil.hasLocationPermissions(context)) {
+      LogUtil.i("LocationHelper.canGetLocation", "no location permissions.");
+      return false;
+    }
+
+    // Ensure that both system location setting is on and google location services are enabled.
+    if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context)
+        || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) {
+      LogUtil.i("LocationHelper.canGetLocation", "location service is disabled.");
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Whether the location is valid. We consider it valid if it was recorded within the specified
+   * time threshold of the present and has an accuracy less than the specified distance threshold.
+   *
+   * @param location The location to determine the validity of.
+   * @return {@code true} if the location is valid, and {@code false} otherwise.
+   */
+  static boolean isValidLocation(Location location) {
+    if (location != null) {
+      long locationTimeMs = location.getTime();
+      long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs;
+      if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) {
+        LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs);
+        return false;
+      }
+      if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) {
+        LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy());
+        return false;
+      }
+      return true;
+    }
+    LogUtil.i("LocationHelper.isValidLocation", "no location");
+    return false;
+  }
+
+  @MainThread
+  void addLocationListener(LocationListener listener) {
+    Assert.isMainThread();
+    listeners.add(listener);
+  }
+
+  @MainThread
+  void removeLocationListener(LocationListener listener) {
+    Assert.isMainThread();
+    listeners.remove(listener);
+  }
+
+  @MainThread
+  void close() {
+    Assert.isMainThread();
+    LogUtil.enterBlock("LocationHelper.close");
+    listeners.clear();
+
+    if (locationHelperInternal != null) {
+      locationHelperInternal.close();
+    }
+  }
+
+  @MainThread
+  void onLocationChanged(Location location, boolean isConnected) {
+    Assert.isMainThread();
+    LogUtil.i("LocationHelper.onLocationChanged", "location: " + location);
+
+    for (LocationListener listener : listeners) {
+      listener.onLocationChanged(location);
+    }
+  }
+
+  /**
+   * This class contains all the asynchronous callbacks. It only posts location changes back to the
+   * outer class on the main thread.
+   */
+  private class LocationHelperInternal
+      implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
+
+    private final GoogleApiClient apiClient;
+    private final ConnectivityManager connectivityManager;
+    private final Handler mainThreadHandler = new Handler();
+
+    @MainThread
+    LocationHelperInternal(Context context) {
+      Assert.isMainThread();
+      apiClient =
+          new GoogleApiClient.Builder(context)
+              .addApi(LocationServices.API)
+              .addConnectionCallbacks(this)
+              .addOnConnectionFailedListener(this)
+              .build();
+
+      LogUtil.i("LocationHelperInternal", "Connecting to location service...");
+      apiClient.connect();
+
+      connectivityManager =
+          (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    void close() {
+      if (apiClient.isConnected()) {
+        LogUtil.i("LocationHelperInternal", "disconnecting");
+        LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this);
+        apiClient.disconnect();
+      }
+    }
+
+    @Override
+    public void onConnected(Bundle bundle) {
+      LogUtil.enterBlock("LocationHelperInternal.onConnected");
+      LocationRequest locationRequest =
+          LocationRequest.create()
+              .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
+              .setInterval(MIN_UPDATE_INTERVAL_MS)
+              .setFastestInterval(MIN_UPDATE_INTERVAL_MS);
+
+      LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this)
+          .setResultCallback(
+              new ResultCallback<Status>() {
+                @Override
+                public void onResult(Status status) {
+                  if (status.getStatus().isSuccess()) {
+                    onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient));
+                  }
+                }
+              });
+    }
+
+    @Override
+    public void onConnectionSuspended(int i) {
+      // Do nothing.
+    }
+
+    @Override
+    public void onConnectionFailed(ConnectionResult result) {
+      // Do nothing.
+    }
+
+    @Override
+    public void onLocationChanged(Location location) {
+      // Post new location on main thread
+      mainThreadHandler.post(
+          new Runnable() {
+            @Override
+            public void run() {
+              LocationHelper.this.onLocationChanged(location, isConnected());
+            }
+          });
+    }
+
+    /** @return Whether the phone is connected to data. */
+    private boolean isConnected() {
+      if (connectivityManager == null) {
+        return false;
+      }
+      NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+      return networkInfo != null && networkInfo.isConnectedOrConnecting();
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
new file mode 100644
index 0000000..a56fd3b
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java
@@ -0,0 +1,98 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.google.android.gms.location.LocationListener;
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+
+/**
+ * Presenter for the {@code LocationFragment}.
+ *
+ * <p>Performs lookup for the address and map image to show.
+ */
+public class LocationPresenter extends Presenter<LocationPresenter.LocationUi>
+    implements LocationListener {
+
+  private Location mLastLocation;
+  private AsyncTask mDownloadMapTask;
+  private AsyncTask mReverseGeocodeTask;
+
+  LocationPresenter() {}
+
+  @Override
+  public void onUiReady(LocationUi ui) {
+    LogUtil.i("LocationPresenter.onUiReady", "");
+    super.onUiReady(ui);
+    updateLocation(mLastLocation, true);
+  }
+
+  @Override
+  public void onUiUnready(LocationUi ui) {
+    LogUtil.i("LocationPresenter.onUiUnready", "");
+    super.onUiUnready(ui);
+
+    if (mDownloadMapTask != null) {
+      mDownloadMapTask.cancel(true);
+    }
+    if (mReverseGeocodeTask != null) {
+      mReverseGeocodeTask.cancel(true);
+    }
+  }
+
+  @Override
+  public void onLocationChanged(Location location) {
+    LogUtil.i("LocationPresenter.onLocationChanged", "");
+    updateLocation(location, false);
+  }
+
+  private void updateLocation(Location location, boolean forceUpdate) {
+    LogUtil.i("LocationPresenter.updateLocation", "location: " + location);
+    if (forceUpdate || !Objects.equals(mLastLocation, location)) {
+      mLastLocation = location;
+      if (LocationHelper.isValidLocation(location)) {
+        LocationUi ui = getUi();
+        mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location);
+        mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location);
+        if (ui != null) {
+          ui.setLocation(location);
+        } else {
+          LogUtil.i("LocationPresenter.updateLocation", "no Ui");
+        }
+      }
+    }
+  }
+
+  /** UI interface */
+  public interface LocationUi extends Ui {
+
+    void setAddress(String address);
+
+    void setMap(Drawable mapImage);
+
+    void setLocation(Location location);
+
+    Context getContext();
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
new file mode 100644
index 0000000..a57bdf6
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java
@@ -0,0 +1,177 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import java.util.Locale;
+
+class LocationUrlBuilder {
+
+  // Static Map API path constants.
+  private static final String HTTPS_SCHEME = "https";
+  private static final String MAPS_API_DOMAIN = "maps.googleapis.com";
+  private static final String MAPS_PATH = "maps";
+  private static final String API_PATH = "api";
+  private static final String STATIC_MAP_PATH = "staticmap";
+  private static final String GEOCODE_PATH = "geocode";
+  private static final String GEOCODE_OUTPUT_TYPE = "json";
+
+  // Static Map API parameter constants.
+  private static final String KEY_PARAM_KEY = "key";
+  private static final String CENTER_PARAM_KEY = "center";
+  private static final String ZOOM_PARAM_KEY = "zoom";
+  private static final String SCALE_PARAM_KEY = "scale";
+  private static final String SIZE_PARAM_KEY = "size";
+  private static final String MARKERS_PARAM_KEY = "markers";
+
+  private static final String ZOOM_PARAM_VALUE = Integer.toString(16);
+
+  private static final String LAT_LONG_DELIMITER = ",";
+
+  private static final String MARKER_DELIMITER = "|";
+  private static final String MARKER_STYLE_DELIMITER = ":";
+  private static final String MARKER_STYLE_COLOR = "color";
+  private static final String MARKER_STYLE_COLOR_RED = "red";
+
+  private static final String LAT_LNG_PARAM_KEY = "latlng";
+
+  private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060";
+  private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI";
+
+  /**
+   * Generates the URL to a static map image for the given location.
+   *
+   * <p>This image has the following characteristics:
+   *
+   * <p>- It is centered at the given latitude and longitutde. - It is scaled according to the
+   * device's pixel density. - There is a red marker at the given latitude and longitude.
+   *
+   * <p>Source: https://developers.google.com/maps/documentation/staticmaps/
+   *
+   * @param contxt The context.
+   * @param Location A location.
+   * @return The URL of a static map image url of the given location.
+   */
+  public static String getStaticMapUrl(Context context, Location location) {
+    final Uri.Builder builder = new Uri.Builder();
+    Resources res = context.getResources();
+    String size =
+        res.getDimensionPixelSize(R.dimen.location_map_width)
+            + "x"
+            + res.getDimensionPixelSize(R.dimen.location_map_height);
+
+    builder
+        .scheme(HTTPS_SCHEME)
+        .authority(MAPS_API_DOMAIN)
+        .appendPath(MAPS_PATH)
+        .appendPath(API_PATH)
+        .appendPath(STATIC_MAP_PATH)
+        .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location))
+        .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE)
+        .appendQueryParameter(SIZE_PARAM_KEY, size)
+        .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density))
+        .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location))
+        .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE);
+
+    return builder.build().toString();
+  }
+
+  /**
+   * Generates the URL for a request to reverse geocode the given location.
+   *
+   * <p>Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+   *
+   * @param Location A location.
+   */
+  public static String getReverseGeocodeUrl(Location location) {
+    final Uri.Builder builder = new Uri.Builder();
+
+    builder
+        .scheme(HTTPS_SCHEME)
+        .authority(MAPS_API_DOMAIN)
+        .appendPath(MAPS_PATH)
+        .appendPath(API_PATH)
+        .appendPath(GEOCODE_PATH)
+        .appendPath(GEOCODE_OUTPUT_TYPE)
+        .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location))
+        .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE);
+
+    return builder.build().toString();
+  }
+
+  public static Intent getShowMapIntent(
+      Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) {
+
+    String latLong = getFormattedLatLng(location);
+    String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong);
+
+    // Add a map label
+    if (addressLine1 != null) {
+      if (addressLine2 != null) {
+        url +=
+            String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString());
+      } else {
+        url += String.format(Locale.US, "(%s)", addressLine1.toString());
+      }
+    } else {
+      // TODO: i18n
+      url +=
+          String.format(
+              Locale.US,
+              "(Latitude: %f, Longitude: %f)",
+              location.getLatitude(),
+              location.getLongitude());
+    }
+
+    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+    intent.setPackage("com.google.android.apps.maps");
+    return intent;
+  }
+
+  /**
+   * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter
+   * value.
+   *
+   * @param location A location.
+   * @return The comma-separated latitude and longitude pair of that location.
+   */
+  @VisibleForTesting
+  static String getFormattedLatLng(Location location) {
+    return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude();
+  }
+
+  /**
+   * Returns the URL parameter value for the marker, specifying its style and position.
+   *
+   * @param location A location.
+   * @return The URL parameter value for the marker.
+   */
+  @VisibleForTesting
+  static String getMarkerUrlParamValue(Location location) {
+    return MARKER_STYLE_COLOR
+        + MARKER_STYLE_DELIMITER
+        + MARKER_STYLE_COLOR_RED
+        + MARKER_DELIMITER
+        + getFormattedLatLng(location);
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
new file mode 100644
index 0000000..eb5957b
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java
@@ -0,0 +1,144 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+import android.location.Location;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi;
+import java.lang.ref.WeakReference;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+class ReverseGeocodeTask extends AsyncTask<Location, Void, String> {
+
+  // Below are the JSON keys for the reverse geocode response.
+  // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
+  private static final String JSON_KEY_RESULTS = "results";
+  private static final String JSON_KEY_ADDRESS = "formatted_address";
+  private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components";
+  private static final String JSON_KEY_PREMISE = "premise";
+  private static final String JSON_KEY_TYPES = "types";
+  private static final String JSON_KEY_LONG_NAME = "long_name";
+  private static final String JSON_KEY_SHORT_NAME = "short_name";
+
+  private WeakReference<LocationUi> mUiReference;
+
+  public ReverseGeocodeTask(WeakReference<LocationUi> uiReference) {
+    mUiReference = uiReference;
+  }
+
+  @Override
+  protected String doInBackground(Location... locations) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return null;
+    }
+    if (locations == null || locations.length == 0) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided");
+      return null;
+    }
+
+    try {
+      String address = null;
+      String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]);
+
+      TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG);
+      String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url);
+
+      // Parse the JSON response for the formatted address of the first result.
+      JSONObject responseObject = new JSONObject(jsonResponse);
+      if (responseObject != null) {
+        JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS);
+        if (results != null && results.length() > 0) {
+          JSONObject topResult = results.optJSONObject(0);
+          if (topResult != null) {
+            address = topResult.getString(JSON_KEY_ADDRESS);
+
+            // Strip off the Premise component from the address, if present.
+            JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS);
+            if (components != null) {
+              boolean stripped = false;
+              for (int i = 0; !stripped && i < components.length(); i++) {
+                JSONObject component = components.optJSONObject(i);
+                JSONArray types = component.optJSONArray(JSON_KEY_TYPES);
+                if (types != null) {
+                  for (int j = 0; !stripped && j < types.length(); j++) {
+                    if (JSON_KEY_PREMISE.equals(types.getString(j))) {
+                      String premise = null;
+                      if (component.has(JSON_KEY_SHORT_NAME)
+                          && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) {
+                        premise = component.getString(JSON_KEY_SHORT_NAME);
+                      } else if (component.has(JSON_KEY_LONG_NAME)
+                          && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) {
+                        premise = component.getString(JSON_KEY_SHORT_NAME);
+                      }
+                      if (premise != null) {
+                        int index = address.indexOf(',', premise.length());
+                        if (index > 0 && index < address.length()) {
+                          address = address.substring(index + 1).trim();
+                        }
+                        stripped = true;
+                        break;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+
+            // Strip off the country, if its USA.  Note: unfortunately the country in the formatted
+            // address field doesn't match the country in the address component fields (USA != US)
+            // so we can't easily strip off the country for all cases, thus this hack.
+            if (address.endsWith(", USA")) {
+              address = address.substring(0, address.length() - 5);
+            }
+          }
+        }
+      }
+
+      return address;
+    } catch (AuthException ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex);
+      return null;
+    } catch (JSONException ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex);
+      return null;
+    } catch (Exception ex) {
+      LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex);
+      return null;
+    } finally {
+      TrafficStats.clearThreadStatsTag();
+    }
+  }
+
+  @Override
+  protected void onPostExecute(String address) {
+    LocationUi ui = mUiReference.get();
+    if (ui == null) {
+      return;
+    }
+
+    try {
+      ui.setAddress(address);
+    } catch (Exception ex) {
+      LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex);
+    }
+  }
+}
diff --git a/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
new file mode 100644
index 0000000..02cc2e0
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java
@@ -0,0 +1,29 @@
+/*
+ * 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.incallui.calllocation.impl;
+
+/** Constants used for logging */
+public class TrafficStatsTags {
+
+  /**
+   * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to
+   * respect the namespace of the tags in ContactsCommon.
+   */
+  public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000;
+
+  public static final int REVERSE_GEOCODE_TAG = 0xd001;
+}
diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
new file mode 100644
index 0000000..a6bd075
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+~ Copyright (C) 2015 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~      http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License
+-->
+
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:id="@+id/location_view_animator"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:layout_marginTop="16dp"
+  android:layout_marginBottom="16dp"
+  android:background="@android:color/white"
+  android:elevation="2dp"
+  android:inAnimation="@android:anim/fade_in"
+  android:measureAllChildren="true"
+  android:outAnimation="@android:anim/fade_out">
+
+  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/location_loading_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_vertical"
+    android:orientation="vertical">
+
+    <ProgressBar
+      android:id="@+id/location_loading_spinner"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="28dp"
+      android:layout_marginBottom="12dp"
+      android:layout_gravity="center_horizontal"/>
+
+    <TextView
+      android:id="@+id/location_loading_text"
+      style="@style/LocationLoadingTextStyle"
+      android:layout_width="match_parent"
+      android:layout_height="24sp"
+      android:layout_marginBottom="20dp"
+      android:layout_marginStart="24dp"
+      android:layout_marginEnd="24dp"
+      android:gravity="center"
+      android:text="@string/location_loading"/>
+
+  </LinearLayout>
+
+  <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/location_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:columnCount="2"
+    android:orientation="horizontal">
+
+    <TextView
+      android:id="@+id/location_address_title"
+      style="@style/LocationAddressTitleTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="20sp"
+      android:layout_marginTop="16dp"
+      android:layout_marginBottom="4dp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:text="@string/location_title"/>
+
+    <ImageView
+      android:id="@+id/location_map"
+      android:layout_width="@dimen/location_map_width"
+      android:layout_height="@dimen/location_map_height"
+      android:layout_margin="16dp"
+      android:layout_gravity="end|center_vertical"
+      android:layout_rowSpan="4"
+      android:contentDescription="@string/location_map_description"
+      android:scaleType="centerCrop"
+      android:visibility="invisible"
+      tools:src="?android:colorPrimaryDark"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/address_line_one"
+      style="@style/LocationAddressTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="1600 Amphitheatre Pkwy And a bit"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/address_line_two"
+      style="@style/LocationAddressTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="Mountain View, CA 94043"
+      tools:visibility="visible"/>
+
+    <TextView
+      android:id="@+id/lat_long_line"
+      style="@style/LocationLatLongTextStyle"
+      android:layout_width="0dp"
+      android:layout_height="24sp"
+      android:layout_marginBottom="12dp"
+      android:layout_marginStart="16dp"
+      android:layout_columnWeight="1"
+      android:ellipsize="end"
+      android:lines="1"
+      android:visibility="invisible"
+      tools:text="Lat: 37.421719, Long: -122.085297"
+      tools:visibility="visible"/>
+
+  </GridLayout>
+
+</ViewAnimator>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
new file mode 100644
index 0000000..1f41816
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<resources>
+  <dimen name="location_map_width">92dp</dimen>
+  <dimen name="location_map_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
new file mode 100644
index 0000000..ef7c162
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+  <!-- Description for location map shown during emergency calls. [CHAR LIMIT=NONE] -->
+  <string name="location_map_description">Emergency Location Map</string>
+
+  <!-- Label for the address and map shown during emergency calls. [CHAR LIMIT=20] -->
+  <string name="location_title">You are here</string>
+
+  <string name="lat_long_format"><xliff:g id="latitude">%f</xliff:g>, <xliff:g id="longitude">%f</xliff:g></string>
+
+  <!-- Progress indicator loading text. [CHAR LIMIT=20] -->
+  <string name="location_loading">Finding your location</string>
+
+</resources>
diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
new file mode 100644
index 0000000..866a4ed
--- /dev/null
+++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+<resources>
+
+  <style name="LocationAddressTitleTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif-medium</item>
+  </style>
+
+  <style name="LocationAddressTextStyle">
+    <item name="android:textSize">16sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+
+  <style name="LocationLatLongTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#88000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+
+  <style name="LocationLoadingTextStyle">
+    <item name="android:textSize">14sp</item>
+    <item name="android:textColor">#dd000000</item>
+    <item name="android:fontFamily">sans-serif</item>
+  </style>
+</resources>
diff --git a/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
new file mode 100644
index 0000000..fc198c7
--- /dev/null
+++ b/java/com/android/incallui/calllocation/stub/StubCallLocationModule.java
@@ -0,0 +1,54 @@
+/*
+ * 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.incallui.calllocation.stub;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.calllocation.CallLocation;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+
+/** This module provides an instance of call location. */
+@Module
+public abstract class StubCallLocationModule {
+
+  @Binds
+  public abstract CallLocation bindCallLocation(StubCallLocation callLocation);
+
+  static public class StubCallLocation implements CallLocation {
+    @Inject
+    public StubCallLocation() {}
+
+    @Override
+    public boolean canGetLocation(@NonNull Context context) {
+      return false;
+    }
+
+    @Override
+    @NonNull
+    public Fragment getLocationFragment(@NonNull Context context) {
+      return null;
+    }
+
+    @Override
+    public void close() {
+    }
+  }
+}
diff --git a/java/com/android/incallui/commontheme/res/anim/blinking.xml b/java/com/android/incallui/commontheme/res/anim/blinking.xml
new file mode 100644
index 0000000..aaec18c
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/anim/blinking.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+  <alpha
+    android:duration="@android:integer/config_mediumAnimTime"
+    android:fromAlpha="0.0"
+    android:interpolator="@android:anim/linear_interpolator"
+    android:repeatCount="infinite"
+    android:repeatMode="reverse"
+    android:toAlpha="1.0"/>
+</set>
\ No newline at end of file
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
index aaf7e82..6ddce45 100644
--- a/java/com/android/incallui/contactgrid/BottomRow.java
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -31,10 +31,10 @@
  * Gets the content of the bottom row. For example:
  *
  * <ul>
- * <li>Mobile +1 (650) 253-0000
- * <li>[HD icon] 00:15
- * <li>Call ended
- * <li>Hanging up
+ *   <li>Mobile +1 (650) 253-0000
+ *   <li>[HD attempting icon]/[HD icon] 00:15
+ *   <li>Call ended
+ *   <li>Hanging up
  * </ul>
  */
 public class BottomRow {
@@ -45,6 +45,7 @@
     @Nullable public final CharSequence label;
     public final boolean isTimerVisible;
     public final boolean isWorkIconVisible;
+    public final boolean isHdAttemptinIconVisible;
     public final boolean isHdIconVisible;
     public final boolean isForwardIconVisible;
     public final boolean isSpamIconVisible;
@@ -54,6 +55,7 @@
         @Nullable CharSequence label,
         boolean isTimerVisible,
         boolean isWorkIconVisible,
+        boolean isHdAttemptinIconVisible,
         boolean isHdIconVisible,
         boolean isForwardIconVisible,
         boolean isSpamIconVisible,
@@ -61,6 +63,7 @@
       this.label = label;
       this.isTimerVisible = isTimerVisible;
       this.isWorkIconVisible = isWorkIconVisible;
+      this.isHdAttemptinIconVisible = isHdAttemptinIconVisible;
       this.isHdIconVisible = isHdIconVisible;
       this.isForwardIconVisible = isForwardIconVisible;
       this.isSpamIconVisible = isSpamIconVisible;
@@ -76,6 +79,7 @@
     boolean isForwardIconVisible = state.isForwardedNumber;
     boolean isWorkIconVisible = state.isWorkCall;
     boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+    boolean isHdAttemptingIconVisible = state.isHdAttempting;
     boolean isSpamIconVisible = false;
     boolean shouldPopulateAccessibilityEvent = true;
 
@@ -110,6 +114,7 @@
         label,
         isTimerVisible,
         isWorkIconVisible,
+        isHdAttemptingIconVisible,
         isHdIconVisible,
         isForwardIconVisible,
         isSpamIconVisible,
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
index 81c2251..a0b687c 100644
--- a/java/com/android/incallui/contactgrid/ContactGridManager.java
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -18,10 +18,14 @@
 
 import android.content.Context;
 import android.os.SystemClock;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
 import android.widget.Chronometer;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -56,7 +60,7 @@
   @Nullable private ImageView avatarImageView;
 
   // Row 2: Mobile +1 (650) 253-0000
-  // Row 2: [HD icon] 00:15
+  // Row 2: [HD attempting icon]/[HD icon] 00:15
   // Row 2: Call ended
   // Row 2: Hanging up
   // Row 2: [Alert sign] Suspected spam caller
@@ -77,7 +81,6 @@
   private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
   private final LetterTileDrawable letterTile;
 
-
   public ContactGridManager(
       View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
     context = view.getContext();
@@ -227,6 +230,24 @@
   }
 
   /**
+   * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state.
+   *
+   * <p>If no special state is detected, yields TYPE_DEFAULT.
+   */
+  private static @LetterTileDrawable.ContactType int getContactTypeForPrimaryCallState(
+      @NonNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo) {
+    if (callState.isVoiceMailNumber) {
+      return LetterTileDrawable.TYPE_VOICEMAIL;
+    } else if (callState.isBusinessNumber) {
+      return LetterTileDrawable.TYPE_BUSINESS;
+    } else if (primaryInfo.numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
+      return LetterTileDrawable.TYPE_GENERIC_AVATAR;
+    } else {
+      return LetterTileDrawable.TYPE_DEFAULT;
+    }
+  }
+
+  /**
    * Updates row 1. For example:
    *
    * <ul>
@@ -255,7 +276,7 @@
     if (avatarImageView != null) {
       if (hideAvatar) {
         avatarImageView.setVisibility(View.GONE);
-      } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+      } else if (avatarSize > 0 && updateAvatarVisibility()) {
         boolean hasPhoto =
             primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
         // Contact has a photo, don't render a letter tile.
@@ -265,27 +286,29 @@
                   context, primaryInfo.photo, avatarSize, avatarSize));
           // Contact has a name, that isn't a number.
         } else {
-          int contactType =
-              primaryCallState.isVoiceMailNumber
-                  ? LetterTileDrawable.TYPE_VOICEMAIL
-                  : LetterTileDrawable.TYPE_DEFAULT;
           letterTile.setCanonicalDialerLetterTileDetails(
               primaryInfo.name,
               primaryInfo.contactInfoLookupKey,
               LetterTileDrawable.SHAPE_CIRCLE,
-              contactType);
+              getContactTypeForPrimaryCallState(primaryCallState, primaryInfo));
+
+          // By invalidating the avatarImageView we force a redraw of the letter tile.
+          // This is required to properly display the updated letter tile iconography based on the
+          // contact type, because the background drawable reference cached in the view, and the
+          // view is not aware of the mutations made to the background.
+          avatarImageView.invalidate();
           avatarImageView.setBackground(letterTile);
+        }
       }
     }
   }
-  }
 
   /**
    * Updates row 2. For example:
    *
    * <ul>
    *   <li>Mobile +1 (650) 253-0000
-   *   <li>[HD icon] 00:15
+   *   <li>[HD attempting icon]/[HD icon] 00:15
    *   <li>Call ended
    *   <li>Hanging up
    * </ul>
@@ -296,7 +319,15 @@
     bottomTextView.setText(info.label);
     bottomTextView.setAllCaps(info.isSpamIconVisible);
     workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
-    hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+    boolean wasHdIconVisible = hdIconImageView.getVisibility() == View.VISIBLE;
+    if (!wasHdIconVisible && info.isHdAttemptinIconVisible) {
+      Animation animation = AnimationUtils.loadAnimation(context, R.anim.blinking);
+      hdIconImageView.startAnimation(animation);
+    } else if (wasHdIconVisible && !info.isHdAttemptinIconVisible) {
+      hdIconImageView.clearAnimation();
+    }
+    hdIconImageView.setVisibility(
+        info.isHdIconVisible || info.isHdAttemptinIconVisible ? View.VISIBLE : View.GONE);
     forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
     spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
 
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
index a340fd0..ecd5eea 100644
--- a/java/com/android/incallui/contactgrid/TopRow.java
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -21,10 +21,10 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import com.android.dialer.common.Assert;
-import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.State;
 import com.android.incallui.call.VideoUtils;
 import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.videotech.VideoTech;
 
 /**
  * Gets the content of the top row. For example:
@@ -95,7 +95,7 @@
   }
 
   private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
-    if (VideoUtils.isVideoCall(state.videoState)) {
+    if (state.isVideoCall) {
       return getLabelForIncomingVideo(context, state.isWifi);
     } else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
       return state.connectionLabel;
@@ -120,7 +120,7 @@
     if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
       return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
     } else {
-      if (VideoUtils.isVideoCall(state.videoState)) {
+      if (state.isVideoCall) {
         if (state.isWifi) {
           return context.getString(R.string.incall_wifi_video_call_requesting);
         } else {
@@ -144,18 +144,18 @@
 
   private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
     switch (state.sessionModificationState) {
-      case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+      case VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
         return context.getString(R.string.incall_video_call_requesting);
-      case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
-      case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+      case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+      case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
         return context.getString(R.string.incall_video_call_request_failed);
-      case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+      case VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
         return context.getString(R.string.incall_video_call_request_rejected);
-      case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+      case VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
         return context.getString(R.string.incall_video_call_request_timed_out);
-      case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+      case VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
         return getLabelForIncomingVideo(context, state.isWifi);
-      case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+      case VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST:
       default:
         Assert.fail();
         return null;
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
index 3900be5..b7a3fe7 100644
--- a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -4,8 +4,8 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
-  android:orientation="horizontal"
   android:gravity="center_horizontal"
+  android:orientation="horizontal"
   tools:showIn="@layout/incall_contact_grid">
   <ImageView
     android:id="@id/contactgrid_workIcon"
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
index c213af5..6128ae5 100644
--- a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -39,7 +39,6 @@
       style="@style/Dialer.Incall.TextAppearance"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
-      android:textAllCaps="true"
       android:textColor="@android:color/white"
       android:text="@string/incall_on_hold"/>
   </LinearLayout>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
deleted file mode 100644
index addebc4..0000000
--- a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
+++ /dev/null
@@ -1,135 +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.incallui.incall.impl;
-
-import javax.annotation.Generated;
-
-@Generated("com.google.auto.value.processor.AutoValueProcessor")
- final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
-
-  private final int slot;
-  private final int slotOrder;
-  private final int conflictOrder;
-
-  private AutoValue_MappedButtonConfig_MappingInfo(
-      int slot,
-      int slotOrder,
-      int conflictOrder) {
-    this.slot = slot;
-    this.slotOrder = slotOrder;
-    this.conflictOrder = conflictOrder;
-  }
-
-  @Override
-  public int getSlot() {
-    return slot;
-  }
-
-  @Override
-  public int getSlotOrder() {
-    return slotOrder;
-  }
-
-  @Override
-  public int getConflictOrder() {
-    return conflictOrder;
-  }
-
-  @Override
-  public String toString() {
-    return "MappingInfo{"
-        + "slot=" + slot + ", "
-        + "slotOrder=" + slotOrder + ", "
-        + "conflictOrder=" + conflictOrder
-        + "}";
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o == this) {
-      return true;
-    }
-    if (o instanceof MappedButtonConfig.MappingInfo) {
-      MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
-      return (this.slot == that.getSlot())
-           && (this.slotOrder == that.getSlotOrder())
-           && (this.conflictOrder == that.getConflictOrder());
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    int h = 1;
-    h *= 1000003;
-    h ^= this.slot;
-    h *= 1000003;
-    h ^= this.slotOrder;
-    h *= 1000003;
-    h ^= this.conflictOrder;
-    return h;
-  }
-
-  static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
-    private Integer slot;
-    private Integer slotOrder;
-    private Integer conflictOrder;
-    Builder() {
-    }
-    private Builder(MappedButtonConfig.MappingInfo source) {
-      this.slot = source.getSlot();
-      this.slotOrder = source.getSlotOrder();
-      this.conflictOrder = source.getConflictOrder();
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
-      this.slot = slot;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
-      this.slotOrder = slotOrder;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
-      this.conflictOrder = conflictOrder;
-      return this;
-    }
-    @Override
-    public MappedButtonConfig.MappingInfo build() {
-      String missing = "";
-      if (this.slot == null) {
-        missing += " slot";
-      }
-      if (this.slotOrder == null) {
-        missing += " slotOrder";
-      }
-      if (this.conflictOrder == null) {
-        missing += " conflictOrder";
-      }
-      if (!missing.isEmpty()) {
-        throw new IllegalStateException("Missing required properties:" + missing);
-      }
-      return new AutoValue_MappedButtonConfig_MappingInfo(
-          this.slot,
-          this.slotOrder,
-          this.conflictOrder);
-    }
-  }
-
-}
diff --git a/java/com/android/incallui/incall/impl/FakeDragAnimation.java b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
new file mode 100644
index 0000000..c84c3c4
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/FakeDragAnimation.java
@@ -0,0 +1,62 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+
+/**
+ * An animation that controls the fake drag of a {@link ViewPager}. See {@link
+ * ViewPager#fakeDragBy(float)} for more details.
+ */
+public class FakeDragAnimation implements AnimatorUpdateListener {
+
+  /** The view to animate. */
+  private final ViewPager pager;
+
+  private final ValueAnimator animator;
+  private int oldDragPosition;
+
+  public FakeDragAnimation(ViewPager pager) {
+    this.pager = pager;
+    animator = ValueAnimator.ofInt(0, pager.getWidth());
+    animator.addUpdateListener(this);
+    animator.setInterpolator(new FastOutSlowInInterpolator());
+    animator.setDuration(600);
+  }
+
+  public void start() {
+    animator.start();
+  }
+
+  @Override
+  public void onAnimationUpdate(ValueAnimator animation) {
+    if (!pager.isFakeDragging()) {
+      pager.beginFakeDrag();
+    }
+    int dragPosition = (Integer) animation.getAnimatedValue();
+    int dragOffset = dragPosition - oldDragPosition;
+    oldDragPosition = dragPosition;
+    pager.fakeDragBy(-dragOffset);
+
+    if (animation.getAnimatedFraction() == 1) {
+      pager.endFakeDrag();
+    }
+  }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index ef8a1ed..3f31651 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -213,9 +213,7 @@
   @Override
   public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
     LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
-    if (adapter == null) {
-      initAdapter(primaryInfo.multimediaData);
-    }
+    setAdapterMedia(primaryInfo.multimediaData);
     contactGridManager.setPrimary(primaryInfo);
 
     if (primaryInfo.shouldShowLocation) {
@@ -241,9 +239,13 @@
     }
   }
 
-  private void initAdapter(MultimediaData multimediaData) {
-    adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
-    pager.setAdapter(adapter);
+  private void setAdapterMedia(MultimediaData multimediaData) {
+    if (adapter == null) {
+      adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+      pager.setAdapter(adapter);
+    } else {
+      adapter.setAttachments(multimediaData);
+    }
 
     if (adapter.getCount() > 1) {
       tabLayout.setVisibility(pager.getVisibility());
@@ -251,16 +253,13 @@
       if (!stateRestored) {
         new Handler()
             .postDelayed(
-                new Runnable() {
-                  @Override
-                  public void run() {
-                    // In order to prevent user confusion and educate the user on our UI, we animate
-                    // the view pager to the button grid after 2 seconds show them when the UI is
-                    // that they are more familiar with.
-                    pager.setCurrentItem(adapter.getButtonGridPosition());
-                  }
+                () -> {
+                  // In order to prevent user confusion and educate the user on our UI, we animate
+                  // the view pager to the button grid after a short period to show them where the
+                  // UI that they are more familiar with is located.
+                  new FakeDragAnimation(pager).start();
                 },
-                2000);
+                333);
       }
     } else {
       tabLayout.setVisibility(View.GONE);
@@ -479,23 +478,39 @@
 
   @Override
   public boolean isShowingLocationUi() {
-    Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+    Fragment fragment = getLocationFragment();
     return fragment != null && fragment.isVisible();
   }
 
   @Override
   public void showLocationUi(@Nullable Fragment locationUi) {
-    boolean isShowing = isShowingLocationUi();
-    if (!isShowing && locationUi != null) {
+    boolean isVisible = isShowingLocationUi();
+    if (locationUi != null && !isVisible) {
       // Show the location fragment.
       getChildFragmentManager()
           .beginTransaction()
           .replace(R.id.incall_location_holder, locationUi)
           .commitAllowingStateLoss();
-    } else if (isShowing && locationUi == null) {
+    } else if (locationUi == null && isVisible) {
       // Hide the location fragment
-      Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
-      getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+      getChildFragmentManager()
+          .beginTransaction()
+          .remove(getLocationFragment())
+          .commitAllowingStateLoss();
     }
   }
+
+  @Override
+  public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+    super.onMultiWindowModeChanged(isInMultiWindowMode);
+    if (isInMultiWindowMode == isShowingLocationUi()) {
+      LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode);
+      // Need to show or hide location
+      showLocationUi(isInMultiWindowMode ? null : getLocationFragment());
+    }
+  }
+
+  private Fragment getLocationFragment() {
+    return getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+  }
 }
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
index 50eb4c8..2e21835 100644
--- a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -19,17 +19,18 @@
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.PagerAdapter;
 import android.text.TextUtils;
 import com.android.dialer.multimedia.MultimediaData;
 import com.android.incallui.sessiondata.MultimediaFragment;
 
 /** View pager adapter for in call ui. */
-public class InCallPagerAdapter extends FragmentPagerAdapter {
+public class InCallPagerAdapter extends FragmentStatePagerAdapter {
 
-  @Nullable private final MultimediaData attachments;
+  @Nullable private MultimediaData attachments;
 
-  public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+  public InCallPagerAdapter(FragmentManager fragmentManager, @Nullable MultimediaData attachments) {
     super(fragmentManager);
     this.attachments = attachments;
   }
@@ -47,13 +48,27 @@
   @Override
   public int getCount() {
     if (attachments != null
-        && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+        && (!TextUtils.isEmpty(attachments.getText()) || attachments.hasImageData())) {
       return 2;
     }
     return 1;
   }
 
+  public void setAttachments(@Nullable MultimediaData attachments) {
+    if (this.attachments != attachments) {
+      this.attachments = attachments;
+      notifyDataSetChanged();
+    }
+  }
+
   public int getButtonGridPosition() {
     return getCount() - 1;
   }
+
+  //this is called when notifyDataSetChanged() is called
+  @Override
+  public int getItemPosition(Object object) {
+    // refresh all fragments when data set changed
+    return PagerAdapter.POSITION_NONE;
+  }
 }
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
index ecdb5df..7229837 100644
--- a/java/com/android/incallui/incall/impl/MappedButtonConfig.java
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -22,7 +22,7 @@
 import com.android.dialer.common.Assert;
 import com.android.incallui.incall.protocol.InCallButtonIds;
 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
-
+import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -151,7 +151,7 @@
   }
 
   /** Holds information about button mapping. */
-
+  @AutoValue
   abstract static class MappingInfo {
 
     /** The Ui slot into which a given button desires to be placed. */
@@ -179,7 +179,7 @@
     }
 
     /** Class used to build instances of {@link MappingInfo}. */
-
+    @AutoValue.Builder
     abstract static class Builder {
       public abstract Builder setSlot(int slot);
 
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
index 7820908..6e1680b 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -18,15 +18,15 @@
 
 import android.graphics.drawable.Drawable;
 import android.telecom.DisconnectCause;
-import android.telecom.VideoProfile;
 import com.android.incallui.call.DialerCall;
-import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
 import java.util.Locale;
 
 /** State of the primary call. */
 public class PrimaryCallState {
   public final int state;
-  public final int videoState;
+  public final boolean isVideoCall;
   @SessionModificationState public final int sessionModificationState;
   public final DisconnectCause disconnectCause;
   public final String connectionLabel;
@@ -37,19 +37,21 @@
   public final boolean isWifi;
   public final boolean isConference;
   public final boolean isWorkCall;
+  public final boolean isHdAttempting;
   public final boolean isHdAudioCall;
   public final boolean isForwardedNumber;
   public final boolean shouldShowContactPhoto;
   public final long connectTimeMillis;
   public final boolean isVoiceMailNumber;
   public final boolean isRemotelyHeld;
+  public final boolean isBusinessNumber;
 
   // TODO: Convert to autovalue. b/34502119
   public static PrimaryCallState createEmptyPrimaryCallState() {
     return new PrimaryCallState(
         DialerCall.State.IDLE,
-        VideoProfile.STATE_AUDIO_ONLY,
-        DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+        false, /* isVideoCall */
+        VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST,
         new DisconnectCause(DisconnectCause.UNKNOWN),
         null, /* connectionLabel */
         null, /* connectionIcon */
@@ -59,17 +61,19 @@
         false /* isWifi */,
         false /* isConference */,
         false /* isWorkCall */,
+        false /* isHdAttempting */,
         false /* isHdAudioCall */,
         false /* isForwardedNumber */,
         false /* shouldShowContactPhoto */,
         0,
         false /* isVoiceMailNumber */,
-        false /* isRemotelyHeld */);
+        false /* isRemotelyHeld */,
+        false /* isBusinessNumber */);
   }
 
   public PrimaryCallState(
       int state,
-      int videoState,
+      boolean isVideoCall,
       @SessionModificationState int sessionModificationState,
       DisconnectCause disconnectCause,
       String connectionLabel,
@@ -80,14 +84,16 @@
       boolean isWifi,
       boolean isConference,
       boolean isWorkCall,
+      boolean isHdAttempting,
       boolean isHdAudioCall,
       boolean isForwardedNumber,
       boolean shouldShowContactPhoto,
       long connectTimeMillis,
       boolean isVoiceMailNumber,
-      boolean isRemotelyHeld) {
+      boolean isRemotelyHeld,
+      boolean isBusinessNumber) {
     this.state = state;
-    this.videoState = videoState;
+    this.isVideoCall = isVideoCall;
     this.sessionModificationState = sessionModificationState;
     this.disconnectCause = disconnectCause;
     this.connectionLabel = connectionLabel;
@@ -98,12 +104,14 @@
     this.isWifi = isWifi;
     this.isConference = isConference;
     this.isWorkCall = isWorkCall;
+    this.isHdAttempting = isHdAttempting;
     this.isHdAudioCall = isHdAudioCall;
     this.isForwardedNumber = isForwardedNumber;
     this.shouldShowContactPhoto = shouldShowContactPhoto;
     this.connectTimeMillis = connectTimeMillis;
     this.isVoiceMailNumber = isVoiceMailNumber;
     this.isRemotelyHeld = isRemotelyHeld;
+    this.isBusinessNumber = isBusinessNumber;
   }
 
   @Override
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
index 1833ed2..c170950 100644
--- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -41,6 +41,7 @@
   // Used for consistent LetterTile coloring.
   @Nullable public final String contactInfoLookupKey;
   @Nullable public final MultimediaData multimediaData;
+  public final int numberPresentation;
 
   // TODO: Convert to autovalue. b/34502119
   public static PrimaryInfo createEmptyPrimaryInfo() {
@@ -59,7 +60,8 @@
         false,
         false,
         null,
-        null);
+        null,
+        -1);
   }
 
   public PrimaryInfo(
@@ -77,7 +79,8 @@
       boolean answeringDisconnectsOngoingCall,
       boolean shouldShowLocation,
       @Nullable String contactInfoLookupKey,
-      @Nullable MultimediaData multimediaData) {
+      @Nullable MultimediaData multimediaData,
+      int numberPresentation) {
     this.number = number;
     this.name = name;
     this.nameIsNumber = nameIsNumber;
@@ -93,6 +96,7 @@
     this.shouldShowLocation = shouldShowLocation;
     this.contactInfoLookupKey = contactInfoLookupKey;
     this.multimediaData = multimediaData;
+    this.numberPresentation = numberPresentation;
   }
 
   @Override
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/maps/Maps.java
similarity index 64%
copy from java/com/android/incallui/maps/StaticMapFactory.java
copy to java/com/android/incallui/maps/Maps.java
index a350138..648cf9f 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/maps/Maps.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -20,9 +20,14 @@
 import android.support.annotation.NonNull;
 import android.support.v4.app.Fragment;
 
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** Used to create a fragment that can display a static map at the given location. */
+public interface Maps {
+  /**
+   * Used to check if maps is available. This will return false if Dialer was compiled without
+   * support for Google Play Services.
+   */
+  boolean isAvailable();
 
   @NonNull
-  Fragment getStaticMap(@NonNull Location location);
+  Fragment createStaticMapFragment(@NonNull Location location);
 }
diff --git a/java/com/android/incallui/maps/MapsComponent.java b/java/com/android/incallui/maps/MapsComponent.java
new file mode 100644
index 0000000..1ca17b7
--- /dev/null
+++ b/java/com/android/incallui/maps/MapsComponent.java
@@ -0,0 +1,49 @@
+/*
+ * 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.incallui.maps;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+import com.android.incallui.maps.stub.StubMapsModule;
+
+/** Subcomponent that can be used to access the maps implementation. */
+public class MapsComponent {
+
+  private static MapsComponent instance;
+  private Maps maps;
+
+  public Maps getMaps() {
+    if (maps == null) {
+        maps = new StubMapsModule.StubMaps();
+    }
+    return maps;
+  }
+
+  public static MapsComponent get(Context context) {
+    if (instance == null) {
+        instance = new MapsComponent();
+    }
+    return instance;
+  }
+
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    MapsComponent mapsComponent();
+  }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
deleted file mode 100644
index 9d24ef2..0000000
--- a/java/com/android/incallui/maps/StaticMapBinding.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.incallui.maps;
-
-import android.app.Application;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-
-/** Utility for getting a {@link StaticMapFactory} */
-public class StaticMapBinding {
-
-  @Nullable
-  public static StaticMapFactory get(@NonNull Application application) {
-    if (useTestingInstance) {
-      return testingInstance;
-    }
-    if (application instanceof StaticMapFactory) {
-      return ((StaticMapFactory) application);
-    }
-    return null;
-  }
-
-  private static StaticMapFactory testingInstance;
-  private static boolean useTestingInstance;
-
-  @VisibleForTesting
-  public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
-    testingInstance = staticMapFactory;
-    useTestingInstance = true;
-  }
-
-  @VisibleForTesting
-  public static void clearForTesting() {
-    useTestingInstance = false;
-  }
-}
diff --git a/java/com/android/incallui/maps/impl/AndroidManifest.xml b/java/com/android/incallui/maps/impl/AndroidManifest.xml
new file mode 100644
index 0000000..4ad0b3b
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.incallui.maps.impl">
+
+  <application>
+    <meta-data
+      android:name="com.google.android.gms.version"
+      android:value="@integer/google_play_services_version"/>
+  </application>
+</manifest>
diff --git a/java/com/android/incallui/maps/impl/MapsImpl.java b/java/com/android/incallui/maps/impl/MapsImpl.java
new file mode 100644
index 0000000..2cecee9
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/MapsImpl.java
@@ -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
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.incallui.maps.Maps;
+import javax.inject.Inject;
+
+/** Uses Google Play Services APIs to create a static map fragment. */
+final class MapsImpl implements Maps {
+  @Inject
+  public MapsImpl() {}
+
+  @Override
+  public boolean isAvailable() {
+    return true;
+  }
+
+  @Override
+  @NonNull
+  public Fragment createStaticMapFragment(@NonNull Location location) {
+    return StaticMapFragment.newInstance(location);
+  }
+}
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/maps/impl/MapsModule.java
similarity index 60%
copy from java/com/android/incallui/maps/StaticMapFactory.java
copy to java/com/android/incallui/maps/impl/MapsModule.java
index a350138..22f2f32 100644
--- a/java/com/android/incallui/maps/StaticMapFactory.java
+++ b/java/com/android/incallui/maps/impl/MapsModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,15 +14,18 @@
  * limitations under the License
  */
 
-package com.android.incallui.maps;
+package com.android.incallui.maps.impl;
 
-import android.location.Location;
-import android.support.annotation.NonNull;
-import android.support.v4.app.Fragment;
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
 
-/** A Factory that can create Fragments for showing a static map */
-public interface StaticMapFactory {
+/** This module provides an instance of maps. */
+@Module
+public abstract class MapsModule {
 
-  @NonNull
-  Fragment getStaticMap(@NonNull Location location);
+  @Binds
+  @Singleton
+  public abstract Maps bindMaps(MapsImpl maps);
 }
diff --git a/java/com/android/incallui/maps/impl/StaticMapFragment.java b/java/com/android/incallui/maps/impl/StaticMapFragment.java
new file mode 100644
index 0000000..38a4c15
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/StaticMapFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.maps.impl;
+
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+/** Shows a static map centered on a specified location */
+public class StaticMapFragment extends Fragment implements OnMapReadyCallback {
+
+  private static final String ARG_LOCATION = "location";
+
+  public static StaticMapFragment newInstance(@NonNull Location location) {
+    Bundle args = new Bundle();
+    args.putParcelable(ARG_LOCATION, Assert.isNotNull(location));
+    StaticMapFragment fragment = new StaticMapFragment();
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  @Nullable
+  @Override
+  public View onCreateView(
+      LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+    return layoutInflater.inflate(R.layout.static_map_fragment, viewGroup, false);
+  }
+
+  @Override
+  public void onViewCreated(View view, @Nullable Bundle bundle) {
+    super.onViewCreated(view, bundle);
+    SupportMapFragment mapFragment =
+        (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.static_map);
+    if (mapFragment != null) {
+      mapFragment.getMapAsync(this);
+    } else {
+      LogUtil.w("StaticMapFragment.onViewCreated", "No map fragment found!");
+    }
+  }
+
+  @Override
+  public void onMapReady(GoogleMap googleMap) {
+    Location location = getArguments().getParcelable(ARG_LOCATION);
+    LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
+    googleMap.addMarker(new MarkerOptions().position(latLng).flat(true).draggable(false));
+    googleMap.getUiSettings().setMapToolbarEnabled(false);
+    googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f));
+  }
+}
diff --git a/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
new file mode 100644
index 0000000..54f41cb
--- /dev/null
+++ b/java/com/android/incallui/maps/impl/res/layout/static_map_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:map="http://schemas.android.com/apk/res-auto"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent">
+  <fragment
+    android:id="@+id/static_map"
+    class="com.google.android.gms.maps.SupportMapFragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    map:liteMode="true"
+    map:mapType="normal"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/maps/stub/StubMapsModule.java b/java/com/android/incallui/maps/stub/StubMapsModule.java
new file mode 100644
index 0000000..7267814
--- /dev/null
+++ b/java/com/android/incallui/maps/stub/StubMapsModule.java
@@ -0,0 +1,52 @@
+/*
+ * 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.incallui.maps.stub;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.Assert;
+import com.android.incallui.maps.Maps;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Stub for the maps module for build variants that don't support Google Play Services. */
+@Module
+public abstract class StubMapsModule {
+
+  @Binds
+  @Singleton
+  public abstract Maps bindMaps(StubMaps maps);
+
+  static public final class StubMaps implements Maps {
+    @Inject
+    public StubMaps() {}
+
+    @Override
+    public boolean isAvailable() {
+      return false;
+    }
+
+    @NonNull
+    @Override
+    public Fragment createStaticMapFragment(@NonNull Location location) {
+      throw Assert.createUnsupportedOperationFailException();
+    }
+  }
+}
diff --git a/java/com/android/incallui/maps/testing/TestMapsModule.java b/java/com/android/incallui/maps/testing/TestMapsModule.java
new file mode 100644
index 0000000..bb09681
--- /dev/null
+++ b/java/com/android/incallui/maps/testing/TestMapsModule.java
@@ -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
+ */
+
+package com.android.incallui.maps.testing;
+
+import android.support.annotation.Nullable;
+import com.android.incallui.maps.Maps;
+import dagger.Module;
+import dagger.Provides;
+
+/** This module provides a instance of maps for testing. */
+@Module
+public final class TestMapsModule {
+
+  @Nullable private static Maps maps;
+
+  public static void setMaps(@Nullable Maps maps) {
+    TestMapsModule.maps = maps;
+  }
+
+  @Provides
+  static Maps getMaps() {
+    return maps;
+  }
+
+  private TestMapsModule() {}
+}
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
index 252d131..0b95a9c 100644
--- a/java/com/android/incallui/res/values/strings.xml
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -223,17 +223,6 @@
     <item>ABSENTNUMBER</item>
   </string-array>
 
-  <!-- Preference for Voicemail service provider under "Voicemail" settings.
-       [CHAR LIMIT=40] -->
-  <string name="voicemail_provider">Service</string>
-
-  <!-- Preference for Voicemail setting of each provider.
-       [CHAR LIMIT=40] -->
-  <string name="voicemail_settings">Setup</string>
-
-  <!-- String to display in voicemail number summary when no voicemail num is set -->
-  <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
-
   <!-- Title displayed above settings coming after voicemail in the call features screen -->
   <string name="other_settings">Other call settings</string>
 
@@ -242,26 +231,6 @@
   <!--  Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
   <string name="selectContact">select contact</string>
 
-  <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
-  <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
-  <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
-  <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
-
-  <!-- Voicemail ringtone title. The user clicks on this preference to select
-       which sound to play when a voicemail notification is received.
-       [CHAR LIMIT=30] -->
-  <string name="voicemail_notification_ringtone_title">Sound</string>
-
-  <!-- The default value value for voicemail notification. -->
-  <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
-
-  <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
-  <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
-    <item>always</item>
-    <item>silent</item>
-    <item>never</item>
-  </string-array>
-
   <!-- Title for the category "ringtone", which is shown above ringtone and vibration
        related settings.
        [CHAR LIMIT=30] -->
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
index d6f671d..14aa0a3 100644
--- a/java/com/android/incallui/sessiondata/MultimediaFragment.java
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -31,12 +31,10 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
-import com.android.dialer.common.Assert;
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.multimedia.MultimediaData;
-import com.android.incallui.maps.StaticMapBinding;
-import com.android.incallui.maps.StaticMapFactory;
+import com.android.incallui.maps.MapsComponent;
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.load.DataSource;
 import com.bumptech.glide.load.engine.GlideException;
@@ -58,17 +56,13 @@
   private static final String ARG_INTERACTIVE = "interactive";
   private static final String ARG_SHOW_AVATAR = "show_avatar";
   private ImageView avatarImageView;
-  // TODO: add click listeners
-  @SuppressWarnings("unused")
-  private boolean isInteractive;
 
   private boolean showAvatar;
-  private StaticMapFactory mapFactory;
 
   public static MultimediaFragment newInstance(
       @NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
     return newInstance(
-        multimediaData.getSubject(),
+        multimediaData.getText(),
         multimediaData.getImageUri(),
         multimediaData.getLocation(),
         isInteractive,
@@ -96,7 +90,6 @@
   @Override
   public void onCreate(@Nullable Bundle bundle) {
     super.onCreate(bundle);
-    isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
     showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
   }
 
@@ -107,10 +100,7 @@
     boolean hasImage = getImageUri() != null;
     boolean hasSubject = !TextUtils.isEmpty(getSubject());
     boolean hasMap = getLocation() != null;
-    if (hasMap) {
-      mapFactory = StaticMapBinding.get(getActivity().getApplication());
-    }
-    if (mapFactory != null) {
+    if (hasMap && MapsComponent.get(getContext()).getMaps().isAvailable()) {
       if (hasImage) {
         if (hasSubject) {
           return layoutInflater.inflate(
@@ -178,7 +168,7 @@
     if (fragmentHolder != null) {
       fragmentHolder.setClipToOutline(true);
       Fragment mapFragment =
-          Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+          MapsComponent.get(getContext()).getMaps().createStaticMapFragment(getLocation());
       getChildFragmentManager()
           .beginTransaction()
           .replace(R.id.answer_message_frag, mapFragment)
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
index 7000f83..0882781 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -46,5 +46,6 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:id="@+id/loading_spinner"
-    android:layout_centerInParent="true"/>
+    android:layout_centerInParent="true"
+    android:elevation="2dp"/>
 </RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
index 9959f4d..c816418 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -42,6 +42,14 @@
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
 
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
+
   <FrameLayout
     android:id="@id/answer_message_frag"
     android:layout_width="0dp"
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
index 9955654..4e6fcba 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -59,4 +59,12 @@
     android:elevation="@dimen/answer_data_elevation"
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
+
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
 </GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
index 387c5cf..ffbe41b 100644
--- a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -61,6 +61,14 @@
     android:outlineProvider="background"
     android:scaleType="centerCrop"/>
 
+  <ProgressBar
+    android:id="@+id/loading_spinner"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_column="1"
+    android:layout_gravity="center"
+    android:elevation="2dp"/>
+
   <FrameLayout
     android:id="@id/answer_message_frag"
     android:layout_width="0dp"
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
index 0897842..ed0a99e 100644
--- a/java/com/android/incallui/spam/SpamCallListListener.java
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -17,6 +17,7 @@
 package com.android.incallui.spam;
 
 import android.app.Notification;
+import android.app.Notification.Builder;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -33,12 +34,13 @@
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.nano.ContactLookupResult;
 import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.notification.NotificationChannelManager;
+import com.android.dialer.notification.NotificationChannelManager.Channel;
 import com.android.dialer.spam.Spam;
 import com.android.incallui.R;
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.CallHistoryStatus;
-import com.android.incallui.call.DialerCall.SessionModificationState;
 import java.util.Random;
 
 /**
@@ -47,7 +49,7 @@
  */
 public class SpamCallListListener implements CallList.Listener {
 
-  static final int NOTIFICATION_ID = 1;
+  static final int NOTIFICATION_ID = R.id.notification_spam_call;
   private static final String TAG = "SpamCallListListener";
   private final Context context;
   private final Random random;
@@ -87,7 +89,7 @@
   public void onUpgradeToVideo(DialerCall call) {}
 
   @Override
-  public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+  public void onSessionModificationStateChange(DialerCall call) {}
 
   @Override
   public void onCallListChange(CallList callList) {}
@@ -173,13 +175,16 @@
    * Creates a notification builder with properties common among the two after call notifications.
    */
   private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
-    return new Notification.Builder(context)
-        .setContentIntent(
-            createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
-        .setCategory(Notification.CATEGORY_STATUS)
-        .setPriority(Notification.PRIORITY_DEFAULT)
-        .setColor(context.getColor(R.color.dialer_theme_color))
-        .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+    Builder builder =
+        new Builder(context)
+            .setContentIntent(
+                createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+            .setCategory(Notification.CATEGORY_STATUS)
+            .setPriority(Notification.PRIORITY_DEFAULT)
+            .setColor(context.getColor(R.color.dialer_theme_color))
+            .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+    NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null);
+    return builder;
   }
 
   private CharSequence getDisplayNumber(DialerCall call) {
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
index 934ff07..a80a6c7 100644
--- a/java/com/android/incallui/video/bindings/VideoBindings.java
+++ b/java/com/android/incallui/video/bindings/VideoBindings.java
@@ -22,7 +22,7 @@
 /** Bindings for video module. */
 public class VideoBindings {
 
-  public static VideoCallScreen createVideoCallScreen() {
-    return new VideoCallFragment();
+  public static VideoCallScreen createVideoCallScreen(String callId) {
+    return VideoCallFragment.newInstance(callId);
   }
 }
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index 77a67d0..92c8b37 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -32,6 +32,7 @@
 import android.renderscript.ScriptIntrinsicBlur;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentTransaction;
 import android.support.v4.view.animation.FastOutLinearInInterpolator;
@@ -92,6 +93,9 @@
         AudioRouteSelectorPresenter,
         OnSystemUiVisibilityChangeListener {
 
+  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+  static final String ARG_CALL_ID = "call_id";
+
   private static final float BLUR_PREVIEW_RADIUS = 16.0f;
   private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
   private static final float BLUR_REMOTE_RADIUS = 25.0f;
@@ -156,6 +160,15 @@
         }
       };
 
+  public static VideoCallFragment newInstance(String callId) {
+    Bundle bundle = new Bundle();
+    bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+
+    VideoCallFragment instance = new VideoCallFragment();
+    instance.setArguments(bundle);
+    return instance;
+  }
+
   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
@@ -308,6 +321,20 @@
   }
 
   @Override
+  public void onStart() {
+    super.onStart();
+    LogUtil.i("VideoCallFragment.onStart", null);
+    onVideoScreenStart();
+  }
+
+  @Override
+  public void onVideoScreenStart() {
+    inCallButtonUiDelegate.refreshMuteState();
+    videoCallScreenDelegate.onVideoCallScreenUiReady();
+    getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
+  }
+
+  @Override
   public void onResume() {
     super.onResume();
     LogUtil.i("VideoCallFragment.onResume", null);
@@ -315,15 +342,6 @@
   }
 
   @Override
-  public void onStart() {
-    super.onStart();
-    LogUtil.i("VideoCallFragment.onStart", null);
-    inCallButtonUiDelegate.refreshMuteState();
-    videoCallScreenDelegate.onVideoCallScreenUiReady();
-    getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
-  }
-
-  @Override
   public void onPause() {
     super.onPause();
     LogUtil.i("VideoCallFragment.onPause", null);
@@ -333,6 +351,11 @@
   public void onStop() {
     super.onStop();
     LogUtil.i("VideoCallFragment.onStop", null);
+    onVideoScreenStop();
+  }
+
+  @Override
+  public void onVideoScreenStop() {
     getView().removeCallbacks(cameraPermissionDialogRunnable);
     videoCallScreenDelegate.onVideoCallScreenUiUnready();
   }
@@ -721,6 +744,12 @@
   }
 
   @Override
+  @NonNull
+  public String getCallId() {
+    return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+  }
+
+  @Override
   public void showButton(@InCallButtonIds int buttonId, boolean show) {
     LogUtil.v(
         "VideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
index dc663dd..f8c6fc3 100644
--- a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -31,6 +31,7 @@
     android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
     android:drawablePadding="8dp"
     android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+    android:drawableTint="@color/videocall_camera_off_tint"
     android:padding="64dp"
     android:text="@string/videocall_remote_video_off"
     android:textAppearance="@style/Dialer.Incall.TextAppearance"
@@ -43,7 +44,8 @@
     android:layout_height="match_parent"
     android:layout_alignParentBottom="true"
     android:layout_alignParentStart="true"
-    android:background="@color/videocall_overlay_background_color"/>
+    android:background="@color/videocall_overlay_background_color"
+    tools:visibility="gone"/>
 
   <TextureView
     android:id="@+id/videocall_video_preview"
@@ -71,7 +73,8 @@
     android:layout_height="match_parent"
     android:layout_alignParentBottom="true"
     android:layout_alignParentStart="true"
-    android:background="@color/videocall_overlay_background_color"/>
+    android:background="@color/videocall_overlay_background_color"
+    tools:visibility="gone"/>
 
   <ImageView
     android:id="@+id/videocall_video_preview_off_overlay"
@@ -82,7 +85,9 @@
     android:layout_alignRight="@+id/videocall_video_preview"
     android:layout_alignTop="@+id/videocall_video_preview"
     android:scaleType="center"
-    android:src="@drawable/quantum_ic_videocam_off_white_36"
+    android:src="@drawable/quantum_ic_videocam_off_white_24"
+    android:tint="@color/videocall_camera_off_tint"
+    android:tintMode="src_in"
     android:visibility="gone"
     android:importantForAccessibility="no"
     tools:visibility="visible"/>
diff --git a/java/com/android/incallui/video/impl/res/values/colors.xml b/java/com/android/incallui/video/impl/res/values/colors.xml
new file mode 100644
index 0000000..874bf94
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?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>
+  <color name="videocall_camera_off_tint">#89ffffff</color>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
index 0eaf692..bad050c 100644
--- a/java/com/android/incallui/video/protocol/VideoCallScreen.java
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -21,6 +21,10 @@
 /** Interface for call video call module. */
 public interface VideoCallScreen {
 
+  void onVideoScreenStart();
+
+  void onVideoScreenStop();
+
   void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
 
   void onLocalVideoDimensionsChanged();
@@ -33,4 +37,6 @@
       boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
 
   Fragment getVideoCallScreenFragment();
+
+  String getCallId();
 }
diff --git a/java/com/android/incallui/videotech/VideoTech.java b/java/com/android/incallui/videotech/VideoTech.java
new file mode 100644
index 0000000..fb26417
--- /dev/null
+++ b/java/com/android/incallui/videotech/VideoTech.java
@@ -0,0 +1,96 @@
+/*
+ * 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.incallui.videotech;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Video calling interface. */
+public interface VideoTech {
+
+  boolean isAvailable();
+
+  boolean isTransmittingOrReceiving();
+
+  void onCallStateChanged(int newState);
+
+  @SessionModificationState
+  int getSessionModificationState();
+
+  void upgradeToVideo();
+
+  void acceptVideoRequest();
+
+  void acceptVideoRequestAsAudio();
+
+  void declineVideoRequest();
+
+  boolean isTransmitting();
+
+  void stopTransmission();
+
+  void resumeTransmission();
+
+  void pause();
+
+  void unpause();
+
+  void setCamera(String cameraId);
+
+  void setDeviceOrientation(int rotation);
+
+  /** Listener for video call events. */
+  interface VideoTechListener {
+
+    void onVideoTechStateChanged();
+
+    void onSessionModificationStateChanged();
+
+    void onCameraDimensionsChanged(int width, int height);
+
+    void onPeerDimensionsChanged(int width, int height);
+
+    void onVideoUpgradeRequestReceived();
+  }
+
+  /**
+   * Defines different states of session modify requests, which are used to upgrade to video, or
+   * downgrade to audio.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    SESSION_MODIFICATION_STATE_NO_REQUEST,
+    SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+    SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+    SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+    SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+    SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+    SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+  })
+  @interface SessionModificationState {}
+
+  int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+  int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+  int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+  int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+  int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+  int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+  int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+  int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+}
diff --git a/java/com/android/incallui/videotech/empty/EmptyVideoTech.java b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
new file mode 100644
index 0000000..bc8db4c
--- /dev/null
+++ b/java/com/android/incallui/videotech/empty/EmptyVideoTech.java
@@ -0,0 +1,76 @@
+/*
+ * 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.incallui.videotech.empty;
+
+import com.android.incallui.videotech.VideoTech;
+
+/** Default video tech that is always available but doesn't do anything. */
+public class EmptyVideoTech implements VideoTech {
+
+  @Override
+  public boolean isAvailable() {
+    return false;
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return false;
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {}
+
+  @Override
+  public int getSessionModificationState() {
+    return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+  }
+
+  @Override
+  public void upgradeToVideo() {}
+
+  @Override
+  public void acceptVideoRequest() {}
+
+  @Override
+  public void acceptVideoRequestAsAudio() {}
+
+  @Override
+  public void declineVideoRequest() {}
+
+  @Override
+  public boolean isTransmitting() {
+    return false;
+  }
+
+  @Override
+  public void stopTransmission() {}
+
+  @Override
+  public void resumeTransmission() {}
+
+  @Override
+  public void pause() {}
+
+  @Override
+  public void unpause() {}
+
+  @Override
+  public void setCamera(String cameraId) {}
+
+  @Override
+  public void setDeviceOrientation(int rotation) {}
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
new file mode 100644
index 0000000..0a15f7e
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -0,0 +1,201 @@
+/*
+ * 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.incallui.videotech.ims;
+
+import android.os.Handler;
+import android.telecom.Call;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+import com.android.incallui.videotech.VideoTech.SessionModificationState;
+import com.android.incallui.videotech.VideoTech.VideoTechListener;
+
+/** Receives IMS video call state updates. */
+public class ImsVideoCallCallback extends VideoCall.Callback {
+  private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+  private final Handler handler = new Handler();
+  private final Call call;
+  private final ImsVideoTech videoTech;
+  private final VideoTechListener listener;
+  private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+  ImsVideoCallCallback(final Call call, ImsVideoTech videoTech, VideoTechListener listener) {
+    this.call = call;
+    this.videoTech = videoTech;
+    this.listener = listener;
+  }
+
+  @Override
+  public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+    LogUtil.i(
+        "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+
+    int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState());
+    int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState());
+
+    boolean wasVideoCall = VideoProfile.isVideo(previousVideoState);
+    boolean isVideoCall = VideoProfile.isVideo(newVideoState);
+
+    if (wasVideoCall && !isVideoCall) {
+      LogUtil.i(
+          "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
+    } else if (previousVideoState != newVideoState) {
+      requestedVideoState = newVideoState;
+      videoTech.setSessionModificationState(
+          VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+      listener.onVideoUpgradeRequestReceived();
+    }
+  }
+
+  /**
+   * @param status Status of the session modify request. Valid values are {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+   *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+   * @param responseProfile The actual profile changes made by the peer device.
+   */
+  @Override
+  public void onSessionModifyResponseReceived(
+      int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+    LogUtil.i(
+        "ImsVideoCallCallback.onSessionModifyResponseReceived",
+        "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d",
+        status,
+        requestedProfile,
+        responseProfile,
+        videoTech.getSessionModificationState());
+
+    if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+      handler.removeCallbacksAndMessages(null); // Clear everything
+
+      final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
+      if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+        // This will update the video UI to display the error message.
+        videoTech.setSessionModificationState(newSessionModificationState);
+      }
+
+      // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+      // to stay up so that the user can read the error message.
+      //
+      // If the other person accepted the upgrade request then this will keep the video UI up until
+      // the call's video state change. Without this we would switch to the voice call and then
+      // switch back to video UI.
+      handler.postDelayed(
+          () -> {
+            if (videoTech.getSessionModificationState() == newSessionModificationState) {
+              LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+              videoTech.setSessionModificationState(
+                  VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+            } else {
+              LogUtil.i(
+                  "ImsVideoCallCallback.onSessionModifyResponseReceived",
+                  "session modification state has changed, not clearing state");
+            }
+          },
+          CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+    } else if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      videoTech.setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    } else if (videoTech.getSessionModificationState()
+        == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+      videoTech.setSessionModificationState(getSessionModificationStateFromTelecomStatus(status));
+    } else {
+      LogUtil.i(
+          "ImsVideoCallCallback.onSessionModifyResponseReceived",
+          "call is not waiting for response, doing nothing");
+    }
+  }
+
+  @SessionModificationState
+  private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
+    switch (telecomStatus) {
+      case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+        return VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+      case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+      case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+        // Check if it's already video call, which means the request is not video upgrade request.
+        if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
+          return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+        } else {
+          return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+        }
+      case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+        return VideoTech.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+      case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+        return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+      default:
+        LogUtil.e(
+            "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus",
+            "unknown status: %d",
+            telecomStatus);
+        return VideoTech.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+    }
+  }
+
+  @Override
+  public void onCallSessionEvent(int event) {
+    switch (event) {
+      case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure");
+        break;
+      case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready");
+        break;
+      default:
+        LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event);
+        break;
+    }
+  }
+
+  @Override
+  public void onPeerDimensionsChanged(int width, int height) {
+    listener.onPeerDimensionsChanged(width, height);
+  }
+
+  @Override
+  public void onVideoQualityChanged(int videoQuality) {
+    LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality);
+  }
+
+  @Override
+  public void onCallDataUsageChanged(long dataUsage) {
+    LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage);
+  }
+
+  @Override
+  public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+    if (cameraCapabilities != null) {
+      listener.onCameraDimensionsChanged(
+          cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+    }
+  }
+
+  int getRequestedVideoState() {
+    return requestedVideoState;
+  }
+}
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
new file mode 100644
index 0000000..890e5c8
--- /dev/null
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -0,0 +1,212 @@
+/*
+ * 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.incallui.videotech.ims;
+
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videotech.VideoTech;
+
+/** ViLTE implementation */
+public class ImsVideoTech implements VideoTech {
+  private final Call call;
+  private final VideoTechListener listener;
+  private ImsVideoCallCallback callback;
+  private @SessionModificationState int sessionModificationState =
+      VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+  private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+  public ImsVideoTech(VideoTechListener listener, Call call) {
+    this.listener = listener;
+    this.call = call;
+  }
+
+  @Override
+  public boolean isAvailable() {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+      return false;
+    }
+
+    boolean hasCapabilities =
+        call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+            && call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+
+    return call.getVideoCall() != null
+        && (hasCapabilities || VideoProfile.isVideo(call.getDetails().getVideoState()));
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return VideoProfile.isVideo(call.getDetails().getVideoState());
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {
+    if (!isAvailable()) {
+      return;
+    }
+
+    if (callback == null) {
+      callback = new ImsVideoCallCallback(call, this, listener);
+      call.getVideoCall().registerCallback(callback);
+    }
+
+    if (getSessionModificationState()
+            == VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+        && isTransmittingOrReceiving()) {
+      // We don't clear the session modification state right away when we find out the video upgrade
+      // request was accepted to avoid having the UI switch from video to voice to video.
+      // Once the underlying telecom call updates to video mode it's safe to clear the state.
+      LogUtil.i(
+          "ImsVideoTech.onCallStateChanged",
+          "upgraded to video, clearing session modification state");
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+
+    // Determines if a received upgrade to video request should be cancelled. This can happen if
+    // another InCall UI responds to the upgrade to video request.
+    int newVideoState = call.getDetails().getVideoState();
+    if (newVideoState != previousVideoState
+        && sessionModificationState
+            == VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+      LogUtil.i("ImsVideoTech.onCallStateChanged", "cancelling upgrade notification");
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+    previousVideoState = newVideoState;
+  }
+
+  @Override
+  public int getSessionModificationState() {
+    return sessionModificationState;
+  }
+
+  void setSessionModificationState(@SessionModificationState int state) {
+    if (state != sessionModificationState) {
+      LogUtil.i(
+          "ImsVideoTech.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+      sessionModificationState = state;
+      listener.onSessionModificationStateChanged();
+    }
+  }
+
+  @Override
+  public void upgradeToVideo() {
+    LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState | VideoProfile.STATE_BIDIRECTIONAL));
+    setSessionModificationState(
+        VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+  }
+
+  @Override
+  public void acceptVideoRequest() {
+    int requestedVideoState = callback.getRequestedVideoState();
+    Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
+    LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
+    call.getVideoCall().sendSessionModifyResponse(new VideoProfile(requestedVideoState));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void acceptVideoRequestAsAudio() {
+    LogUtil.enterBlock("ImsVideoTech.acceptVideoRequestAsAudio");
+    call.getVideoCall().sendSessionModifyResponse(new VideoProfile(VideoProfile.STATE_AUDIO_ONLY));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void declineVideoRequest() {
+    LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
+    call.getVideoCall()
+        .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public boolean isTransmitting() {
+    return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
+  }
+
+  @Override
+  public void stopTransmission() {
+    LogUtil.enterBlock("ImsVideoTech.stopTransmission");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
+  }
+
+  @Override
+  public void resumeTransmission() {
+    LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
+
+    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+    call.getVideoCall()
+        .sendSessionModifyRequest(
+            new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+  }
+
+  @Override
+  public void pause() {
+    if (canPause()) {
+      LogUtil.i("ImsVideoTech.pause", "sending pause request");
+      int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
+      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
+    } else {
+      LogUtil.i("ImsVideoTech.pause", "not sending request: canPause: %b", canPause());
+    }
+  }
+
+  @Override
+  public void unpause() {
+    if (canPause()) {
+      LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
+      int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
+      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
+    } else {
+      LogUtil.i("ImsVideoTech.unpause", "not sending request: canPause: %b", canPause());
+    }
+  }
+
+  @Override
+  public void setCamera(String cameraId) {
+    call.getVideoCall().setCamera(cameraId);
+    call.getVideoCall().requestCameraCapabilities();
+  }
+
+  @Override
+  public void setDeviceOrientation(int rotation) {
+    call.getVideoCall().setDeviceOrientation(rotation);
+  }
+
+  private boolean canPause() {
+    return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO)
+        && call.getState() == Call.STATE_ACTIVE;
+  }
+
+  static int getUnpausedVideoState(int videoState) {
+    return videoState & (~VideoProfile.STATE_PAUSED);
+  }
+}
diff --git a/java/com/android/incallui/videotech/rcs/RcsVideoShare.java b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
new file mode 100644
index 0000000..2cb4303
--- /dev/null
+++ b/java/com/android/incallui/videotech/rcs/RcsVideoShare.java
@@ -0,0 +1,195 @@
+/*
+ * 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.incallui.videotech.rcs;
+
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.videoshare.VideoShareListener;
+import com.android.incallui.videotech.VideoTech;
+
+/** Allows the in-call UI to make video calls over RCS. */
+public class RcsVideoShare implements VideoTech, CapabilitiesListener, VideoShareListener {
+  private final EnrichedCallManager enrichedCallManager;
+  private final VideoTechListener listener;
+  private final String callingNumber;
+  private int previousCallState = Call.STATE_NEW;
+  private long inviteSessionId = Session.NO_SESSION_ID;
+  private long transmittingSessionId = Session.NO_SESSION_ID;
+  private long receivingSessionId = Session.NO_SESSION_ID;
+
+  private @SessionModificationState int sessionModificationState =
+      VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST;
+
+  public RcsVideoShare(
+      @NonNull EnrichedCallManager enrichedCallManager,
+      @NonNull VideoTechListener listener,
+      @NonNull String callingNumber) {
+    this.enrichedCallManager = Assert.isNotNull(enrichedCallManager);
+    this.listener = Assert.isNotNull(listener);
+    this.callingNumber = Assert.isNotNull(callingNumber);
+
+    enrichedCallManager.registerCapabilitiesListener(this);
+    enrichedCallManager.registerVideoShareListener(this);
+  }
+
+  @Override
+  public boolean isAvailable() {
+    EnrichedCallCapabilities capabilities = enrichedCallManager.getCapabilities(callingNumber);
+    return capabilities != null && capabilities.supportsVideoShare();
+  }
+
+  @Override
+  public boolean isTransmittingOrReceiving() {
+    return transmittingSessionId != Session.NO_SESSION_ID
+        || receivingSessionId != Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void onCallStateChanged(int newState) {
+    if (newState == Call.STATE_DISCONNECTING) {
+      enrichedCallManager.unregisterVideoShareListener(this);
+      enrichedCallManager.unregisterCapabilitiesListener(this);
+    }
+
+    if (newState != previousCallState && newState == Call.STATE_ACTIVE) {
+      // Per spec, request capabilities when the call becomes active
+      enrichedCallManager.requestCapabilities(callingNumber);
+    }
+
+    previousCallState = newState;
+  }
+
+  @Override
+  public int getSessionModificationState() {
+    return sessionModificationState;
+  }
+
+  private void setSessionModificationState(@SessionModificationState int state) {
+    if (state != sessionModificationState) {
+      LogUtil.i(
+          "RcsVideoShare.setSessionModificationState", "%d -> %d", sessionModificationState, state);
+      sessionModificationState = state;
+      listener.onSessionModificationStateChanged();
+    }
+  }
+
+  @Override
+  public void upgradeToVideo() {
+    LogUtil.enterBlock("RcsVideoShare.upgradeToVideo");
+    transmittingSessionId = enrichedCallManager.startVideoShareSession(callingNumber);
+    if (transmittingSessionId != Session.NO_SESSION_ID) {
+      setSessionModificationState(
+          VideoTech.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+    }
+  }
+
+  @Override
+  public void acceptVideoRequest() {
+    LogUtil.enterBlock("RcsVideoShare.acceptVideoRequest");
+    if (enrichedCallManager.acceptVideoShareSession(inviteSessionId)) {
+      receivingSessionId = inviteSessionId;
+    }
+    inviteSessionId = Session.NO_SESSION_ID;
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public void acceptVideoRequestAsAudio() {
+    throw Assert.createUnsupportedOperationFailException();
+  }
+
+  @Override
+  public void declineVideoRequest() {
+    LogUtil.enterBlock("RcsVideoTech.declineUpgradeRequest");
+    enrichedCallManager.endVideoShareSession(
+        enrichedCallManager.getVideoShareInviteSessionId(callingNumber));
+    inviteSessionId = Session.NO_SESSION_ID;
+    setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+  }
+
+  @Override
+  public boolean isTransmitting() {
+    return transmittingSessionId != Session.NO_SESSION_ID;
+  }
+
+  @Override
+  public void stopTransmission() {
+    LogUtil.enterBlock("RcsVideoTech.stopTransmission");
+  }
+
+  @Override
+  public void resumeTransmission() {
+    LogUtil.enterBlock("RcsVideoTech.resumeTransmission");
+  }
+
+  @Override
+  public void pause() {}
+
+  @Override
+  public void unpause() {}
+
+  @Override
+  public void setCamera(String cameraId) {}
+
+  @Override
+  public void setDeviceOrientation(int rotation) {}
+
+  @Override
+  public void onCapabilitiesUpdated() {
+    listener.onVideoTechStateChanged();
+  }
+
+  @Override
+  public void onVideoShareChanged() {
+    long existingInviteSessionId = inviteSessionId;
+
+    inviteSessionId = enrichedCallManager.getVideoShareInviteSessionId(callingNumber);
+    if (inviteSessionId != Session.NO_SESSION_ID) {
+      if (existingInviteSessionId == Session.NO_SESSION_ID) {
+        // This is a new invite
+        setSessionModificationState(
+            VideoTech.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+        listener.onVideoUpgradeRequestReceived();
+      }
+    } else {
+      setSessionModificationState(VideoTech.SESSION_MODIFICATION_STATE_NO_REQUEST);
+    }
+
+    if (sessionIsClosed(transmittingSessionId)) {
+      LogUtil.i("RcsVideoShare.onSessionClosed", "transmitting session closed");
+      transmittingSessionId = Session.NO_SESSION_ID;
+    }
+
+    if (sessionIsClosed(receivingSessionId)) {
+      LogUtil.i("RcsVideoShare.onSessionClosed", "receiving session closed");
+      receivingSessionId = Session.NO_SESSION_ID;
+    }
+
+    listener.onVideoTechStateChanged();
+  }
+
+  private boolean sessionIsClosed(long sessionId) {
+    return sessionId != Session.NO_SESSION_ID
+        && enrichedCallManager.getVideoShareSession(sessionId) == null;
+  }
+}