summaryrefslogtreecommitdiffstats
path: root/java/com/android/incallui/video/impl
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/incallui/video/impl
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
downloadandroid_packages_apps_Dialer-ccca31529c07970e89419fb85a9e8153a5396838.tar.gz
android_packages_apps_Dialer-ccca31529c07970e89419fb85a9e8153a5396838.tar.bz2
android_packages_apps_Dialer-ccca31529c07970e89419fb85a9e8153a5396838.zip
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
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>