Show a warning dialog about charges when user starts a video call

The user starts a video call a warning dialog shall be presented.
If the user presses "OK" with the "Do not show again" box selected,
the dialog shall not be presented anymore.

Test: manual - Verified that a warning dialog about charges is shown
when a video call is started if
KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL is true.

This is an upstream change from:
https://android-review.googlesource.com/c/platform/packages/apps/Dialer/+/518977/8

Bug: 67832837
Test: partner manual test
PiperOrigin-RevId: 188103414
Change-Id: I62628a32557297acaef096db90d2ddf049ef5017
diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
index 236f779..c4ed6e6 100644
--- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
+++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java
@@ -78,6 +78,13 @@
   public static final Integer FEATURES_ASSISTED_DIALING = 1 << 4;
 
   /**
+   * Flag specifying whether to show an alert dialog for video call charges. By default this value
+   * is {@code false}. TODO(a bug): Replace with public API for these constants when available.
+   */
+  public static final String CARRIER_CONFIG_KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL =
+      "show_video_call_charges_alert_dialog_bool";
+
+  /**
    * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM
    * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality)
    *
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
index a19d45f..f5d681c 100644
--- a/java/com/android/incallui/VideoCallPresenter.java
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.os.Handler;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telecom.InCallService.VideoCall;
 import android.telecom.VideoProfile;
@@ -80,7 +81,8 @@
         InCallDetailsListener,
         SurfaceChangeListener,
         InCallPresenter.InCallEventListener,
-        VideoCallScreenDelegate {
+        VideoCallScreenDelegate,
+        CallList.Listener {
 
   private static boolean isVideoMode = false;
 
@@ -325,6 +327,8 @@
     InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(new LocalDelegate());
     InCallPresenter.getInstance().getRemoteVideoSurfaceTexture().setDelegate(new RemoteDelegate());
 
+    CallList.getInstance().addListener(this);
+
     // Register for surface and video events from {@link InCallVideoCallListener}s.
     InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
     currentVideoState = VideoProfile.STATE_AUDIO_ONLY;
@@ -354,6 +358,8 @@
     InCallPresenter.getInstance().removeInCallEventListener(this);
     InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
 
+    CallList.getInstance().removeListener(this);
+
     InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
 
     // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
@@ -1126,6 +1132,34 @@
         || VideoUtils.hasReceivedVideoUpgradeRequest(state);
   }
 
+  @Override
+  public void onIncomingCall(DialerCall call) {}
+
+  @Override
+  public void onUpgradeToVideo(DialerCall call) {}
+
+  @Override
+  public void onSessionModificationStateChange(DialerCall call) {}
+
+  @Override
+  public void onCallListChange(CallList callList) {}
+
+  @Override
+  public void onDisconnect(DialerCall call) {}
+
+  @Override
+  public void onWiFiToLteHandover(DialerCall call) {
+    if (call.isVideoCall() || call.hasSentVideoUpgradeRequest()) {
+      videoCallScreen.onHandoverFromWiFiToLte();
+    }
+  }
+
+  @Override
+  public void onHandoverToWifiFailed(DialerCall call) {}
+
+  @Override
+  public void onInternationalCallOnWifi(@NonNull DialerCall call) {}
+
   private class LocalDelegate implements VideoSurfaceDelegate {
     @Override
     public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) {
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
index 2f10a5b..7a21676 100644
--- a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -109,6 +109,9 @@
     return callId;
   }
 
+  @Override
+  public void onHandoverFromWiFiToLte() {}
+
   private void updatePreviewVideoScaling() {
     if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
       LogUtil.i(
diff --git a/java/com/android/incallui/answer/impl/SelfManagedAnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/SelfManagedAnswerVideoCallScreen.java
index 522d772..b74c1ec 100644
--- a/java/com/android/incallui/answer/impl/SelfManagedAnswerVideoCallScreen.java
+++ b/java/com/android/incallui/answer/impl/SelfManagedAnswerVideoCallScreen.java
@@ -110,6 +110,9 @@
     return callId;
   }
 
+  @Override
+  public void onHandoverFromWiFiToLte() {}
+
   /**
    * Opens the first front facing camera on the device into a {@link SurfaceView} while preserving
    * aspect ratio.
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index d36b00d..30d2bcb 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -17,6 +17,7 @@
 package com.android.incallui.call;
 
 import android.Manifest.permission;
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
@@ -25,6 +26,7 @@
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.Trace;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
@@ -156,6 +158,8 @@
 
   @Nullable private Boolean isInGlobalSpamList;
   private boolean didShowCameraPermission;
+  private boolean didDismissVideoChargesAlertDialog;
+  private PersistableBundle carrierConfig;
   private String callProviderLabel;
   private String callbackNumber;
   private int cameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
@@ -464,7 +468,7 @@
   /* package-private */ Call getTelecomCall() {
     return telecomCall;
   }
-  
+
   public StatusHints getStatusHints() {
     return telecomCall.getDetails().getStatusHints();
   }
@@ -585,6 +589,9 @@
         if (phoneAccount != null) {
           isCallSubjectSupported =
               phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+          if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+            cacheCarrierConfiguration(phoneAccountHandle);
+          }
         }
       }
     }
@@ -597,6 +604,26 @@
   }
 
   /**
+   * Caches frequently used carrier configuration locally.
+   *
+   * @param accountHandle The PhoneAccount handle.
+   */
+  @SuppressLint("MissingPermission")
+  private void cacheCarrierConfiguration(PhoneAccountHandle accountHandle) {
+    if (!PermissionsUtil.hasPermission(context, permission.READ_PHONE_STATE)) {
+      return;
+    }
+    if (VERSION.SDK_INT < VERSION_CODES.O) {
+      return;
+    }
+    // TODO(a bug): This may take several seconds to complete, revisit it to move it to worker
+    // thread.
+    carrierConfig =
+        TelephonyManagerCompat.getTelephonyManagerForPhoneAccountHandle(context, accountHandle)
+            .getCarrierConfig();
+  }
+
+  /**
    * Tests corruption of the {@code callExtras} bundle by calling {@link
    * Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} will
    * be thrown and caught by this function.
@@ -712,6 +739,14 @@
     doNotShowDialogForHandoffToWifiFailure = bool;
   }
 
+  public boolean showVideoChargesAlertDialog() {
+    if (carrierConfig == null) {
+      return false;
+    }
+    return carrierConfig.getBoolean(
+        TelephonyManagerCompat.CARRIER_CONFIG_KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL);
+  }
+
   public long getTimeAddedMs() {
     return timeAddedMs;
   }
@@ -1071,6 +1106,14 @@
     didShowCameraPermission = didShow;
   }
 
+  public boolean didDismissVideoChargesAlertDialog() {
+    return didDismissVideoChargesAlertDialog;
+  }
+
+  public void setDidDismissVideoChargesAlertDialog(boolean didDismiss) {
+    didDismissVideoChargesAlertDialog = didDismiss;
+  }
+
   @Nullable
   public Boolean isInGlobalSpamList() {
     return isInGlobalSpamList;
diff --git a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
index 28ee774..b97d2eb 100644
--- a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
@@ -725,6 +725,9 @@
   }
 
   @Override
+  public void onHandoverFromWiFiToLte() {}
+
+  @Override
   public void showButton(@InCallButtonIds int buttonId, boolean show) {
     LogUtil.v(
         "SurfaceViewVideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index 6b5a979..2a810cf 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -99,6 +99,8 @@
   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
   static final String ARG_CALL_ID = "call_id";
 
+  private static final String TAG_VIDEO_CHARGES_ALERT = "tag_video_charges_alert";
+
   @VisibleForTesting static final float BLUR_PREVIEW_RADIUS = 16.0f;
   @VisibleForTesting static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
   private static final float BLUR_REMOTE_RADIUS = 25.0f;
@@ -108,6 +110,7 @@
   private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
   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 static final long VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS = 500L;
 
   private final ViewOutlineProvider circleOutlineProvider =
       new ViewOutlineProvider() {
@@ -162,6 +165,24 @@
         }
       };
 
+  private final Runnable videoChargesAlertDialogRunnable =
+      () -> {
+        VideoChargesAlertDialogFragment existingVideoChargesAlertFragment =
+            (VideoChargesAlertDialogFragment)
+                getChildFragmentManager().findFragmentByTag(TAG_VIDEO_CHARGES_ALERT);
+        if (existingVideoChargesAlertFragment != null) {
+          LogUtil.i(
+              "VideoCallFragment.videoChargesAlertDialogRunnable", "already shown for this call");
+          return;
+        }
+
+        if (VideoChargesAlertDialogFragment.shouldShow(getContext(), getCallId())) {
+          LogUtil.i("VideoCallFragment.videoChargesAlertDialogRunnable", "showing dialog");
+          VideoChargesAlertDialogFragment.newInstance(getCallId())
+              .show(getChildFragmentManager(), TAG_VIDEO_CHARGES_ALERT);
+        }
+      };
+
   public static VideoCallFragment newInstance(String callId) {
     Bundle bundle = new Bundle();
     bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
@@ -352,6 +373,8 @@
     inCallButtonUiDelegate.refreshMuteState();
     videoCallScreenDelegate.onVideoCallScreenUiReady();
     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
+    getView()
+        .postDelayed(videoChargesAlertDialogRunnable, VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS);
   }
 
   @Override
@@ -377,6 +400,7 @@
 
   @Override
   public void onVideoScreenStop() {
+    getView().removeCallbacks(videoChargesAlertDialogRunnable);
     getView().removeCallbacks(cameraPermissionDialogRunnable);
     videoCallScreenDelegate.onVideoCallScreenUiUnready();
   }
@@ -768,6 +792,11 @@
   }
 
   @Override
+  public void onHandoverFromWiFiToLte() {
+    getView().post(videoChargesAlertDialogRunnable);
+  }
+
+  @Override
   public void showButton(@InCallButtonIds int buttonId, boolean show) {
     LogUtil.v(
         "VideoCallFragment.showButton",
diff --git a/java/com/android/incallui/video/impl/VideoChargesAlertDialogFragment.java b/java/com/android/incallui/video/impl/VideoChargesAlertDialogFragment.java
new file mode 100644
index 0000000..6762a9d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/VideoChargesAlertDialogFragment.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2018 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.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.Call.Details;
+import android.view.View;
+import android.widget.CheckBox;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+
+/** Alert dialog for video charges. */
+public class VideoChargesAlertDialogFragment extends DialogFragment {
+
+  /** Preference key for whether to show the alert dialog for video charges next time. */
+  @VisibleForTesting
+  static final String KEY_DO_NOT_SHOW_VIDEO_CHARGES_ALERT = "key_do_not_show_video_charges_alert";
+
+  /** Key in the arguments bundle for call id. */
+  private static final String ARG_CALL_ID = "call_id";
+
+  /**
+   * Returns {@code true} if an {@link VideoChargesAlertDialogFragment} should be shown.
+   *
+   * <p>Attempting to show an VideoChargesAlertDialogFragment when this method returns {@code false}
+   * will result in an {@link IllegalStateException}.
+   */
+  public static boolean shouldShow(@NonNull Context context, String callId) {
+    DialerCall call = CallList.getInstance().getCallById(callId);
+    if (call == null) {
+      LogUtil.i("VideoChargesAlertDialogFragment.shouldShow", "null call");
+      return false;
+    }
+
+    if (call.hasProperty(Details.PROPERTY_WIFI)) {
+      return false;
+    }
+
+    if (call.didDismissVideoChargesAlertDialog()) {
+      LogUtil.i(
+          "VideoChargesAlertDialogFragment.shouldShow", "The dialog has been dismissed by user");
+      return false;
+    }
+
+    if (!call.showVideoChargesAlertDialog()) {
+      return false;
+    }
+
+    if (!UserManagerCompat.isUserUnlocked(context)) {
+      LogUtil.i("VideoChargesAlertDialogFragment.shouldShow", "user locked, returning false");
+      return false;
+    }
+
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+    if (preferences.getBoolean(KEY_DO_NOT_SHOW_VIDEO_CHARGES_ALERT, false)) {
+      LogUtil.i(
+          "VideoChargesAlertDialogFragment.shouldShow",
+          "Video charges alert has been disabled by user, returning false");
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Returns a new instance of {@link VideoChargesAlertDialogFragment}
+   *
+   * <p>Prefer this method over the default constructor.
+   */
+  public static VideoChargesAlertDialogFragment newInstance(@NonNull String callId) {
+    VideoChargesAlertDialogFragment fragment = new VideoChargesAlertDialogFragment();
+    Bundle args = new Bundle();
+    args.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  @NonNull
+  @Override
+  public Dialog onCreateDialog(Bundle bundle) {
+    super.onCreateDialog(bundle);
+
+    if (!VideoChargesAlertDialogFragment.shouldShow(
+        getActivity(), getArguments().getString(ARG_CALL_ID))) {
+      throw new IllegalStateException(
+          "shouldShow indicated VideoChargesAlertDialogFragment should not have showed");
+    }
+
+    View dialogView = View.inflate(getActivity(), R.layout.frag_video_charges_alert_dialog, null);
+
+    CheckBox alertCheckBox = dialogView.findViewById(R.id.do_not_show);
+
+    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+    AlertDialog alertDialog =
+        new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme)
+            .setView(dialogView)
+            .setPositiveButton(
+                android.R.string.ok,
+                (dialog, which) -> onPositiveButtonClicked(preferences, alertCheckBox.isChecked()))
+            .create();
+    this.setCancelable(false);
+    return alertDialog;
+  }
+
+  private void onPositiveButtonClicked(@NonNull SharedPreferences preferences, boolean isChecked) {
+    LogUtil.i(
+        "VideoChargesAlertDialogFragment.onPositiveButtonClicked", "isChecked: %b", isChecked);
+    preferences.edit().putBoolean(KEY_DO_NOT_SHOW_VIDEO_CHARGES_ALERT, isChecked).apply();
+
+    DialerCall dialerCall =
+        CallList.getInstance().getCallById(getArguments().getString(ARG_CALL_ID));
+    if (dialerCall != null) {
+      dialerCall.setDidDismissVideoChargesAlertDialog(true);
+    }
+  }
+}
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_video_charges_alert_dialog.xml b/java/com/android/incallui/video/impl/res/layout/frag_video_charges_alert_dialog.xml
new file mode 100644
index 0000000..a547c7d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_video_charges_alert_dialog.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:paddingTop="24dp"
+      android:paddingStart="24dp"
+      android:paddingEnd="24dp"
+      android:paddingBottom="4dp"
+      android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/message"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="10dp"
+        android:text="@string/videocall_charges_alert_dialog_description"
+        android:textColor="@color/dialer_primary_text_color"
+        android:textSize="16sp"/>
+
+    <CheckBox
+        android:id="@+id/do_not_show"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:buttonTint="@color/dialer_theme_color"
+        android:focusable="true"
+        android:clickable="true"
+        android:text="@string/do_not_show_again"
+        android:textColor="@color/dialer_primary_text_color"
+        android:textSize="14sp"/>
+  </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/java/com/android/incallui/video/impl/res/values/strings.xml b/java/com/android/incallui/video/impl/res/values/strings.xml
index 58ea8bd..6c90af5 100644
--- a/java/com/android/incallui/video/impl/res/values/strings.xml
+++ b/java/com/android/incallui/video/impl/res/values/strings.xml
@@ -29,4 +29,10 @@
   <!-- Text indicates the call is resumed from held by remote party. [CHAR LIMIT=20] -->
   <string name="videocall_remotely_resumed">Call resumed</string>
 
+  <!-- Instruction text to notify user that charges may apply on video calling. [CHAR LIMIT=NONE] -->
+  <string name="videocall_charges_alert_dialog_description">Video calls made over the mobile network use both data and voice minutes. Charges may apply.</string>
+
+  <!-- Option to hide the popup dialog if it is not necessary for the user. [CHAR LIMIT=40] -->
+  <string name="do_not_show_again">Do not show again</string>
+
 </resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
index bad050c..582d4c6 100644
--- a/java/com/android/incallui/video/protocol/VideoCallScreen.java
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -39,4 +39,6 @@
   Fragment getVideoCallScreenFragment();
 
   String getCallId();
+
+  void onHandoverFromWiFiToLte();
 }