Merge "Fix for button flickering issue when video call screen rotates"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b24b3de..dad5f4e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -45,6 +45,7 @@
   <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
   <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
   <uses-permission android:name="android.permission.VIBRATE"/>
+  <uses-permission android:name="android.permission.BLUETOOTH"/>
   <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
   <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
   <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
diff --git a/OWNERS b/OWNERS
index 205dc43..42a8443 100644
--- a/OWNERS
+++ b/OWNERS
@@ -3,3 +3,7 @@
 twyen@google.com
 zachh@google.com
 linyuh@google.com
+tgunn@google.com
+hallliu@google.com
+breadley@google.com
+paulye@google.com
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index 7d12d52..2a9600a 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -42,6 +42,7 @@
 import com.android.incallui.call.CallList;
 import com.android.incallui.call.DialerCall;
 import com.android.incallui.call.DialerCall.CameraDirection;
+import com.android.incallui.call.DialerCallListener;
 import com.android.incallui.call.TelecomAdapter;
 import com.android.incallui.call.state.DialerCallState;
 import com.android.incallui.incall.protocol.InCallButtonIds;
@@ -58,17 +59,12 @@
         InCallDetailsListener,
         CanAddCallListener,
         Listener,
-        InCallButtonUiDelegate {
-
-  private static final String KEY_AUTOMATICALLY_MUTED_BY_ADD_CALL =
-      "incall_key_automatically_muted_by_add_call";
-  private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
+        InCallButtonUiDelegate,
+        DialerCallListener {
 
   private final Context context;
   private InCallButtonUi inCallButtonUi;
   private DialerCall call;
-  private boolean automaticallyMutedByAddCall = false;
-  private boolean previousMuteState = false;
   private boolean isInCallButtonUiReady;
   private PhoneAccountHandle otherAccount;
 
@@ -106,11 +102,18 @@
     InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
     InCallPresenter.getInstance().removeCanAddCallListener(this);
     isInCallButtonUiReady = false;
+
+    if (call != null) {
+      call.removeListener(this);
+    }
   }
 
   @Override
   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
     Trace.beginSection("CallButtonPresenter.onStateChange");
+    if (call != null) {
+      call.removeListener(this);
+    }
     if (newState == InCallState.OUTGOING) {
       call = callList.getOutgoingCall();
     } else if (newState == InCallState.INCALL) {
@@ -133,6 +136,10 @@
     } else {
       call = null;
     }
+
+    if (call != null) {
+      call.addListener(this);
+    }
     updateUi(newState, call);
     Trace.endSection();
   }
@@ -275,18 +282,7 @@
             DialerImpression.Type.IN_CALL_ADD_CALL_BUTTON_PRESSED,
             call.getUniqueCallId(),
             call.getTimeAddedMs());
-    if (automaticallyMutedByAddCall) {
-      // Since clicking add call button brings user to MainActivity and coming back refreshes mute
-      // state, add call button should only be clicked once during InCallActivity shows. Otherwise,
-      // we set previousMuteState wrong.
-      return;
-    }
-    // Automatically mute the current call
-    automaticallyMutedByAddCall = true;
-    previousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
-    // Simulate a click on the mute button
-    muteClicked(true /* checked */, false /* clickedByUser */);
-    TelecomAdapter.getInstance().addCall();
+    InCallPresenter.getInstance().addCallClicked();
   }
 
   @Override
@@ -393,7 +389,6 @@
             call.getTimeAddedMs());
 
     if (pause) {
-      call.getVideoTech().setCamera(null);
       call.getVideoTech().stopTransmission();
     } else {
       updateCamera(
@@ -542,31 +537,10 @@
   }
 
   @Override
-  public void refreshMuteState() {
-    // Restore the previous mute state
-    if (automaticallyMutedByAddCall
-        && AudioModeProvider.getInstance().getAudioState().isMuted() != previousMuteState) {
-      if (inCallButtonUi == null) {
-        return;
-      }
-      muteClicked(previousMuteState, false /* clickedByUser */);
-    }
-    automaticallyMutedByAddCall = false;
-  }
+  public void onSaveInstanceState(Bundle outState) {}
 
   @Override
-  public void onSaveInstanceState(Bundle outState) {
-    outState.putBoolean(KEY_AUTOMATICALLY_MUTED_BY_ADD_CALL, automaticallyMutedByAddCall);
-    outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, previousMuteState);
-  }
-
-  @Override
-  public void onRestoreInstanceState(Bundle savedInstanceState) {
-    automaticallyMutedByAddCall =
-        savedInstanceState.getBoolean(
-            KEY_AUTOMATICALLY_MUTED_BY_ADD_CALL, automaticallyMutedByAddCall);
-    previousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, previousMuteState);
-  }
+  public void onRestoreInstanceState(Bundle savedInstanceState) {}
 
   @Override
   public void onCameraPermissionGranted() {
@@ -584,6 +558,41 @@
   }
 
   @Override
+  public void onDialerCallSessionModificationStateChange() {
+    if (inCallButtonUi != null && call != null) {
+      inCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, true);
+      updateButtonsState(call);
+    }
+  }
+
+  @Override
+  public void onDialerCallDisconnect() {}
+
+  @Override
+  public void onDialerCallUpdate() {}
+
+  @Override
+  public void onDialerCallChildNumberChange() {}
+
+  @Override
+  public void onDialerCallLastForwardedNumberChange() {}
+
+  @Override
+  public void onDialerCallUpgradeToVideo() {}
+
+  @Override
+  public void onWiFiToLteHandover() {}
+
+  @Override
+  public void onHandoverToWifiFailure() {}
+
+  @Override
+  public void onInternationalCallOnWifi() {}
+
+  @Override
+  public void onEnrichedCallSessionUpdate() {}
+
+  @Override
   public Context getContext() {
     return context;
   }
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 150499f..17af756 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -273,6 +273,9 @@
 
   private SpeakEasyCallManager speakEasyCallManager;
 
+  private boolean addCallClicked = false;
+  private boolean automaticallyMutedByAddCall = false;
+
   /** Inaccessible constructor. Must use getRunningInstance() to get this singleton. */
   @VisibleForTesting
   InCallPresenter() {}
@@ -1226,7 +1229,9 @@
       proximitySensor.onInCallShowing(showing);
     }
 
-    if (!showing) {
+    if (showing) {
+      refreshMuteState();
+    } else {
       updateIsChangingConfigurations();
     }
 
@@ -2033,5 +2038,38 @@
     return true;
   }
 
+  public void addCallClicked() {
+    if (addCallClicked) {
+      // Since clicking add call button brings user to MainActivity and coming back refreshes mute
+      // state, add call button should only be clicked once during InCallActivity shows.
+      return;
+    }
+    addCallClicked = true;
+    if (!AudioModeProvider.getInstance().getAudioState().isMuted()) {
+      // Automatically mute the current call
+      TelecomAdapter.getInstance().mute(true);
+      automaticallyMutedByAddCall = true;
+    }
+    TelecomAdapter.getInstance().addCall();
+  }
+
+  /** Refresh mute state after call UI resuming from add call screen. */
+  public void refreshMuteState() {
+    LogUtil.i(
+        "InCallPresenter.refreshMuteState",
+        "refreshMuteStateAfterAddCall: %b addCallClicked: %b",
+        automaticallyMutedByAddCall,
+        addCallClicked);
+    if (!addCallClicked) {
+      return;
+    }
+    if (automaticallyMutedByAddCall) {
+      // Restore the previous mute state
+      TelecomAdapter.getInstance().mute(false);
+      automaticallyMutedByAddCall = false;
+    }
+    addCallClicked = false;
+  }
+
   private final Set<InCallUiLock> inCallUiLocks = new ArraySet<>();
 }
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index 536af8e..0d1d1a5 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -323,6 +323,17 @@
     InCallPresenter.InCallState inCallState = InCallPresenter.getInstance().getInCallState();
     onStateChange(inCallState, inCallState, CallList.getInstance());
     isVideoCallScreenUiReady = true;
+
+    Point sourceVideoDimensions = getRemoteVideoSurfaceTexture().getSourceVideoDimensions();
+    if (sourceVideoDimensions != null && primaryCall != null) {
+      int width = primaryCall.getPeerDimensionWidth();
+      int height = primaryCall.getPeerDimensionHeight();
+      boolean updated = DialerCall.UNKNOWN_PEER_DIMENSIONS != width
+          && DialerCall.UNKNOWN_PEER_DIMENSIONS != height;
+      if (updated && (sourceVideoDimensions.x != width || sourceVideoDimensions.y != height)) {
+        onUpdatePeerDimensions(primaryCall, width, height);
+      }
+    }
   }
 
   /** Called when the user interface is no longer ready to be used. */
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index 1f4e49a..f9afd2d 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -126,6 +126,8 @@
 
   private static int idCounter = 0;
 
+  public static final int UNKNOWN_PEER_DIMENSIONS = -1;
+
   /**
    * A counter used to append to restricted/private/hidden calls so that users can identify them in
    * a conversation. This value is reset in {@link CallList#onCallRemoved(Context, Call)} when there
@@ -386,6 +388,8 @@
       };
 
   private long timeAddedMs;
+  private int peerDimensionWidth = UNKNOWN_PEER_DIMENSIONS;
+  private int peerDimensionHeight = UNKNOWN_PEER_DIMENSIONS;
 
   public DialerCall(
       Context context,
@@ -1558,6 +1562,8 @@
 
   @Override
   public void onPeerDimensionsChanged(int width, int height) {
+    peerDimensionWidth = width;
+    peerDimensionHeight = height;
     InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
   }
 
@@ -1974,4 +1980,14 @@
   public interface CannedTextResponsesLoadedListener {
     void onCannedTextResponsesLoaded(DialerCall call);
   }
+
+  /** Gets peer dimension width. */
+  public int getPeerDimensionWidth() {
+    return peerDimensionWidth;
+  }
+
+  /** Gets peer dimension height. */
+  public int getPeerDimensionHeight() {
+    return peerDimensionHeight;
+  }
 }
diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java
index a686308..4086e14 100644
--- a/java/com/android/incallui/callpending/CallPendingActivity.java
+++ b/java/com/android/incallui/callpending/CallPendingActivity.java
@@ -232,9 +232,6 @@
           public void onRestoreInstanceState(Bundle savedInstanceState) {}
 
           @Override
-          public void refreshMuteState() {}
-
-          @Override
           public void addCallClicked() {}
 
           @Override
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
index d242c3a..213a3c6 100644
--- a/java/com/android/incallui/contactgrid/TopRow.java
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -92,6 +92,8 @@
     } else if (VideoUtils.hasSentVideoUpgradeRequest(state.sessionModificationState())
         || VideoUtils.hasReceivedVideoUpgradeRequest(state.sessionModificationState())) {
       label = getLabelForVideoRequest(context, state);
+    } else if (state.sessionModificationState() == SessionModificationState.REQUEST_FAILED) {
+      label = context.getString(R.string.incall_video_call_operation_failed);
     } else if (state.state() == DialerCallState.PULLING) {
       label = context.getString(R.string.incall_transferring);
     } else if (state.state() == DialerCallState.DIALING
diff --git a/java/com/android/incallui/contactgrid/res/values/strings.xml b/java/com/android/incallui/contactgrid/res/values/strings.xml
index 9ee10c3..e8592b2 100644
--- a/java/com/android/incallui/contactgrid/res/values/strings.xml
+++ b/java/com/android/incallui/contactgrid/res/values/strings.xml
@@ -65,6 +65,9 @@
        requests and we timed out. -->
   <string name="incall_video_call_request_timed_out">Call timed out</string>
 
+  <!-- Displayed above the contact name when the user's operation for video calling is failed due to an unknown reason. -->
+  <string name="incall_video_call_operation_failed">Unable to operate</string>
+
   <!-- In-call screen: status label for a call that's in the process of hanging up
        [CHAR LIMIT=25] -->
   <string name="incall_hanging_up">Hanging up</string>
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index 7f20b40..3062069 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -206,7 +206,6 @@
   @Override
   public void onResume() {
     super.onResume();
-    inCallButtonUiDelegate.refreshMuteState();
     inCallScreenDelegate.onInCallScreenResumed();
   }
 
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
index b0e23ef..4cf40ef 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -31,8 +31,6 @@
 
   void onRestoreInstanceState(Bundle savedInstanceState);
 
-  void refreshMuteState();
-
   void addCallClicked();
 
   void muteClicked(boolean checked, boolean clickedByUser);
diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java
index 988f20c..649e808 100644
--- a/java/com/android/incallui/rtt/impl/RttChatFragment.java
+++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java
@@ -361,7 +361,6 @@
 
   @Override
   public void onRttScreenStart() {
-    inCallButtonUiDelegate.refreshMuteState();
     rttCallScreenDelegate.onRttCallScreenUiReady();
     Activity activity = getActivity();
     Window window = getActivity().getWindow();
diff --git a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
index f270eda..89db079 100644
--- a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
@@ -308,7 +308,6 @@
 
   @Override
   public void onVideoScreenStart() {
-    inCallButtonUiDelegate.refreshMuteState();
     videoCallScreenDelegate.onVideoCallScreenUiReady();
     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
   }
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index be78bc1..edfc17c 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -397,7 +397,6 @@
 
   @Override
   public void onVideoScreenStart() {
-    inCallButtonUiDelegate.refreshMuteState();
     videoCallScreenDelegate.onVideoCallScreenUiReady();
     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
     getView()
@@ -578,13 +577,13 @@
       return new Point();
     }
     if (isLandscape()) {
-      int stableInsetEnd =
+      int systemWindowInsetEnd =
           getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
-              ? getView().getRootWindowInsets().getStableInsetLeft()
-              : -getView().getRootWindowInsets().getStableInsetRight();
-      return new Point(stableInsetEnd, 0);
+              ? getView().getRootWindowInsets().getSystemWindowInsetLeft()
+              : -getView().getRootWindowInsets().getSystemWindowInsetRight();
+      return new Point(systemWindowInsetEnd, 0);
     } else {
-      return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
+      return new Point(0, -getView().getRootWindowInsets().getSystemWindowInsetBottom());
     }
   }
 
@@ -733,9 +732,17 @@
     videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
     videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
 
-    this.isRemotelyHeld = isRemotelyHeld;
+    boolean updateRemoteOffView = false;
     if (this.shouldShowRemote != shouldShowRemote) {
       this.shouldShowRemote = shouldShowRemote;
+      updateRemoteOffView = true;
+    }
+    if (this.isRemotelyHeld != isRemotelyHeld) {
+      this.isRemotelyHeld = isRemotelyHeld;
+      updateRemoteOffView = true;
+    }
+
+    if (updateRemoteOffView) {
       updateRemoteOffView();
     }
     if (this.shouldShowPreview != shouldShowPreview) {
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
index d254d6d..3e6f441 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoCallCallback.java
@@ -103,8 +103,6 @@
 
     if (videoTech.getSessionModificationState()
         == SessionModificationState.WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
-      handler.removeCallbacksAndMessages(null); // Clear everything
-
       final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
       if (status == VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
         // Telecom manages audio route for us
@@ -114,31 +112,21 @@
         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(SessionModificationState.NO_REQUEST);
-            } else {
-              LogUtil.i(
-                  "ImsVideoCallCallback.onSessionModifyResponseReceived",
-                  "session modification state has changed, not clearing state");
-            }
-          },
-          CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+      clearFailedResponseState(newSessionModificationState);
     } else if (videoTech.getSessionModificationState()
         == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
       requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
       videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST);
     } else if (videoTech.getSessionModificationState()
         == SessionModificationState.WAITING_FOR_RESPONSE) {
-      videoTech.setSessionModificationState(getSessionModificationStateFromTelecomStatus(status));
+      final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
+      videoTech.setSessionModificationState(newSessionModificationState);
+      if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+        clearFailedResponseState(newSessionModificationState);
+      }
     } else {
       LogUtil.i(
           "ImsVideoCallCallback.onSessionModifyResponseReceived",
@@ -146,6 +134,24 @@
     }
   }
 
+  private void clearFailedResponseState(final int newSessionModificationState) {
+    handler.removeCallbacksAndMessages(null); // Clear everything
+    // 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.
+    handler.postDelayed(
+        () -> {
+          if (videoTech.getSessionModificationState() == newSessionModificationState) {
+            LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+            videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST);
+          } else {
+            LogUtil.i(
+                "ImsVideoCallCallback.onSessionModifyResponseReceived",
+                "session modification state has changed, not clearing state");
+          }
+        },
+        CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+  }
+
   @SessionModificationState
   private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
     switch (telecomStatus) {
diff --git a/java/com/android/incallui/videotech/ims/ImsVideoTech.java b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
index 5b733d6..1d4fe76 100644
--- a/java/com/android/incallui/videotech/ims/ImsVideoTech.java
+++ b/java/com/android/incallui/videotech/ims/ImsVideoTech.java
@@ -227,6 +227,7 @@
     call.getVideoCall()
         .sendSessionModifyRequest(
             new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
+    setSessionModificationState(SessionModificationState.WAITING_FOR_RESPONSE);
   }
 
   @Override