summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/video/impl
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/video/impl')
-rw-r--r--java/com/android/incallui/video/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java62
-rw-r--r--java/com/android/incallui/video/impl/CheckableImageButton.java222
-rw-r--r--java/com/android/incallui/video/impl/SpeakerButtonController.java118
-rw-r--r--java/com/android/incallui/video/impl/SwitchOnHoldCallController.java91
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java1215
-rw-r--r--java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml5
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.pngbin0 -> 1930 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.pngbin0 -> 3103 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.pngbin0 -> 3304 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.pngbin0 -> 4836 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.pngbin0 -> 4209 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.pngbin0 -> 4022 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.pngbin0 -> 5695 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.pngbin0 -> 1293 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.pngbin0 -> 1426 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.pngbin0 -> 1715 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.pngbin0 -> 2724 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.pngbin0 -> 2155 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.pngbin0 -> 1990 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.pngbin0 -> 3188 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.pngbin0 -> 2518 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.pngbin0 -> 4603 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.pngbin0 -> 4957 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.pngbin0 -> 7213 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.pngbin0 -> 6352 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.pngbin0 -> 6054 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.pngbin0 -> 8418 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.pngbin0 -> 4001 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.pngbin0 -> 9032 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.pngbin0 -> 8611 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.pngbin0 -> 13529 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.pngbin0 -> 11101 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.pngbin0 -> 10736 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.pngbin0 -> 15167 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.pngbin0 -> 2424 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml27
-rw-r--r--java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall.xml114
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml111
-rw-r--r--java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml33
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls.xml113
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml115
-rw-r--r--java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values/attrs.xml8
-rw-r--r--java/com/android/incallui/video/impl/res/values/dimens.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/values/strings.xml28
-rw-r--r--java/com/android/incallui/video/impl/res/values/styles.xml11
51 files changed, 2322 insertions, 0 deletions
diff --git a/java/com/android/incallui/video/impl/AndroidManifest.xml b/java/com/android/incallui/video/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a36828e29
--- /dev/null
+++ b/java/com/android/incallui/video/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.video.impl">
+</manifest>
diff --git a/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
new file mode 100644
index 000000000..291fce4a0
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
@@ -0,0 +1,62 @@
+/*
+ * 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.video.impl;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import com.android.dialer.common.FragmentUtils;
+
+/** Dialog fragment to ask for camera permission from user. */
+public class CameraPermissionDialogFragment extends DialogFragment {
+
+ static CameraPermissionDialogFragment newInstance() {
+ CameraPermissionDialogFragment fragment = new CameraPermissionDialogFragment();
+ return fragment;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle bundle) {
+ return new AlertDialog.Builder(getContext())
+ .setTitle(R.string.camera_permission_dialog_title)
+ .setMessage(R.string.camera_permission_dialog_message)
+ .setPositiveButton(
+ R.string.camera_permission_dialog_positive_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ VideoCallFragment fragment =
+ FragmentUtils.getParentUnsafe(
+ CameraPermissionDialogFragment.this, VideoCallFragment.class);
+ fragment.onCameraPermissionGranted();
+ }
+ })
+ .setNegativeButton(
+ R.string.camera_permission_dialog_negative_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .create();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/CheckableImageButton.java b/java/com/android/incallui/video/impl/CheckableImageButton.java
new file mode 100644
index 000000000..320f0571a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CheckableImageButton.java
@@ -0,0 +1,222 @@
+/*
+ * 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.video.impl;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.SoundEffectConstants;
+import android.widget.Checkable;
+import android.widget.ImageButton;
+
+/** Image button that maintains a checked state. */
+public class CheckableImageButton extends ImageButton implements Checkable {
+
+ private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+
+ /** Callback interface to notify when the button's checked state has changed */
+ public interface OnCheckedChangeListener {
+
+ void onCheckedChanged(CheckableImageButton button, boolean isChecked);
+ }
+
+ private boolean broadcasting;
+ private boolean isChecked;
+ private OnCheckedChangeListener onCheckedChangeListener;
+ private CharSequence contentDescriptionChecked;
+ private CharSequence contentDescriptionUnchecked;
+
+ public CheckableImageButton(Context context) {
+ this(context, null);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageButton);
+ setChecked(typedArray.getBoolean(R.styleable.CheckableImageButton_android_checked, false));
+ contentDescriptionChecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionChecked);
+ contentDescriptionUnchecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionUnchecked);
+ typedArray.recycle();
+
+ updateContentDescription();
+ setClickable(true);
+ setFocusable(true);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ performSetChecked(checked);
+ }
+
+ /**
+ * Called when the state of the button should be updated, this should not be the result of user
+ * interaction.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void performSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ isChecked = checked;
+ CharSequence contentDescription = updateContentDescription();
+ announceForAccessibility(contentDescription);
+ refreshDrawableState();
+ }
+
+ private CharSequence updateContentDescription() {
+ CharSequence contentDescription =
+ isChecked ? contentDescriptionChecked : contentDescriptionUnchecked;
+ setContentDescription(contentDescription);
+ return contentDescription;
+ }
+
+ /**
+ * Called when the user interacts with a button. This should not result in the button updating
+ * state, rather the request should be propagated to the associated listener.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void userRequestedSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ if (broadcasting) {
+ return;
+ }
+ broadcasting = true;
+ if (onCheckedChangeListener != null) {
+ onCheckedChangeListener.onCheckedChanged(this, checked);
+ }
+ broadcasting = false;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public void toggle() {
+ userRequestedSetChecked(!isChecked());
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ this.onCheckedChangeListener = listener;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!isCheckable()) {
+ return super.performClick();
+ }
+
+ toggle();
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return handled;
+ }
+
+ private boolean isCheckable() {
+ return onCheckedChangeListener != null;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ performSetChecked(savedState.isChecked);
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ return new SavedState(isChecked(), super.onSaveInstanceState());
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ public final boolean isChecked;
+
+ private SavedState(boolean isChecked, Parcelable superState) {
+ super(superState);
+ this.isChecked = isChecked;
+ }
+
+ protected SavedState(Parcel in) {
+ super(in);
+ isChecked = in.readByte() != 0;
+ }
+
+ public static final Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeByte((byte) (isChecked ? 1 : 0));
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SpeakerButtonController.java b/java/com/android/incallui/video/impl/SpeakerButtonController.java
new file mode 100644
index 000000000..e12032abf
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SpeakerButtonController.java
@@ -0,0 +1,118 @@
+/*
+ * 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.video.impl;
+
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.telecom.CallAudioState;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages a single button. */
+public class SpeakerButtonController implements OnCheckedChangeListener, OnClickListener {
+
+ @NonNull private final InCallButtonUiDelegate inCallButtonUiDelegate;
+ @NonNull private final VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private CheckableImageButton button;
+
+ @DrawableRes private int icon = R.drawable.quantum_ic_volume_up_white_36;
+
+ private boolean isChecked;
+ private boolean checkable;
+ private boolean isEnabled;
+ private CharSequence contentDescription;
+
+ public SpeakerButtonController(
+ @NonNull CheckableImageButton button,
+ @NonNull InCallButtonUiDelegate inCallButtonUiDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.inCallButtonUiDelegate = Assert.isNotNull(inCallButtonUiDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ this.button = Assert.isNotNull(button);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ }
+
+ public void updateButtonState() {
+ button.setVisibility(View.VISIBLE);
+ button.setEnabled(isEnabled);
+ button.setChecked(isChecked);
+ button.setOnClickListener(checkable ? null : this);
+ button.setOnCheckedChangeListener(checkable ? this : null);
+ button.setImageResource(icon);
+ button.setContentDescription(contentDescription);
+ }
+
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("SpeakerButtonController.setSupportedAudio", "audioState: " + audioState);
+
+ @StringRes int contentDescriptionResId;
+ if ((audioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ checkable = false;
+ isChecked = false;
+
+ if ((audioState.getRoute() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ icon = R.drawable.quantum_ic_bluetooth_audio_white_36;
+ contentDescriptionResId = R.string.incall_content_description_bluetooth;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ icon = R.drawable.quantum_ic_headset_white_36;
+ contentDescriptionResId = R.string.incall_content_description_headset;
+ } else {
+ icon = R.drawable.ic_phone_audio_white_36dp;
+ contentDescriptionResId = R.string.incall_content_description_earpiece;
+ }
+ } else {
+ checkable = true;
+ isChecked = audioState.getRoute() == CallAudioState.ROUTE_SPEAKER;
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ }
+
+ contentDescription = button.getContext().getText(contentDescriptionResId);
+ updateButtonState();
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ LogUtil.i("SpeakerButtonController.onCheckedChanged", null);
+ inCallButtonUiDelegate.toggleSpeakerphone();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+
+ @Override
+ public void onClick(View view) {
+ LogUtil.i("SpeakerButtonController.onClick", null);
+ inCallButtonUiDelegate.showAudioRouteSelector();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
new file mode 100644
index 000000000..372b56b4e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
@@ -0,0 +1,91 @@
+/*
+ * 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.video.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages the swap button and on hold banner. */
+public class SwitchOnHoldCallController implements OnClickListener {
+
+ @NonNull private InCallScreenDelegate inCallScreenDelegate;
+ @NonNull private VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private View switchOnHoldButton;
+
+ @NonNull private View onHoldBanner;
+
+ private boolean isVisible;
+
+ private boolean isEnabled;
+
+ @Nullable private SecondaryInfo secondaryInfo;
+
+ public SwitchOnHoldCallController(
+ @NonNull View switchOnHoldButton,
+ @NonNull View onHoldBanner,
+ @NonNull InCallScreenDelegate inCallScreenDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.switchOnHoldButton = Assert.isNotNull(switchOnHoldButton);
+ switchOnHoldButton.setOnClickListener(this);
+ this.onHoldBanner = Assert.isNotNull(onHoldBanner);
+ this.inCallScreenDelegate = Assert.isNotNull(inCallScreenDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ updateButtonState();
+ }
+
+ public void setVisible(boolean isVisible) {
+ this.isVisible = isVisible;
+ updateButtonState();
+ }
+
+ public void setOnScreen() {
+ isVisible = hasSecondaryInfo();
+ updateButtonState();
+ }
+
+ public void setSecondaryInfo(@Nullable SecondaryInfo secondaryInfo) {
+ this.secondaryInfo = secondaryInfo;
+ isVisible = hasSecondaryInfo();
+ }
+
+ private boolean hasSecondaryInfo() {
+ return secondaryInfo != null && secondaryInfo.shouldShow;
+ }
+
+ public void updateButtonState() {
+ switchOnHoldButton.setEnabled(isEnabled);
+ switchOnHoldButton.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ onHoldBanner.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onSecondaryInfoClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
new file mode 100644
index 000000000..77a67d032
--- /dev/null
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -0,0 +1,1215 @@
+/*
+ * 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.video.impl;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.graphics.Point;
+import android.graphics.drawable.Animatable;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnSystemUiVisibilityChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+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.compat.ActivityCompat;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+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.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Contains UI elements for a video call. */
+public class VideoCallFragment extends Fragment
+ implements InCallScreen,
+ InCallButtonUi,
+ VideoCallScreen,
+ OnClickListener,
+ OnCheckedChangeListener,
+ AudioRouteSelectorPresenter,
+ OnSystemUiVisibilityChangeListener {
+
+ 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;
+ private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f;
+ private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f;
+
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
+ private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG =
+ "CameraPermissionDialogFragment";
+ private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L;
+ private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L;
+
+ private final ViewOutlineProvider circleOutlineProvider =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int x = view.getWidth() / 2;
+ int y = view.getHeight() / 2;
+ int radius = Math.min(x, y);
+ outline.setOval(x - radius, y - radius, x + radius, y + radius);
+ }
+ };
+ private InCallScreenDelegate inCallScreenDelegate;
+ private VideoCallScreenDelegate videoCallScreenDelegate;
+ private InCallButtonUiDelegate inCallButtonUiDelegate;
+ private View endCallButton;
+ private CheckableImageButton speakerButton;
+ private SpeakerButtonController speakerButtonController;
+ private CheckableImageButton muteButton;
+ private CheckableImageButton cameraOffButton;
+ private ImageButton swapCameraButton;
+ private View switchOnHoldButton;
+ private View onHoldContainer;
+ private SwitchOnHoldCallController switchOnHoldCallController;
+ private TextView remoteVideoOff;
+ private ImageView remoteOffBlurredImageView;
+ private View mutePreviewOverlay;
+ private View previewOffOverlay;
+ private ImageView previewOffBlurredImageView;
+ private View controls;
+ private View controlsContainer;
+ private TextureView previewTextureView;
+ private TextureView remoteTextureView;
+ private View greenScreenBackgroundView;
+ private View fullscreenBackgroundView;
+ private boolean shouldShowRemote;
+ private boolean shouldShowPreview;
+ private boolean isInFullscreenMode;
+ private boolean isInGreenScreenMode;
+ private boolean hasInitializedScreenModes;
+ private boolean isRemotelyHeld;
+ private ContactGridManager contactGridManager;
+ private SecondaryInfo savedSecondaryInfo;
+ private final Runnable cameraPermissionDialogRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) {
+ LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog");
+ checkCameraPermission();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ LogUtil.i("VideoCallFragment.onCreate", null);
+
+ inCallButtonUiDelegate =
+ FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
+ .newInCallButtonUiDelegate();
+ if (savedInstanceState != null) {
+ inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted.");
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ } else {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied.");
+ }
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ LogUtil.i("VideoCallFragment.onCreateView", null);
+
+ View view =
+ layoutInflater.inflate(
+ isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall,
+ viewGroup,
+ false);
+ contactGridManager =
+ new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */);
+
+ controls = view.findViewById(R.id.videocall_video_controls);
+ controls.setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ controlsContainer = view.findViewById(R.id.videocall_video_controls_container);
+ speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button);
+ muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button);
+ muteButton.setOnCheckedChangeListener(this);
+ mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay);
+ cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video);
+ cameraOffButton.setOnCheckedChangeListener(this);
+ previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay);
+ previewOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view);
+ swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video);
+ swapCameraButton.setOnClickListener(this);
+ view.findViewById(R.id.videocall_switch_controls)
+ .setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold);
+ onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner);
+ remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off);
+ remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ remoteOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view);
+ endCallButton = view.findViewById(R.id.videocall_end_call);
+ endCallButton.setOnClickListener(this);
+ previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview);
+ previewTextureView.setClipToOutline(true);
+ previewOffOverlay.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ checkCameraPermission();
+ }
+ });
+ remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote);
+ greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background);
+ fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background);
+
+ // We need the texture view size to be able to scale the remote video. At this point the view
+ // layout won't be complete so add a layout listener.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ LogUtil.i("VideoCallFragment.onGlobalLayout", null);
+ updateRemoteVideoScaling();
+ updatePreviewVideoScaling();
+ updateVideoOffViews();
+ // Remove the listener so we don't continually re-layout.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ if (observer.isAlive()) {
+ observer.removeOnGlobalLayoutListener(this);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ LogUtil.i("VideoCallFragment.onViewCreated", null);
+
+ inCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
+ .newInCallScreenDelegate();
+ videoCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
+ .newVideoCallScreenDelegate();
+
+ speakerButtonController =
+ new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
+ switchOnHoldCallController =
+ new SwitchOnHoldCallController(
+ switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
+
+ videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
+
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ inCallButtonUiDelegate.onInCallButtonUiReady(this);
+
+ view.setOnSystemUiVisibilityChangeListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ inCallButtonUiDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ LogUtil.i("VideoCallFragment.onDestroyView", null);
+ inCallButtonUiDelegate.onInCallButtonUiUnready();
+ inCallScreenDelegate.onInCallScreenUnready();
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (savedSecondaryInfo != null) {
+ setSecondary(savedSecondaryInfo);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("VideoCallFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @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);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ LogUtil.i("VideoCallFragment.onStop", null);
+ getView().removeCallbacks(cameraPermissionDialogRunnable);
+ videoCallScreenDelegate.onVideoCallScreenUiUnready();
+ }
+
+ private void exitFullscreenMode() {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", null);
+
+ if (!getView().isAttachedToWindow()) {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached");
+ return;
+ }
+
+ showSystemUI();
+
+ LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
+
+ // Animate the controls to the shown state.
+ controls
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .start();
+
+ // Animate onHold to the shown state.
+ switchOnHoldButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ switchOnHoldCallController.setOnScreen();
+ }
+ });
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the shown state.
+ contactGridView
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ contactGridManager.show();
+ }
+ });
+
+ endCallButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.VISIBLE);
+ }
+ })
+ .start();
+
+ // Animate all the preview controls up to make room for the navigation bar.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ Point previewOffsetStartShown = getPreviewOffsetStartShown();
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate up with the preview offset above the navigation bar.
+ view.animate()
+ .translationX(previewOffsetStartShown.x)
+ .translationY(previewOffsetStartShown.y)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+
+ updateOverlayBackground();
+ }
+
+ private void showSystemUI() {
+ View view = getView();
+ if (view != null) {
+ // Code is more expressive with all flags present, even though some may be combined
+ //noinspection PointlessBitwiseExpression
+ view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ /** Set view flags to hide the system UI. System UI will return on any touch event */
+ private void hideSystemUI() {
+ View view = getView();
+ if (view != null) {
+ view.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ private Point getControlsOffsetEndHidden(View controls) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetBottom(controls));
+ } else {
+ return new Point(getOffsetStart(controls), 0);
+ }
+ }
+
+ private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetTop(swapCallButton));
+ } else {
+ return new Point(getOffsetEnd(swapCallButton), 0);
+ }
+ }
+
+ private Point getContactGridOffsetEndHidden(View view) {
+ return new Point(0, getOffsetTop(view));
+ }
+
+ private Point getEndCallOffsetEndHidden(View endCallButton) {
+ if (isLandscape()) {
+ return new Point(getOffsetEnd(endCallButton), 0);
+ } else {
+ return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
+ }
+ }
+
+ private Point getPreviewOffsetStartShown() {
+ // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
+ if (ActivityCompat.isInMultiWindowMode(getActivity())) {
+ return new Point();
+ }
+ if (isLandscape()) {
+ int stableInsetEnd =
+ getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+ ? getView().getRootWindowInsets().getStableInsetLeft()
+ : -getView().getRootWindowInsets().getStableInsetRight();
+ return new Point(stableInsetEnd, 0);
+ } else {
+ return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
+ }
+ }
+
+ private View[] getAllPreviewRelatedViews() {
+ return new View[] {
+ previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay,
+ };
+ }
+
+ private int getOffsetTop(View view) {
+ return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
+ }
+
+ private int getOffsetBottom(View view) {
+ return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
+ }
+
+ private int getOffsetStart(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return -offset;
+ }
+
+ private int getOffsetEnd(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return offset;
+ }
+
+ private void enterFullscreenMode() {
+ LogUtil.i("VideoCallFragment.enterFullscreenMode", null);
+
+ hideSystemUI();
+
+ Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
+
+ // Animate controls to the hidden state.
+ Point offset = getControlsOffsetEndHidden(controls);
+ controls
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .start();
+
+ // Animate onHold to the hidden state.
+ offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
+ switchOnHoldButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the hidden state.
+ offset = getContactGridOffsetEndHidden(contactGridView);
+ contactGridView
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ offset = getEndCallOffsetEndHidden(endCallButton);
+ // Use a fast out interpolator to quickly fade out the button. This is important because the
+ // button can't draw under the navigation bar which means that it'll look weird if it just
+ // abruptly disappears when it reaches the edge of the naivgation bar.
+ endCallButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.INVISIBLE);
+ }
+ })
+ .setInterpolator(new FastOutLinearInInterpolator())
+ .start();
+
+ // Animate all the preview controls down now that the navigation bar is hidden.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate down with the navigation bar hidden.
+ view.animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+ updateOverlayBackground();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == endCallButton) {
+ LogUtil.i("VideoCallFragment.onClick", "end call button clicked");
+ inCallButtonUiDelegate.onEndCallClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ } else if (v == swapCameraButton) {
+ if (swapCameraButton.getDrawable() instanceof Animatable) {
+ ((Animatable) swapCameraButton.getDrawable()).start();
+ }
+ inCallButtonUiDelegate.toggleCameraClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ if (button == cameraOffButton) {
+ if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog");
+ checkCameraPermission();
+ } else {
+ inCallButtonUiDelegate.pauseVideoClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ } else if (button == muteButton) {
+ inCallButtonUiDelegate.muteClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void showVideoViews(
+ boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
+ LogUtil.i(
+ "VideoCallFragment.showVideoViews",
+ "showPreview: %b, shouldShowRemote: %b",
+ shouldShowPreview,
+ shouldShowRemote);
+ this.shouldShowPreview = shouldShowPreview;
+ this.shouldShowRemote = shouldShowRemote;
+ this.isRemotelyHeld = isRemotelyHeld;
+
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
+
+ updateVideoOffViews();
+ updateRemoteVideoScaling();
+ }
+
+ /**
+ * This method scales the video feed inside the texture view, it doesn't change the texture view's
+ * size. In the old UI we would change the view size to match the aspect ratio of the video. In
+ * the new UI the view is always square (with the circular clip) so we have to do additional work
+ * to make sure the non-square video doesn't look squished.
+ */
+ @Override
+ public void onLocalVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void onLocalVideoOrientationChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ /** Called when the remote video's dimensions change. */
+ @Override
+ public void onRemoteVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null);
+ updateRemoteVideoScaling();
+ }
+
+ @Override
+ public void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode",
+ "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
+ shouldShowFullscreen,
+ shouldShowGreenScreen);
+
+ if (getActivity() == null) {
+ LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity");
+ return;
+ }
+
+ // Check if anything is actually going to change. The first time this function is called we
+ // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
+ // and green screen modes to update even if only one has changed. That's because they both
+ // depend on each other.
+ if (hasInitializedScreenModes
+ && shouldShowGreenScreen == isInGreenScreenMode
+ && shouldShowFullscreen == isInFullscreenMode) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes");
+ return;
+ }
+ hasInitializedScreenModes = true;
+ isInGreenScreenMode = shouldShowGreenScreen;
+ isInFullscreenMode = shouldShowFullscreen;
+
+ if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) {
+ controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
+ }
+ if (shouldShowGreenScreen) {
+ enterGreenScreenMode();
+ } else {
+ exitGreenScreenMode();
+ }
+ if (shouldShowFullscreen) {
+ enterFullscreenMode();
+ } else {
+ exitFullscreenMode();
+ }
+ updateVideoOffViews();
+
+ OnHoldFragment onHoldFragment =
+ ((OnHoldFragment)
+ getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
+ if (onHoldFragment != null) {
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ }
+ }
+
+ @Override
+ public Fragment getVideoCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void showButton(@InCallButtonIds int buttonId, boolean show) {
+ LogUtil.v(
+ "VideoCallFragment.showButton",
+ "buttonId: %s, show: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ show);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setVisible(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
+ swapCameraButton.setEnabled(show);
+ }
+ }
+
+ @Override
+ public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
+ LogUtil.v(
+ "VideoCallFragment.setEnabled",
+ "buttonId: %s, enable: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ enable);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setEnabled(enable);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled);
+ speakerButtonController.setEnabled(enabled);
+ muteButton.setEnabled(enabled);
+ cameraOffButton.setEnabled(enabled);
+ switchOnHoldCallController.setEnabled(enabled);
+ }
+
+ @Override
+ public void setHold(boolean value) {
+ LogUtil.i("VideoCallFragment.setHold", "value: " + value);
+ }
+
+ @Override
+ public void setCameraSwitched(boolean isBackFacingCamera) {
+ LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera);
+ }
+
+ @Override
+ public void setVideoPaused(boolean isPaused) {
+ LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
+ cameraOffButton.setChecked(isPaused);
+ }
+
+ @Override
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState);
+ speakerButtonController.setAudioState(audioState);
+ muteButton.setChecked(audioState.isMuted());
+ updateMutePreviewOverlayVisibility();
+ }
+
+ @Override
+ public void updateButtonStates() {
+ LogUtil.i("VideoCallFragment.updateButtonState", null);
+ speakerButtonController.updateButtonState();
+ switchOnHoldCallController.updateButtonState();
+ }
+
+ @Override
+ public void updateInCallButtonUiColors() {}
+
+ @Override
+ public Fragment getInCallButtonUiFragment() {
+ return this;
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ LogUtil.i("VideoCallFragment.showAudioRouteSelector", null);
+ AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
+ .show(getChildFragmentManager(), null);
+ }
+
+ @Override
+ public void onAudioRouteSelected(int audioRoute) {
+ LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
+ inCallButtonUiDelegate.setAudioRoute(audioRoute);
+ }
+
+ @Override
+ public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
+ LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString());
+ contactGridManager.setPrimary(primaryInfo);
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
+ LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString());
+ if (!isAdded()) {
+ savedSecondaryInfo = secondaryInfo;
+ return;
+ }
+ savedSecondaryInfo = null;
+ switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
+ updateButtonStates();
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
+ if (secondaryInfo.shouldShow) {
+ OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
+ } else {
+ if (oldBanner != null) {
+ transaction.remove(oldBanner);
+ }
+ }
+ transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
+ transaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString());
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
+ LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
+ }
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {
+ LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
+ }
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ LogUtil.i("VideoCallFragment.isManageConferenceVisible", null);
+ return false;
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public void showNoteSentToast() {
+ LogUtil.i("VideoCallFragment.showNoteSentToast", null);
+ }
+
+ @Override
+ public void updateInCallScreenColors() {
+ LogUtil.i("VideoCallFragment.updateColors", null);
+ }
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
+ }
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ return 0;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ return false;
+ }
+
+ @Override
+ public void showLocationUi(Fragment locationUi) {
+ LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported");
+ // Do nothing
+ }
+
+ private void updatePreviewVideoScaling() {
+ if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+ VideoSurfaceTexture localVideoSurfaceTexture =
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture();
+ Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions();
+ if (cameraDimensions == null) {
+ LogUtil.i(
+ "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set");
+ return;
+ }
+ if (isLandscape()) {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.x,
+ cameraDimensions.y,
+ videoCallScreenDelegate.getDeviceOrientation());
+ } else {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.y,
+ cameraDimensions.x,
+ videoCallScreenDelegate.getDeviceOrientation());
+ }
+ }
+
+ private void updateRemoteVideoScaling() {
+ VideoSurfaceTexture remoteVideoSurfaceTexture =
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture();
+ Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions();
+ if (videoSize == null) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null");
+ return;
+ }
+ if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+
+ // If the video and display aspect ratio's are close then scale video to fill display
+ float videoAspectRatio = ((float) videoSize.x) / videoSize.y;
+ float displayAspectRatio =
+ ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight();
+ float delta = Math.abs(videoAspectRatio - displayAspectRatio);
+ float sum = videoAspectRatio + displayAspectRatio;
+ if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) {
+ VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0);
+ } else {
+ VideoSurfaceBindings.scaleVideoMaintainingAspectRatio(
+ remoteTextureView, videoSize.x, videoSize.y);
+ }
+ }
+
+ private boolean isLandscape() {
+ // Choose orientation based on display orientation, not window orientation
+ int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
+ return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
+ }
+
+ private void enterGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.enterGreenScreenMode", null);
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(null);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(true);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(null);
+ previewOffBlurredImageView.setClipToOutline(false);
+ }
+
+ private void exitGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.exitGreenScreenMode", null);
+ Resources resources = getResources();
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ (int) resources.getDimension(R.dimen.videocall_preview_width),
+ (int) resources.getDimension(R.dimen.videocall_preview_height));
+ params.setMargins(
+ 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom));
+ if (isLandscape()) {
+ params.addRule(RelativeLayout.ALIGN_PARENT_END);
+ params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end));
+ } else {
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start));
+ }
+ params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(circleOutlineProvider);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(false);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider);
+ previewOffBlurredImageView.setClipToOutline(true);
+ }
+
+ private void updateVideoOffViews() {
+ // Always hide the preview off and remote off views in green screen mode.
+ boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
+ previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
+ updateBlurredImageView(
+ previewTextureView,
+ previewOffBlurredImageView,
+ shouldShowPreview,
+ BLUR_PREVIEW_RADIUS,
+ BLUR_PREVIEW_SCALE_FACTOR);
+
+ boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
+ boolean isResumed = remoteEnabled && !isRemotelyHeld;
+ if (isResumed) {
+ boolean wasRemoteVideoOff =
+ TextUtils.equals(
+ remoteVideoOff.getText(),
+ remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
+ // The text needs to be updated and hidden after enough delay in order to be announced by
+ // talkback.
+ remoteVideoOff.setText(
+ wasRemoteVideoOff
+ ? R.string.videocall_remote_video_on
+ : R.string.videocall_remotely_resumed);
+ remoteVideoOff.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ remoteVideoOff.setVisibility(View.GONE);
+ }
+ },
+ VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
+ } else {
+ remoteVideoOff.setText(
+ isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
+ remoteVideoOff.setVisibility(View.VISIBLE);
+ }
+ LogUtil.i("VideoCallFragment.updateVideoOffViews", "calling updateBlurredImageView");
+ updateBlurredImageView(
+ remoteTextureView,
+ remoteOffBlurredImageView,
+ shouldShowRemote,
+ BLUR_REMOTE_RADIUS,
+ BLUR_REMOTE_SCALE_FACTOR);
+ }
+
+ private void updateBlurredImageView(
+ TextureView textureView,
+ ImageView blurredImageView,
+ boolean isVideoEnabled,
+ float blurRadius,
+ float scaleFactor) {
+ boolean didBlur = false;
+ long startTimeMillis = SystemClock.elapsedRealtime();
+ if (!isVideoEnabled) {
+ int width = Math.round(textureView.getWidth() * scaleFactor);
+ int height = Math.round(textureView.getHeight() * scaleFactor);
+ // This call takes less than 10 milliseconds.
+ Bitmap bitmap = textureView.getBitmap(width, height);
+ if (bitmap != null) {
+ // TODO: When the view is first displayed after a rotation the bitmap is empty
+ // and thus this blur has no effect.
+ // This call can take 100 milliseconds.
+ blur(getContext(), bitmap, blurRadius);
+
+ // TODO: Figure out why only have to apply the transform in landscape mode
+ if (width > height) {
+ bitmap =
+ Bitmap.createBitmap(
+ bitmap,
+ 0,
+ 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ textureView.getTransform(null),
+ true);
+ }
+
+ blurredImageView.setImageBitmap(bitmap);
+ blurredImageView.setVisibility(View.VISIBLE);
+ didBlur = true;
+ }
+ }
+ if (!didBlur) {
+ blurredImageView.setImageBitmap(null);
+ blurredImageView.setVisibility(View.GONE);
+ }
+
+ LogUtil.i(
+ "VideoCallFragment.updateBlurredImageView",
+ "didBlur: %b, took %d millis",
+ didBlur,
+ (SystemClock.elapsedRealtime() - startTimeMillis));
+ }
+
+ private void updateOverlayBackground() {
+ if (isInGreenScreenMode) {
+ // We want to darken the preview view to make text and buttons readable. The fullscreen
+ // background is below the preview view so use the green screen background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ } else if (!isInFullscreenMode) {
+ // We want to darken the remote view to make text and buttons readable. The green screen
+ // background is above the preview view so it would darken the preview too. Use the fullscreen
+ // background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
+ } else {
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ }
+ }
+
+ private void updateMutePreviewOverlayVisibility() {
+ // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
+ // mode the preview is fullscreen so there's no where to anchor it.
+ mutePreviewOverlay.setVisibility(
+ muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
+ }
+
+ private static void animateSetVisibility(final View view, final int visibility) {
+ if (view.getVisibility() == visibility) {
+ return;
+ }
+
+ int startAlpha;
+ int endAlpha;
+ if (visibility == View.GONE) {
+ startAlpha = 1;
+ endAlpha = 0;
+ } else if (visibility == View.VISIBLE) {
+ startAlpha = 0;
+ endAlpha = 1;
+ } else {
+ Assert.fail();
+ return;
+ }
+
+ view.setAlpha(startAlpha);
+ view.setVisibility(View.VISIBLE);
+ view.animate()
+ .alpha(endAlpha)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ view.setVisibility(visibility);
+ }
+ })
+ .start();
+ }
+
+ private static void blur(Context context, Bitmap image, float blurRadius) {
+ RenderScript renderScript = RenderScript.create(context);
+ ScriptIntrinsicBlur blurScript =
+ ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
+ Allocation allocationIn = Allocation.createFromBitmap(renderScript, image);
+ Allocation allocationOut = Allocation.createFromBitmap(renderScript, image);
+ blurScript.setRadius(blurRadius);
+ blurScript.setInput(allocationIn);
+ blurScript.forEach(allocationOut);
+ allocationOut.copyTo(image);
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
+ videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
+ }
+
+ protected void onCameraPermissionGranted() {
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ }
+
+ private void checkCameraPermission() {
+ // Checks if user has consent of camera permission and the permission is granted.
+ // If camera permission is revoked, shows system permission dialog.
+ // If camera permission is granted but user doesn't have consent of camera permission
+ // (which means it's first time making video call), shows custom dialog instead. This
+ // will only be shown to user once.
+ if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ videoCallScreenDelegate.onCameraPermissionDialogShown();
+ if (!VideoUtils.hasCameraPermission(getContext())) {
+ requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
+ } else {
+ CameraPermissionDialogFragment.newInstance()
+ .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
new file mode 100644
index 000000000..b46607b1b
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#ff000000" android:state_checked="true"/>
+ <item android:color="#ffffffff"/>
+</selector>
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
new file mode 100644
index 000000000..b5c6f0a87
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..2ab2f21a7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..2deaadd76
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..c4147fa62
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c59e21504
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..95d6824f5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..9a525a374
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
new file mode 100644
index 000000000..f3427a02e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..c3ff7b2bb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..c75281332
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..fd16baef7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
new file mode 100644
index 000000000..3fe2446e3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..1ff3e7c25
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..aa7289af1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..491547189
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..799a78ebb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..4d5e03320
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..62cd1a477
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c68ad909a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..e5c3fc48d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..583c3de82
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..19a9344e9
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..5a7702bbc
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..a0be8d17d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..5671bfa06
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..527b3c47e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..996185890
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..56295b10f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..529c0a4d5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
new file mode 100644
index 000000000..ee514c776
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80888888">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="@color/incall_button_white"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
new file mode 100644
index 000000000..5e4841327
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/incall_button_ripple">
+ <item android:id="@android:id/mask">
+ <inset android:inset="5dp">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </inset>
+ </item>
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/video_button_bg_checked_pressed"
+ android:state_checked="true"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_checked"
+ android:state_checked="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_pressed"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_default"/>
+ </selector>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
new file mode 100644
index 000000000..1fb1bb088
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/front_back_switch_button_animation"/>
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
new file mode 100644
index 000000000..dc663dda1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ android:orientation="vertical">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ 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:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
new file mode 100644
index 000000000..2353deea1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ 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:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls_land"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
new file mode 100644
index 000000000..87c2e1b6c
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/ic_switch_camera"/>
diff --git a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
new file mode 100644
index 000000000..ad984f36e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingTop="16dp"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ app:autoResizeText_minTextSize="28sp"
+ tools:text="Jake Peralta"
+ tools:ignore="Deprecated"/>
+
+ <include
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
new file mode 100644
index 000000000..b3141bdf3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/videocall_video_preview_placeholder"
+ android:layout_alignEnd="@+id/videocall_video_preview_placeholder"
+ android:layout_alignStart="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="36dp"
+ android:layout_marginEnd="24dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginBottom="36dp"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
new file mode 100644
index 000000000..d71b3c00e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview_placeholder"
+ android:layout_alignTop="@+id/videocall_video_preview_placeholder"
+ android:layout_toStartOf="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="36dp"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"
+ tools:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/attrs.xml b/java/com/android/incallui/video/impl/res/values/attrs.xml
new file mode 100644
index 000000000..e4cd8af89
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="CheckableImageButton">
+ <attr name="android:checked"/>
+ <attr name="contentDescriptionChecked" format="reference|string"/>
+ <attr name="contentDescriptionUnchecked" format="reference|string"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/dimens.xml b/java/com/android/incallui/video/impl/res/values/dimens.xml
new file mode 100644
index 000000000..45860036f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_preview_width">72dp</dimen>
+ <dimen name="videocall_preview_height">72dp</dimen>
+ <dimen name="videocall_preview_margin_bottom">24dp</dimen>
+ <dimen name="videocall_preview_margin_start">24dp</dimen>
+ <dimen name="videocall_preview_margin_end">24dp</dimen>
+ <dimen name="videocall_button_spacing">8dp</dimen>
+ <dimen name="videocall_button_size">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/strings.xml b/java/com/android/incallui/video/impl/res/values/strings.xml
new file mode 100644
index 000000000..2b72b8004
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Text indicates the video from remote party is off. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_off">Their video is off</string>
+
+ <!-- Text indicates the video from remote party is on. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_on">Their video is on</string>
+
+ <!-- Text indicates the call is held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_held">Call on hold</string>
+
+ <!-- Text indicates the call is resumed from held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_resumed">Call resumed</string>
+
+ <!-- Title of dialog to ask user for camera permission. [CHAR LIMIT=30] -->
+ <string name="camera_permission_dialog_title">Allow video?</string>
+
+ <!-- Message of dialog to ask user for camera permission. [CHAR LIMIT=100] -->
+ <string name="camera_permission_dialog_message">The Phone app wants to use your camera for video calls.</string>
+
+ <!-- Text of button to be confirmed for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_positive_button">Allow</string>
+
+ <!-- Text of button to be declined for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_negative_button">Deny</string>
+
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/styles.xml b/java/com/android/incallui/video/impl/res/values/styles.xml
new file mode 100644
index 000000000..b94400875
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/styles.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Incall.Button.VideoCall" parent="Widget.AppCompat.ImageButton">
+ <item name="android:background">@drawable/videocall_video_button_background</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:tint">@color/videocall_button_icon_tint</item>
+ <item name="android:tintMode">src_atop</item>
+ <item name="android:stateListAnimator">@animator/disabled_alpha</item>
+ </style>
+</resources>