summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/call
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/call
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
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/call')
-rw-r--r--java/com/android/incallui/call/CallList.java763
-rw-r--r--java/com/android/incallui/call/DialerCall.java1401
-rw-r--r--java/com/android/incallui/call/DialerCallDelegate.java25
-rw-r--r--java/com/android/incallui/call/DialerCallListener.java39
-rw-r--r--java/com/android/incallui/call/ExternalCallList.java136
-rw-r--r--java/com/android/incallui/call/InCallServiceListener.java40
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindings.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsStub.java24
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallback.java197
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java279
-rw-r--r--java/com/android/incallui/call/TelecomAdapter.java160
-rw-r--r--java/com/android/incallui/call/VideoUtils.java151
13 files changed, 3267 insertions, 0 deletions
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
new file mode 100644
index 000000000..862c71cf9
--- /dev/null
+++ b/java/com/android/incallui/call/CallList.java
@@ -0,0 +1,763 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Trace;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
+import android.telecom.Call;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.util.ArrayMap;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.shortcuts.ShortcutUsageReporter;
+import com.android.dialer.spam.Spam;
+import com.android.dialer.spam.SpamBindings;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Maintains the list of active calls and notifies interested classes of changes to the call list as
+ * they are received from the telephony stack. Primary listener of changes to this class is
+ * InCallPresenter.
+ */
+public class CallList implements DialerCallDelegate {
+
+ private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
+ private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
+ private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;
+
+ private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
+
+ private static CallList sInstance = new CallList();
+
+ private final Map<String, DialerCall> mCallById = new ArrayMap<>();
+ private final Map<android.telecom.Call, DialerCall> mCallByTelecomCall = new ArrayMap<>();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ private final Set<DialerCall> mPendingDisconnectCalls =
+ Collections.newSetFromMap(new ConcurrentHashMap<DialerCall, Boolean>(8, 0.9f, 1));
+ /** Handles the timeout for destroying disconnected calls. */
+ private final Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_DISCONNECTED_TIMEOUT:
+ LogUtil.d("CallList.handleMessage", "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
+ finishDisconnectedCall((DialerCall) msg.obj);
+ break;
+ default:
+ LogUtil.e("CallList.handleMessage", "Message not expected: " + msg.what);
+ break;
+ }
+ }
+ };
+
+ /**
+ * USED ONLY FOR TESTING Testing-only constructor. Instance should only be acquired through
+ * getInstance().
+ */
+ @VisibleForTesting
+ public CallList() {}
+
+ /** Static singleton accessor method. */
+ public static CallList getInstance() {
+ return sInstance;
+ }
+
+ public void onCallAdded(
+ final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport) {
+ Trace.beginSection("onCallAdded");
+ final DialerCall call =
+ new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+ final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
+ call.addListener(dialerCallListener);
+ LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
+ if (Spam.get(context).isSpamEnabled()) {
+ String number = TelecomCallUtil.getNumber(telecomCall);
+ Spam.get(context)
+ .checkSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isSpam) {
+ if (isSpam) {
+ if (call.getState() != DialerCall.State.INCOMING
+ && call.getState() != DialerCall.State.CALL_WAITING) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because it's not an incoming call");
+ isSpam = false;
+ } else if (isPotentialEmergencyCallback(context, call)) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because an emergency call was made on this"
+ + " device recently");
+ isSpam = false;
+ }
+ }
+
+ Logger.get(context)
+ .logCallImpression(
+ isSpam
+ ? DialerImpression.Type.INCOMING_SPAM_CALL
+ : DialerImpression.Type.INCOMING_NON_SPAM_CALL,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ call.setSpam(isSpam);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ });
+
+ updateUserMarkedSpamStatus(call, context, number, dialerCallListener);
+ }
+
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(context);
+
+ filteredNumberAsyncQueryHandler.isBlockedNumber(
+ new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ call.setBlockedStatus(true);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ }
+ },
+ call.getNumber(),
+ GeoUtil.getCurrentCountryIso(context));
+
+ if (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING) {
+ onIncoming(call);
+ } else {
+ dialerCallListener.onDialerCallUpdate();
+ }
+
+ if (call.getState() != State.INCOMING) {
+ // Only report outgoing calls
+ ShortcutUsageReporter.onOutgoingCallAdded(context, call.getNumber());
+ }
+
+ Trace.endSection();
+ }
+
+ private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
+ if (BuildCompat.isAtLeastO()) {
+ return call.isPotentialEmergencyCallback();
+ } else {
+ long timestampMillis = FilteredNumbersUtil.getLastEmergencyCallTimeMillis(context);
+ return call.isInEmergencyCallbackWindow(timestampMillis);
+ }
+ }
+
+ @Override
+ public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
+ return mCallByTelecomCall.get(telecomCall);
+ }
+
+ public void updateUserMarkedSpamStatus(
+ final DialerCall call,
+ final Context context,
+ String number,
+ final DialerCallListenerImpl dialerCallListener) {
+
+ Spam.get(context)
+ .checkUserMarkedNonSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserWhiteList) {
+ call.setIsInUserWhiteList(isInUserWhiteList);
+ }
+ });
+
+ Spam.get(context)
+ .checkGlobalSpamListStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInGlobalSpamList) {
+ call.setIsInGlobalSpamList(isInGlobalSpamList);
+ }
+ });
+
+ Spam.get(context)
+ .checkUserMarkedSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserSpamList) {
+ call.setIsInUserSpamList(isInUserSpamList);
+ }
+ });
+ }
+
+ public void onCallRemoved(Context context, android.telecom.Call telecomCall) {
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+ Assert.checkArgument(!call.isExternalCall());
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.w(
+ "CallList.onCallRemoved", "Removing call not previously disconnected " + call.getId());
+ }
+ }
+ }
+
+ InCallUiLegacyBindings getLegacyBindings(Context context) {
+ Objects.requireNonNull(context);
+
+ Context application = context.getApplicationContext();
+ InCallUiLegacyBindings legacyInstance = null;
+ if (application instanceof InCallUiLegacyBindingsFactory) {
+ legacyInstance = ((InCallUiLegacyBindingsFactory) application).newInCallUiLegacyBindings();
+ }
+
+ if (legacyInstance == null) {
+ legacyInstance = new InCallUiLegacyBindingsStub();
+ }
+ return legacyInstance;
+ }
+
+ /**
+ * Handles the case where an internal call has become an exteral call. We need to
+ *
+ * @param context
+ * @param telecomCall
+ */
+ public void onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall) {
+
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ // When removing a call from the call list because it became an external call, we need to
+ // ensure the callback is unregistered -- this is normally only done when calls disconnect.
+ // However, the call won't be disconnected in this case. Also, logic in updateCallInMap
+ // would just re-add the call anyways.
+ call.unregisterCallback();
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(telecomCall);
+ }
+ }
+
+ /** Called when a single call has changed. */
+ private void onIncoming(DialerCall call) {
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onIncoming", String.valueOf(call));
+ }
+
+ for (Listener listener : mListeners) {
+ listener.onIncomingCall(call);
+ }
+ }
+
+ public void addListener(@NonNull Listener listener) {
+ Objects.requireNonNull(listener);
+
+ mListeners.add(listener);
+
+ // Let the listener know about the active calls immediately.
+ listener.onCallListChange(this);
+ }
+
+ public void removeListener(@Nullable Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * TODO: Change so that this function is not needed. Instead of assuming there is an active call,
+ * the code should rely on the status of a specific DialerCall and allow the presenters to update
+ * the DialerCall object when the active call changes.
+ */
+ public DialerCall getIncomingOrActive() {
+ DialerCall retval = getIncomingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ public DialerCall getOutgoingOrActive() {
+ DialerCall retval = getOutgoingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ /** A call that is waiting for {@link PhoneAccount} selection */
+ public DialerCall getWaitingForAccountCall() {
+ return getFirstCallWithState(DialerCall.State.SELECT_PHONE_ACCOUNT);
+ }
+
+ public DialerCall getPendingOutgoingCall() {
+ return getFirstCallWithState(DialerCall.State.CONNECTING);
+ }
+
+ public DialerCall getOutgoingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.DIALING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.REDIALING);
+ }
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.PULLING);
+ }
+ return call;
+ }
+
+ public DialerCall getActiveCall() {
+ return getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+
+ public DialerCall getSecondActiveCall() {
+ return getCallWithState(DialerCall.State.ACTIVE, 1);
+ }
+
+ public DialerCall getBackgroundCall() {
+ return getFirstCallWithState(DialerCall.State.ONHOLD);
+ }
+
+ public DialerCall getDisconnectedCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTED);
+ }
+
+ public DialerCall getDisconnectingCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTING);
+ }
+
+ public DialerCall getSecondBackgroundCall() {
+ return getCallWithState(DialerCall.State.ONHOLD, 1);
+ }
+
+ public DialerCall getActiveOrBackgroundCall() {
+ DialerCall call = getActiveCall();
+ if (call == null) {
+ call = getBackgroundCall();
+ }
+ return call;
+ }
+
+ public DialerCall getIncomingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.INCOMING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.CALL_WAITING);
+ }
+
+ return call;
+ }
+
+ public DialerCall getFirstCall() {
+ DialerCall result = getIncomingCall();
+ if (result == null) {
+ result = getPendingOutgoingCall();
+ }
+ if (result == null) {
+ result = getOutgoingCall();
+ }
+ if (result == null) {
+ result = getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+ if (result == null) {
+ result = getDisconnectingCall();
+ }
+ if (result == null) {
+ result = getDisconnectedCall();
+ }
+ return result;
+ }
+
+ public boolean hasLiveCall() {
+ DialerCall call = getFirstCall();
+ return call != null && call != getDisconnectingCall() && call != getDisconnectedCall();
+ }
+
+ /**
+ * Returns the first call found in the call map with the upgrade to video modification state.
+ *
+ * @return The first call with the upgrade to video state.
+ */
+ public DialerCall getVideoUpgradeRequestCall() {
+ for (DialerCall call : mCallById.values()) {
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return call;
+ }
+ }
+ return null;
+ }
+
+ public DialerCall getCallById(String callId) {
+ return mCallById.get(callId);
+ }
+
+ /** Returns first call found in the call map with the specified state. */
+ public DialerCall getFirstCallWithState(int state) {
+ return getCallWithState(state, 0);
+ }
+
+ /**
+ * Returns the [position]th call found in the call map with the specified state. TODO: Improve
+ * this logic to sort by call time.
+ */
+ public DialerCall getCallWithState(int state, int positionToFind) {
+ DialerCall retval = null;
+ int position = 0;
+ for (DialerCall call : mCallById.values()) {
+ if (call.getState() == state) {
+ if (position >= positionToFind) {
+ retval = call;
+ break;
+ } else {
+ position++;
+ }
+ }
+ }
+
+ return retval;
+ }
+
+ /**
+ * This is called when the service disconnects, either expectedly or unexpectedly. For the
+ * expected case, it's because we have no calls left. For the unexpected case, it is likely a
+ * crash of phone and we need to clean up our calls manually. Without phone, there can be no
+ * active calls, so this is relatively safe thing to do.
+ */
+ public void clearOnDisconnect() {
+ for (DialerCall call : mCallById.values()) {
+ final int state = call.getState();
+ if (state != DialerCall.State.IDLE
+ && state != DialerCall.State.INVALID
+ && state != DialerCall.State.DISCONNECTED) {
+
+ call.setState(DialerCall.State.DISCONNECTED);
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
+ updateCallInMap(call);
+ }
+ }
+ notifyGenericListeners();
+ }
+
+ /**
+ * Called when the user has dismissed an error dialog. This indicates acknowledgement of the
+ * disconnect cause, and that any pending disconnects should immediately occur.
+ */
+ public void onErrorDialogDismissed() {
+ final Iterator<DialerCall> iterator = mPendingDisconnectCalls.iterator();
+ while (iterator.hasNext()) {
+ DialerCall call = iterator.next();
+ iterator.remove();
+ finishDisconnectedCall(call);
+ }
+ }
+
+ /**
+ * Processes an update for a single call.
+ *
+ * @param call The call to update.
+ */
+ private void onUpdateCall(DialerCall call) {
+ LogUtil.d("CallList.onUpdateCall", String.valueOf(call));
+ if (!mCallById.containsKey(call.getId()) && call.isExternalCall()) {
+ // When a regular call becomes external, it is removed from the call list, and there may be
+ // pending updates to Telecom which are queued up on the Telecom call's handler which we no
+ // longer wish to cause updates to the call in the CallList. Bail here if the list of tracked
+ // calls doesn't contain the call which received the update.
+ return;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onUpdateCall", String.valueOf(call));
+ }
+ }
+
+ /**
+ * Sends a generic notification to all listeners that something has changed. It is up to the
+ * listeners to call back to determine what changed.
+ */
+ private void notifyGenericListeners() {
+ for (Listener listener : mListeners) {
+ listener.onCallListChange(this);
+ }
+ }
+
+ private void notifyListenersOfDisconnect(DialerCall call) {
+ for (Listener listener : mListeners) {
+ listener.onDisconnect(call);
+ }
+ }
+
+ /**
+ * Updates the call entry in the local map.
+ *
+ * @return false if no call previously existed and no call was added, otherwise true.
+ */
+ private boolean updateCallInMap(DialerCall call) {
+ Objects.requireNonNull(call);
+
+ boolean updated = false;
+
+ if (call.getState() == DialerCall.State.DISCONNECTED) {
+ // update existing (but do not add!!) disconnected calls
+ if (mCallById.containsKey(call.getId())) {
+ // For disconnected calls, we want to keep them alive for a few seconds so that the
+ // UI has a chance to display anything it needs when a call is disconnected.
+
+ // Set up a timer to destroy the call after X seconds.
+ final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
+ mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
+ mPendingDisconnectCalls.add(call);
+
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ }
+ } else if (!isCallDead(call)) {
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ } else if (mCallById.containsKey(call.getId())) {
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(call.getTelecomCall());
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private int getDelayForDisconnect(DialerCall call) {
+ if (call.getState() != DialerCall.State.DISCONNECTED) {
+ throw new IllegalStateException();
+ }
+
+ final int cause = call.getDisconnectCause().getCode();
+ final int delay;
+ switch (cause) {
+ case DisconnectCause.LOCAL:
+ delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REMOTE:
+ case DisconnectCause.ERROR:
+ delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REJECTED:
+ case DisconnectCause.MISSED:
+ case DisconnectCause.CANCELED:
+ // no delay for missed/rejected incoming calls and canceled outgoing calls.
+ delay = 0;
+ break;
+ default:
+ delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
+ break;
+ }
+
+ return delay;
+ }
+
+ private boolean isCallDead(DialerCall call) {
+ final int state = call.getState();
+ return DialerCall.State.IDLE == state || DialerCall.State.INVALID == state;
+ }
+
+ /** Sets up a call for deletion and notifies listeners of change. */
+ private void finishDisconnectedCall(DialerCall call) {
+ if (mPendingDisconnectCalls.contains(call)) {
+ mPendingDisconnectCalls.remove(call);
+ }
+ call.setState(DialerCall.State.IDLE);
+ updateCallInMap(call);
+ notifyGenericListeners();
+ }
+
+ /**
+ * Notifies all video calls of a change in device orientation.
+ *
+ * @param rotation The new rotation angle (in degrees).
+ */
+ public void notifyCallsOfDeviceRotation(int rotation) {
+ for (DialerCall call : mCallById.values()) {
+ // First, ensure that the call videoState has video enabled (there is no need to set
+ // device orientation on a voice call which has not yet been upgraded to video).
+ // Second, ensure a VideoCall is set on the call so that the change can be sent to the
+ // provider (a VideoCall can be present for a call that does not currently have video,
+ // but can be upgraded to video).
+
+ // NOTE: is it necessary to use this order because getVideoCall references the class
+ // VideoProfile which is not available on APIs <23 (M).
+ if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
+ call.getVideoCall().setDeviceOrientation(rotation);
+ }
+ }
+ }
+
+ public void onInCallUiShown(boolean forFullScreenIntent) {
+ for (DialerCall call : mCallById.values()) {
+ call.getLatencyReport().onInCallUiShown(forFullScreenIntent);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of changes to the call list. */
+ public interface Listener {
+
+ /**
+ * Called when a new incoming call comes in. This is the only method that gets called for
+ * incoming calls. Listeners that want to perform an action on incoming call should respond in
+ * this method because {@link #onCallListChange} does not automatically get called for incoming
+ * calls.
+ */
+ void onIncomingCall(DialerCall call);
+
+ /**
+ * Called when a new modify call request comes in This is the only method that gets called for
+ * modify requests.
+ */
+ void onUpgradeToVideo(DialerCall call);
+
+ /** Called when the session modification state of a call changes. */
+ void onSessionModificationStateChange(@SessionModificationState int newState);
+
+ /**
+ * Called anytime there are changes to the call list. The change can be switching call states,
+ * updating information, etc. This method will NOT be called for new incoming calls and for
+ * calls that switch to disconnected state. Listeners must add actions to those method
+ * implementations if they want to deal with those actions.
+ */
+ void onCallListChange(CallList callList);
+
+ /**
+ * Called when a call switches to the disconnected state. This is the only method that will get
+ * called upon disconnection.
+ */
+ void onDisconnect(DialerCall call);
+
+ void onWiFiToLteHandover(DialerCall call);
+
+ /**
+ * Called when a user is in a video call and the call is unable to be handed off successfully to
+ * WiFi
+ */
+ void onHandoverToWifiFailed(DialerCall call);
+ }
+
+ private class DialerCallListenerImpl implements DialerCallListener {
+
+ private final DialerCall mCall;
+
+ DialerCallListenerImpl(DialerCall call) {
+ Assert.isNotNull(call);
+ mCall = call;
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {
+ if (updateCallInMap(mCall)) {
+ LogUtil.i("DialerCallListenerImpl.onDialerCallDisconnect", String.valueOf(mCall));
+ // notify those listening for all disconnects
+ notifyListenersOfDisconnect(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallUpdate() {
+ Trace.beginSection("onUpdate");
+ onUpdateCall(mCall);
+ notifyGenericListeners();
+ Trace.endSection();
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {
+ for (Listener listener : mListeners) {
+ listener.onUpgradeToVideo(mCall);
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {
+ for (Listener listener : mListeners) {
+ listener.onWiFiToLteHandover(mCall);
+ }
+ }
+
+ @Override
+ public void onHandoverToWifiFailure() {
+ for (Listener listener : mListeners) {
+ listener.onHandoverToWifiFailed(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ for (Listener listener : mListeners) {
+ listener.onSessionModificationStateChange(state);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
new file mode 100644
index 000000000..bd8f006dd
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -0,0 +1,1401 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Trace;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.dialer.callintent.CallIntentParser;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.util.CallUtil;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Describes a single call and its state. */
+public class DialerCall {
+
+ public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
+ public static final int CALL_HISTORY_STATUS_PRESENT = 1;
+ public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+ private static final String ID_PREFIX = "DialerCall_";
+ private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
+ "emergency_callback_window_millis";
+ private static int sIdCounter = 0;
+
+ /**
+ * The unique call ID for every call. This will help us to identify each call and allow us the
+ * ability to stitch impressions to calls if needed.
+ */
+ private final String uniqueCallId = UUID.randomUUID().toString();
+
+ private final Call mTelecomCall;
+ private final LatencyReport mLatencyReport;
+ private final String mId;
+ private final List<String> mChildCallIds = new ArrayList<>();
+ private final VideoSettings mVideoSettings = new VideoSettings();
+ private final LogState mLogState = new LogState();
+ private final Context mContext;
+ private final DialerCallDelegate mDialerCallDelegate;
+ private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
+ private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
+ new CopyOnWriteArrayList<>();
+
+ private boolean mIsEmergencyCall;
+ private Uri mHandle;
+ private int mState = State.INVALID;
+ private DisconnectCause mDisconnectCause;
+
+ private boolean hasShownWiFiToLteHandoverToast;
+ private boolean doNotShowDialogForHandoffToWifiFailure;
+
+ @SessionModificationState private int mSessionModificationState;
+ private int mVideoState;
+ /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
+ private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ private InCallVideoCallCallback mVideoCallCallback;
+ private boolean mIsVideoCallCallbackRegistered;
+ private String mChildNumber;
+ private String mLastForwardedNumber;
+ private String mCallSubject;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
+ private boolean mIsSpam;
+ private boolean mIsBlocked;
+ private boolean isInUserSpamList;
+ private boolean isInUserWhiteList;
+ private boolean isInGlobalSpamList;
+ private boolean didShowCameraPermission;
+ private String callProviderLabel;
+ private String callbackNumber;
+
+ public static String getNumberFromHandle(Uri handle) {
+ return handle == null ? "" : handle.getSchemeSpecificPart();
+ }
+
+ /**
+ * Whether the call is put on hold by remote party. This is different than the {@link
+ * State.ONHOLD} state which indicates that the call is being held locally on the device.
+ */
+ private boolean isRemotelyHeld;
+
+ /**
+ * Indicates whether the phone account associated with this call supports specifying a call
+ * subject.
+ */
+ private boolean mIsCallSubjectSupported;
+
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int newState) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call + " newState=" + newState);
+ update();
+ }
+
+ @Override
+ public void onParentChanged(Call call, Call newParent) {
+ LogUtil.v(
+ "TelecomCallCallback.onParentChanged", "call=" + call + " newParent=" + newParent);
+ update();
+ }
+
+ @Override
+ public void onChildrenChanged(Call call, List<Call> children) {
+ update();
+ }
+
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", " call=" + call + " details=" + details);
+ update();
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " cannedTextResponses=" + cannedTextResponses);
+ for (CannedTextResponsesLoadedListener listener : mCannedTextResponsesLoadedListeners) {
+ listener.onCannedTextResponsesLoaded(DialerCall.this);
+ }
+ }
+
+ @Override
+ public void onPostDialWait(Call call, String remainingPostDialSequence) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " remainingPostDialSequence=" + remainingPostDialSequence);
+ update();
+ }
+
+ @Override
+ public void onVideoCallChanged(Call call, VideoCall videoCall) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged", "call=" + call + " videoCall=" + videoCall);
+ update();
+ }
+
+ @Override
+ public void onCallDestroyed(Call call) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
+ call.unregisterCallback(this);
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
+ LogUtil.v(
+ "DialerCall.onConferenceableCallsChanged",
+ "call %s, conferenceable calls: %d",
+ call,
+ conferenceableCalls.size());
+ update();
+ }
+
+ @Override
+ public void onConnectionEvent(android.telecom.Call call, String event, Bundle extras) {
+ LogUtil.v(
+ "DialerCall.onConnectionEvent",
+ "Call: " + call + ", Event: " + event + ", Extras: " + extras);
+ switch (event) {
+ // The Previous attempt to Merge two calls together has failed in Telecom. We must
+ // now update the UI to possibly re-enable the Merge button based on the number of
+ // currently conferenceable calls available or Connection Capabilities.
+ case android.telecom.Connection.EVENT_CALL_MERGE_FAILED:
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE:
+ notifyWiFiToLteHandover();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_TO_WIFI_FAILED:
+ notifyHandoverToWifiFailed();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_HELD:
+ isRemotelyHeld = true;
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_UNHELD:
+ isRemotelyHeld = false;
+ update();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ private long mTimeAddedMs;
+
+ public DialerCall(
+ Context context,
+ DialerCallDelegate dialerCallDelegate,
+ Call telecomCall,
+ LatencyReport latencyReport,
+ boolean registerCallback) {
+ Assert.isNotNull(context);
+ mContext = context;
+ mDialerCallDelegate = dialerCallDelegate;
+ mTelecomCall = telecomCall;
+ mLatencyReport = latencyReport;
+ mId = ID_PREFIX + Integer.toString(sIdCounter++);
+
+ updateFromTelecomCall(registerCallback);
+
+ if (registerCallback) {
+ mTelecomCall.registerCallback(mTelecomCallCallback);
+ }
+
+ mTimeAddedMs = System.currentTimeMillis();
+ parseCallSpecificAppData();
+ }
+
+ private static int translateState(int state) {
+ switch (state) {
+ case Call.STATE_NEW:
+ case Call.STATE_CONNECTING:
+ return DialerCall.State.CONNECTING;
+ case Call.STATE_SELECT_PHONE_ACCOUNT:
+ return DialerCall.State.SELECT_PHONE_ACCOUNT;
+ case Call.STATE_DIALING:
+ return DialerCall.State.DIALING;
+ case Call.STATE_PULLING_CALL:
+ return DialerCall.State.PULLING;
+ case Call.STATE_RINGING:
+ return DialerCall.State.INCOMING;
+ case Call.STATE_ACTIVE:
+ return DialerCall.State.ACTIVE;
+ case Call.STATE_HOLDING:
+ return DialerCall.State.ONHOLD;
+ case Call.STATE_DISCONNECTED:
+ return DialerCall.State.DISCONNECTED;
+ case Call.STATE_DISCONNECTING:
+ return DialerCall.State.DISCONNECTING;
+ default:
+ return DialerCall.State.INVALID;
+ }
+ }
+
+ public static boolean areSame(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Ids
+ return call1.getId().equals(call2.getId());
+ }
+
+ public static boolean areSameNumber(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Numbers
+ return TextUtils.equals(call1.getNumber(), call2.getNumber());
+ }
+
+ public void addListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.add(listener);
+ }
+
+ public void removeListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.remove(listener);
+ }
+
+ public void addCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.add(listener);
+ }
+
+ public void removeCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.remove(listener);
+ }
+
+ public void notifyWiFiToLteHandover() {
+ LogUtil.i("DialerCall.notifyWiFiToLteHandover", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onWiFiToLteHandover();
+ }
+ }
+
+ public void notifyHandoverToWifiFailed() {
+ LogUtil.i("DialerCall.notifyHandoverToWifiFailed", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onHandoverToWifiFailure();
+ }
+ }
+
+ /* package-private */ Call getTelecomCall() {
+ return mTelecomCall;
+ }
+
+ public StatusHints getStatusHints() {
+ return mTelecomCall.getDetails().getStatusHints();
+ }
+
+ /**
+ * @return video settings of the call, null if the call is not a video call.
+ * @see VideoProfile
+ */
+ public VideoSettings getVideoSettings() {
+ return mVideoSettings;
+ }
+
+ private void update() {
+ Trace.beginSection("Update");
+ int oldState = getState();
+ // We want to potentially register a video call callback here.
+ updateFromTelecomCall(true /* registerCallback */);
+ if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallDisconnect();
+ }
+ } else {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ }
+ Trace.endSection();
+ }
+
+ private void updateFromTelecomCall(boolean registerCallback) {
+ LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+ final int translatedState = translateState(mTelecomCall.getState());
+ if (mState != State.BLOCKED) {
+ setState(translatedState);
+ setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
+ maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
+ }
+
+ if (registerCallback && mTelecomCall.getVideoCall() != null) {
+ if (mVideoCallCallback == null) {
+ mVideoCallCallback = new InCallVideoCallCallback(this);
+ }
+ mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
+ mIsVideoCallCallbackRegistered = true;
+ }
+
+ mChildCallIds.clear();
+ final int numChildCalls = mTelecomCall.getChildren().size();
+ for (int i = 0; i < numChildCalls; i++) {
+ mChildCallIds.add(
+ mDialerCallDelegate
+ .getDialerCallFromTelecomCall(mTelecomCall.getChildren().get(i))
+ .getId());
+ }
+
+ // The number of conferenced calls can change over the course of the call, so use the
+ // maximum number of conferenced child calls as the metric for conference call usage.
+ mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls);
+
+ updateFromCallExtras(mTelecomCall.getDetails().getExtras());
+
+ // If the handle of the call has changed, update state for the call determining if it is an
+ // emergency call.
+ Uri newHandle = mTelecomCall.getDetails().getHandle();
+ if (!Objects.equals(mHandle, newHandle)) {
+ mHandle = newHandle;
+ updateEmergencyCallState();
+ }
+
+ // If the phone account handle of the call is set, cache capability bit indicating whether
+ // the phone account supports call subjects.
+ PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle();
+ if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) {
+ mPhoneAccountHandle = newPhoneAccountHandle;
+
+ if (mPhoneAccountHandle != null) {
+ PhoneAccount phoneAccount =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(mPhoneAccountHandle);
+ if (phoneAccount != null) {
+ mIsCallSubjectSupported =
+ phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+ }
+ }
+ }
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ && isVideoCall()) {
+ // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
+ // whether the video upgrade request was accepted. We don't clear the session modification
+ // state right away though to avoid having the UI switch from video to voice to video.
+ // Once the underlying telecom call updates to video mode it's safe to clear the state.
+ LogUtil.i(
+ "DialerCall.updateFromTelecomCall",
+ "upgraded to video, clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param callExtras the bundle to verify
+ * @return {@code true} if the bundle is corrupted, {@code false} otherwise.
+ */
+ protected boolean areCallExtrasCorrupted(Bundle callExtras) {
+ /**
+ * There's currently a bug in Telephony service (b/25613098) that could corrupt the extras
+ * bundle, resulting in a IllegalArgumentException while validating data under {@link
+ * Bundle#containsKey(String)}.
+ */
+ try {
+ callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS);
+ return false;
+ } catch (IllegalArgumentException e) {
+ LogUtil.e(
+ "DialerCall.areCallExtrasCorrupted", "callExtras is corrupted, ignoring exception", e);
+ return true;
+ }
+ }
+
+ protected void updateFromCallExtras(Bundle callExtras) {
+ if (callExtras == null || areCallExtrasCorrupted(callExtras)) {
+ /**
+ * If the bundle is corrupted, abandon information update as a work around. These are not
+ * critical for the dialer to function.
+ */
+ return;
+ }
+ // Check for a change in the child address and notify any listeners.
+ if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
+ String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS);
+ if (!Objects.equals(childNumber, mChildNumber)) {
+ mChildNumber = childNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallChildNumberChange();
+ }
+ }
+ }
+
+ // Last forwarded number comes in as an array of strings. We want to choose the
+ // last item in the array. The forwarding numbers arrive independently of when the
+ // call is originally set up, so we need to notify the the UI of the change.
+ if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) {
+ ArrayList<String> lastForwardedNumbers =
+ callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER);
+
+ if (lastForwardedNumbers != null) {
+ String lastForwardedNumber = null;
+ if (!lastForwardedNumbers.isEmpty()) {
+ lastForwardedNumber = lastForwardedNumbers.get(lastForwardedNumbers.size() - 1);
+ }
+
+ if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) {
+ mLastForwardedNumber = lastForwardedNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallLastForwardedNumberChange();
+ }
+ }
+ }
+ }
+
+ // DialerCall subject is present in the extras at the start of call, so we do not need to
+ // notify any other listeners of this.
+ if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) {
+ String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT);
+ if (!Objects.equals(mCallSubject, callSubject)) {
+ mCallSubject = callSubject;
+ }
+ }
+ }
+
+ /**
+ * Determines if a received upgrade to video request should be cancelled. This can happen if
+ * another InCall UI responds to the upgrade to video request.
+ *
+ * @param newVideoState The new video state.
+ */
+ private void maybeCancelVideoUpgrade(int newVideoState) {
+ boolean isVideoStateChanged = mVideoState != newVideoState;
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ && isVideoStateChanged) {
+
+ LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ mVideoState = newVideoState;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public boolean hasShownWiFiToLteHandoverToast() {
+ return hasShownWiFiToLteHandoverToast;
+ }
+
+ public void setHasShownWiFiToLteHandoverToast() {
+ hasShownWiFiToLteHandoverToast = true;
+ }
+
+ public boolean showWifiHandoverAlertAsToast() {
+ return doNotShowDialogForHandoffToWifiFailure;
+ }
+
+ public void setDoNotShowDialogForHandoffToWifiFailure(boolean bool) {
+ doNotShowDialogForHandoffToWifiFailure = bool;
+ }
+
+ public long getTimeAddedMs() {
+ return mTimeAddedMs;
+ }
+
+ @Nullable
+ public String getNumber() {
+ return TelecomCallUtil.getNumber(mTelecomCall);
+ }
+
+ public void blockCall() {
+ mTelecomCall.reject(false, null);
+ setState(State.BLOCKED);
+ }
+
+ @Nullable
+ public Uri getHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle();
+ }
+
+ public boolean isEmergencyCall() {
+ return mIsEmergencyCall;
+ }
+
+ public boolean isPotentialEmergencyCallback() {
+ // The property PROPERTY_EMERGENCY_CALLBACK_MODE is only set for CDMA calls when the system
+ // is actually in emergency callback mode (ie data is disabled).
+ if (hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
+ return true;
+ }
+ // We want to treat any incoming call that arrives a short time after an outgoing emergency call
+ // as a potential emergency callback.
+ if (getExtras() != null
+ && getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0)
+ > 0) {
+ long lastEmergencyCallMillis =
+ getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
+ if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isInEmergencyCallbackWindow(long timestampMillis) {
+ long emergencyCallbackWindowMillis =
+ ConfigProviderBindings.get(mContext)
+ .getLong(CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS, TimeUnit.MINUTES.toMillis(5));
+ return System.currentTimeMillis() - timestampMillis < emergencyCallbackWindowMillis;
+ }
+
+ public int getState() {
+ if (mTelecomCall != null && mTelecomCall.getParent() != null) {
+ return State.CONFERENCED;
+ } else {
+ return mState;
+ }
+ }
+
+ public void setState(int state) {
+ mState = state;
+ if (mState == State.INCOMING) {
+ mLogState.isIncoming = true;
+ } else if (mState == State.DISCONNECTED) {
+ mLogState.duration =
+ getConnectTimeMillis() == 0 ? 0 : System.currentTimeMillis() - getConnectTimeMillis();
+ }
+ }
+
+ public int getNumberPresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getHandlePresentation();
+ }
+
+ public int getCnapNamePresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getCallerDisplayNamePresentation();
+ }
+
+ @Nullable
+ public String getCnapName() {
+ return mTelecomCall == null ? null : getTelecomCall().getDetails().getCallerDisplayName();
+ }
+
+ public Bundle getIntentExtras() {
+ return mTelecomCall.getDetails().getIntentExtras();
+ }
+
+ @Nullable
+ public Bundle getExtras() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras();
+ }
+
+ /** @return The child number for the call, or {@code null} if none specified. */
+ public String getChildNumber() {
+ return mChildNumber;
+ }
+
+ /** @return The last forwarded number for the call, or {@code null} if none specified. */
+ public String getLastForwardedNumber() {
+ return mLastForwardedNumber;
+ }
+
+ /** @return The call subject, or {@code null} if none specified. */
+ public String getCallSubject() {
+ return mCallSubject;
+ }
+
+ /**
+ * @return {@code true} if the call's phone account supports call subjects, {@code false}
+ * otherwise.
+ */
+ public boolean isCallSubjectSupported() {
+ return mIsCallSubjectSupported;
+ }
+
+ /** Returns call disconnect cause, defined by {@link DisconnectCause}. */
+ public DisconnectCause getDisconnectCause() {
+ if (mState == State.DISCONNECTED || mState == State.IDLE) {
+ return mDisconnectCause;
+ }
+
+ return new DisconnectCause(DisconnectCause.UNKNOWN);
+ }
+
+ public void setDisconnectCause(DisconnectCause disconnectCause) {
+ mDisconnectCause = disconnectCause;
+ mLogState.disconnectCause = mDisconnectCause;
+ }
+
+ /** Returns the possible text message responses. */
+ public List<String> getCannedSmsResponses() {
+ return mTelecomCall.getCannedTextResponses();
+ }
+
+ /** Checks if the call supports the given set of capabilities supplied as a bit mask. */
+ public boolean can(int capabilities) {
+ int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities();
+
+ if ((capabilities & Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) {
+ // We allow you to merge if the capabilities allow it or if it is a call with
+ // conferenceable calls.
+ if (mTelecomCall.getConferenceableCalls().isEmpty()
+ && ((Call.Details.CAPABILITY_MERGE_CONFERENCE & supportedCapabilities) == 0)) {
+ // Cannot merge calls if there are no calls to merge with.
+ return false;
+ }
+ capabilities &= ~Call.Details.CAPABILITY_MERGE_CONFERENCE;
+ }
+ return (capabilities == (capabilities & supportedCapabilities));
+ }
+
+ public boolean hasProperty(int property) {
+ return mTelecomCall.getDetails().hasProperty(property);
+ }
+
+ public String getUniqueCallId() {
+ return uniqueCallId;
+ }
+
+ /** Gets the time when the call first became active. */
+ public long getConnectTimeMillis() {
+ return mTelecomCall.getDetails().getConnectTimeMillis();
+ }
+
+ public boolean isConferenceCall() {
+ return hasProperty(Call.Details.PROPERTY_CONFERENCE);
+ }
+
+ @Nullable
+ public GatewayInfo getGatewayInfo() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo();
+ }
+
+ @Nullable
+ public PhoneAccountHandle getAccountHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
+ }
+
+ /**
+ * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
+ * null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
+ * {@link VideoCall}.
+ */
+ public VideoCall getVideoCall() {
+ return mTelecomCall == null || !mIsVideoCallCallbackRegistered
+ ? null
+ : mTelecomCall.getVideoCall();
+ }
+
+ public List<String> getChildCallIds() {
+ return mChildCallIds;
+ }
+
+ public String getParentId() {
+ Call parentCall = mTelecomCall.getParent();
+ if (parentCall != null) {
+ return mDialerCallDelegate.getDialerCallFromTelecomCall(parentCall).getId();
+ }
+ return null;
+ }
+
+ public int getVideoState() {
+ return mTelecomCall.getDetails().getVideoState();
+ }
+
+ public boolean isVideoCall() {
+ return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+ }
+
+ /**
+ * Determines if the call handle is an emergency number or not and caches the result to avoid
+ * repeated calls to isEmergencyNumber.
+ */
+ private void updateEmergencyCallState() {
+ mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
+ }
+
+ /**
+ * Gets the video state which was requested via a session modification request.
+ *
+ * @return The video state.
+ */
+ public int getRequestedVideoState() {
+ return mRequestedVideoState;
+ }
+
+ /**
+ * Handles incoming session modification requests. Stores the pending video request and sets the
+ * session modification state to {@link
+ * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
+ * track of the fact the request was received. Only upgrade requests require user confirmation and
+ * will be handled by this method. The remote user can turn off their own camera without
+ * confirmation.
+ *
+ * @param videoState The requested video state.
+ */
+ public void setRequestedVideoState(int videoState) {
+ LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
+ if (videoState == getVideoState()) {
+ LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ return;
+ }
+
+ mRequestedVideoState = videoState;
+ setSessionModificationState(
+ DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpgradeToVideo();
+ }
+
+ LogUtil.i(
+ "DialerCall.setRequestedVideoState",
+ "mSessionModificationState: %d, videoState: %d",
+ mSessionModificationState,
+ videoState);
+ update();
+ }
+
+ /**
+ * Gets the current video session modification state.
+ *
+ * @return The session modification state.
+ */
+ @SessionModificationState
+ public int getSessionModificationState() {
+ return mSessionModificationState;
+ }
+
+ /**
+ * Set the session modification state. Used to keep track of pending video session modification
+ * operations and to inform listeners of these changes.
+ *
+ * @param state the new session modification state.
+ */
+ public void setSessionModificationState(@SessionModificationState int state) {
+ boolean hasChanged = mSessionModificationState != state;
+ if (hasChanged) {
+ LogUtil.i(
+ "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
+ mSessionModificationState = state;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallSessionModificationStateChange(state);
+ }
+ }
+ }
+
+ public LogState getLogState() {
+ return mLogState;
+ }
+
+ /**
+ * Determines if the call is an external call.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
+ }
+
+ /**
+ * Determines if the external call is pullable.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
+ * which means that the user can request it be transferred to the current device.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isPullableExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && (mTelecomCall.getDetails().getCallCapabilities()
+ & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
+ == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
+ }
+
+ /**
+ * Determines if answering this call will cause an ongoing video call to be dropped.
+ *
+ * @return {@code true} if answering this call will drop an ongoing video call, {@code false}
+ * otherwise.
+ */
+ public boolean answeringDisconnectsForegroundVideoCall() {
+ Bundle extras = getExtras();
+ if (extras == null
+ || !extras.containsKey(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL)) {
+ return false;
+ }
+ return extras.getBoolean(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL);
+ }
+
+ private void parseCallSpecificAppData() {
+ if (isExternalCall()) {
+ return;
+ }
+
+ mLogState.callSpecificAppData = CallIntentParser.getCallSpecificAppData(getIntentExtras());
+ if (mLogState.callSpecificAppData == null) {
+ mLogState.callSpecificAppData = new CallSpecificAppData();
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.EXTERNAL_INITIATION;
+ }
+ if (getState() == State.INCOMING) {
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.INCOMING_INITIATION;
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (mTelecomCall == null) {
+ // This should happen only in testing since otherwise we would never have a null
+ // Telecom call.
+ return String.valueOf(mId);
+ }
+
+ return String.format(
+ Locale.US,
+ "[%s, %s, %s, %s, children:%s, parent:%s, "
+ + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+ mId,
+ State.toString(getState()),
+ Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
+ Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()),
+ mChildCallIds,
+ getParentId(),
+ this.mTelecomCall.getConferenceableCalls(),
+ VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
+ mSessionModificationState,
+ getVideoSettings());
+ }
+
+ public String toSimpleString() {
+ return super.toString();
+ }
+
+ @CallHistoryStatus
+ public int getCallHistoryStatus() {
+ return mCallHistoryStatus;
+ }
+
+ public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
+ mCallHistoryStatus = callHistoryStatus;
+ }
+
+ public boolean didShowCameraPermission() {
+ return didShowCameraPermission;
+ }
+
+ public void setDidShowCameraPermission(boolean didShow) {
+ didShowCameraPermission = didShow;
+ }
+
+ public boolean isInGlobalSpamList() {
+ return isInGlobalSpamList;
+ }
+
+ public void setIsInGlobalSpamList(boolean inSpamList) {
+ isInGlobalSpamList = inSpamList;
+ }
+
+ public boolean isInUserSpamList() {
+ return isInUserSpamList;
+ }
+
+ public void setIsInUserSpamList(boolean inSpamList) {
+ isInUserSpamList = inSpamList;
+ }
+
+ public boolean isInUserWhiteList() {
+ return isInUserWhiteList;
+ }
+
+ public void setIsInUserWhiteList(boolean inWhiteList) {
+ isInUserWhiteList = inWhiteList;
+ }
+
+ public boolean isSpam() {
+ return mIsSpam;
+ }
+
+ public void setSpam(boolean isSpam) {
+ mIsSpam = isSpam;
+ }
+
+ public boolean isBlocked() {
+ return mIsBlocked;
+ }
+
+ public void setBlockedStatus(boolean isBlocked) {
+ mIsBlocked = isBlocked;
+ }
+
+ public boolean isRemotelyHeld() {
+ return isRemotelyHeld;
+ }
+
+ public boolean isIncoming() {
+ return mLogState.isIncoming;
+ }
+
+ public LatencyReport getLatencyReport() {
+ return mLatencyReport;
+ }
+
+ public void unregisterCallback() {
+ mTelecomCall.unregisterCallback(mTelecomCallCallback);
+ }
+
+ public void acceptUpgradeRequest(int videoState) {
+ LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
+ VideoProfile videoProfile = new VideoProfile(videoState);
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void declineUpgradeRequest() {
+ LogUtil.i("DialerCall.declineUpgradeRequest", "");
+ VideoProfile videoProfile = new VideoProfile(getVideoState());
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
+ LogUtil.i(
+ "DialerCall.phoneAccountSelected",
+ "accountHandle: %s, setDefault: %b",
+ accountHandle,
+ setDefault);
+ mTelecomCall.phoneAccountSelected(accountHandle, setDefault);
+ }
+
+ public void disconnect() {
+ LogUtil.i("DialerCall.disconnect", "");
+ setState(DialerCall.State.DISCONNECTING);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ mTelecomCall.disconnect();
+ }
+
+ public void hold() {
+ LogUtil.i("DialerCall.hold", "");
+ mTelecomCall.hold();
+ }
+
+ public void unhold() {
+ LogUtil.i("DialerCall.unhold", "");
+ mTelecomCall.unhold();
+ }
+
+ public void splitFromConference() {
+ LogUtil.i("DialerCall.splitFromConference", "");
+ mTelecomCall.splitFromConference();
+ }
+
+ public void answer(int videoState) {
+ LogUtil.i("DialerCall.answer", "videoState: " + videoState);
+ mTelecomCall.answer(videoState);
+ }
+
+ public void reject(boolean rejectWithMessage, String message) {
+ LogUtil.i("DialerCall.reject", "");
+ mTelecomCall.reject(rejectWithMessage, message);
+ }
+
+ /** Return the string label to represent the call provider */
+ public String getCallProviderLabel() {
+ if (callProviderLabel == null) {
+ PhoneAccount account = getPhoneAccount();
+ if (account != null && !TextUtils.isEmpty(account.getLabel())) {
+ List<PhoneAccountHandle> accounts =
+ mContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+ if (accounts != null && accounts.size() > 1) {
+ callProviderLabel = account.getLabel().toString();
+ }
+ }
+ if (callProviderLabel == null) {
+ callProviderLabel = "";
+ }
+ }
+ return callProviderLabel;
+ }
+
+ private PhoneAccount getPhoneAccount() {
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle == null) {
+ return null;
+ }
+ return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ }
+
+ public String getCallbackNumber() {
+ if (callbackNumber == null) {
+ // Show the emergency callback number if either:
+ // 1. This is an emergency call.
+ // 2. The phone is in Emergency Callback Mode, which means we should show the callback
+ // number.
+ boolean showCallbackNumber = hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
+
+ if (isEmergencyCall() || showCallbackNumber) {
+ callbackNumber = getSubscriptionNumber();
+ } else {
+ StatusHints statusHints = getTelecomCall().getDetails().getStatusHints();
+ if (statusHints != null) {
+ Bundle extras = statusHints.getExtras();
+ if (extras != null) {
+ callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
+ }
+ }
+ }
+
+ String simNumber =
+ mContext.getSystemService(TelecomManager.class).getLine1Number(getAccountHandle());
+ if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
+ LogUtil.v(
+ "DialerCall.getCallbackNumber",
+ "numbers are the same (and callback number is not being forced to show);"
+ + " not showing the callback number");
+ callbackNumber = "";
+ }
+ if (callbackNumber == null) {
+ callbackNumber = "";
+ }
+ }
+ return callbackNumber;
+ }
+
+ private String getSubscriptionNumber() {
+ // If it's an emergency call, and they're not populating the callback number,
+ // then try to fall back to the phone sub info (to hopefully get the SIM's
+ // number directly from the telephony layer).
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle != null) {
+ PhoneAccount account =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ if (account != null) {
+ return getNumberFromHandle(account.getSubscriptionAddress());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
+ * means there is no result.
+ */
+ @IntDef({
+ CALL_HISTORY_STATUS_UNKNOWN,
+ CALL_HISTORY_STATUS_PRESENT,
+ CALL_HISTORY_STATUS_NOT_PRESENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallHistoryStatus {}
+
+ /* Defines different states of this call */
+ public static class State {
+
+ public static final int INVALID = 0;
+ public static final int NEW = 1; /* The call is new. */
+ public static final int IDLE = 2; /* The call is idle. Nothing active */
+ public static final int ACTIVE = 3; /* There is an active call */
+ public static final int INCOMING = 4; /* A normal incoming phone call */
+ public static final int CALL_WAITING = 5; /* Incoming call while another is active */
+ public static final int DIALING = 6; /* An outgoing call during dial phase */
+ public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */
+ public static final int ONHOLD = 8; /* An active phone call placed on hold */
+ public static final int DISCONNECTING = 9; /* A call is being ended. */
+ public static final int DISCONNECTED = 10; /* State after a call disconnects */
+ public static final int CONFERENCED = 11; /* DialerCall part of a conference call */
+ public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */
+ public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */
+ public static final int BLOCKED = 14; /* The number was found on the block list */
+ public static final int PULLING = 15; /* An external call being pulled to the device */
+
+ public static boolean isConnectingOrConnected(int state) {
+ switch (state) {
+ case ACTIVE:
+ case INCOMING:
+ case CALL_WAITING:
+ case CONNECTING:
+ case DIALING:
+ case PULLING:
+ case REDIALING:
+ case ONHOLD:
+ case CONFERENCED:
+ return true;
+ default:
+ }
+ return false;
+ }
+
+ public static boolean isDialing(int state) {
+ return state == DIALING || state == PULLING || state == REDIALING;
+ }
+
+ public static String toString(int state) {
+ switch (state) {
+ case INVALID:
+ return "INVALID";
+ case NEW:
+ return "NEW";
+ case IDLE:
+ return "IDLE";
+ case ACTIVE:
+ return "ACTIVE";
+ case INCOMING:
+ return "INCOMING";
+ case CALL_WAITING:
+ return "CALL_WAITING";
+ case DIALING:
+ return "DIALING";
+ case PULLING:
+ return "PULLING";
+ case REDIALING:
+ return "REDIALING";
+ case ONHOLD:
+ return "ONHOLD";
+ case DISCONNECTING:
+ return "DISCONNECTING";
+ case DISCONNECTED:
+ return "DISCONNECTED";
+ case CONFERENCED:
+ return "CONFERENCED";
+ case SELECT_PHONE_ACCOUNT:
+ return "SELECT_PHONE_ACCOUNT";
+ case CONNECTING:
+ return "CONNECTING";
+ case BLOCKED:
+ return "BLOCKED";
+ default:
+ return "UNKNOWN";
+ }
+ }
+ }
+
+ /**
+ * Defines different states of session modify requests, which are used to upgrade to video, or
+ * downgrade to audio.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_MODIFICATION_STATE_NO_REQUEST,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+ SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+ })
+ public @interface SessionModificationState {}
+
+ public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+ public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+
+ public static class VideoSettings {
+
+ public static final int CAMERA_DIRECTION_UNKNOWN = -1;
+ public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
+ public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
+
+ private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+
+ /**
+ * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public int getCameraDir() {
+ return mCameraDirection;
+ }
+
+ /**
+ * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public void setCameraDir(int cameraDirection) {
+ if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
+ || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
+ mCameraDirection = cameraDirection;
+ } else {
+ mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(CameraDir:" + getCameraDir() + ")";
+ }
+ }
+
+ /**
+ * Tracks any state variables that is useful for logging. There is some amount of overlap with
+ * existing call member variables, but this duplication helps to ensure that none of these logging
+ * variables will interface with/and affect call logic.
+ */
+ public static class LogState {
+
+ public DisconnectCause disconnectCause;
+ public boolean isIncoming = false;
+ public int contactLookupResult = ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE;
+ public CallSpecificAppData callSpecificAppData;
+ // If this was a conference call, the total number of calls involved in the conference.
+ public int conferencedCalls = 0;
+ public long duration = 0;
+ public boolean isLogged = false;
+
+ private static String lookupToString(int lookupType) {
+ switch (lookupType) {
+ case ContactLookupResult.Type.LOCAL_CONTACT:
+ return "Local";
+ case ContactLookupResult.Type.LOCAL_CACHE:
+ return "Cache";
+ case ContactLookupResult.Type.REMOTE:
+ return "Remote";
+ case ContactLookupResult.Type.EMERGENCY:
+ return "Emergency";
+ case ContactLookupResult.Type.VOICEMAIL:
+ return "Voicemail";
+ default:
+ return "Not found";
+ }
+ }
+
+ private static String initiationToString(CallSpecificAppData callSpecificAppData) {
+ if (callSpecificAppData == null) {
+ return "null";
+ }
+ switch (callSpecificAppData.callInitiationType) {
+ case CallInitiationType.Type.INCOMING_INITIATION:
+ return "Incoming";
+ case CallInitiationType.Type.DIALPAD:
+ return "Dialpad";
+ case CallInitiationType.Type.SPEED_DIAL:
+ return "Speed Dial";
+ case CallInitiationType.Type.REMOTE_DIRECTORY:
+ return "Remote Directory";
+ case CallInitiationType.Type.SMART_DIAL:
+ return "Smart Dial";
+ case CallInitiationType.Type.REGULAR_SEARCH:
+ return "Regular Search";
+ case CallInitiationType.Type.CALL_LOG:
+ return "DialerCall Log";
+ case CallInitiationType.Type.CALL_LOG_FILTER:
+ return "DialerCall Log Filter";
+ case CallInitiationType.Type.VOICEMAIL_LOG:
+ return "Voicemail Log";
+ case CallInitiationType.Type.CALL_DETAILS:
+ return "DialerCall Details";
+ case CallInitiationType.Type.QUICK_CONTACTS:
+ return "Quick Contacts";
+ case CallInitiationType.Type.EXTERNAL_INITIATION:
+ return "External";
+ case CallInitiationType.Type.LAUNCHER_SHORTCUT:
+ return "Launcher Shortcut";
+ default:
+ return "Unknown: " + callSpecificAppData.callInitiationType;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "["
+ + "%s, " // DisconnectCause toString already describes the object type
+ + "isIncoming: %s, "
+ + "contactLookup: %s, "
+ + "callInitiation: %s, "
+ + "duration: %s"
+ + "]",
+ disconnectCause,
+ isIncoming,
+ lookupToString(contactLookupResult),
+ initiationToString(callSpecificAppData),
+ duration);
+ }
+ }
+
+ /** Called when canned text responses have been loaded. */
+ public interface CannedTextResponsesLoadedListener {
+ void onCannedTextResponsesLoaded(DialerCall call);
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCallDelegate.java b/java/com/android/incallui/call/DialerCallDelegate.java
new file mode 100644
index 000000000..463b4916a
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallDelegate.java
@@ -0,0 +1,25 @@
+/*
+ * 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.call;
+
+import android.telecom.Call;
+
+/** Callback from the call module to the container. */
+public interface DialerCallDelegate {
+
+ DialerCall getDialerCallFromTelecomCall(Call telecomCall);
+}
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
new file mode 100644
index 000000000..b426cd72e
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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.call;
+
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Used to monitor state changes in a dialer call. */
+public interface DialerCallListener {
+
+ void onDialerCallDisconnect();
+
+ void onDialerCallUpdate();
+
+ void onDialerCallChildNumberChange();
+
+ void onDialerCallLastForwardedNumberChange();
+
+ void onDialerCallUpgradeToVideo();
+
+ void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+
+ void onWiFiToLteHandover();
+
+ void onHandoverToWifiFailure();
+}
diff --git a/java/com/android/incallui/call/ExternalCallList.java b/java/com/android/incallui/call/ExternalCallList.java
new file mode 100644
index 000000000..52a7a304b
--- /dev/null
+++ b/java/com/android/incallui/call/ExternalCallList.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import android.util.ArraySet;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks the external calls known to the InCall UI.
+ *
+ * <p>External calls are those with {@code android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}.
+ */
+public class ExternalCallList {
+
+ private final Set<Call> mExternalCalls = new ArraySet<>();
+ private final Set<ExternalCallListener> mExternalCallListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<ExternalCallListener, Boolean>(8, 0.9f, 1));
+ /** Handles {@link android.telecom.Call.Callback} callbacks. */
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ notifyExternalCallUpdated(call);
+ }
+ };
+
+ /** Begins tracking an external call and notifies listeners of the new call. */
+ public void onCallAdded(Call telecomCall) {
+ if (!telecomCall.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ throw new IllegalArgumentException();
+ }
+ mExternalCalls.add(telecomCall);
+ telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper()));
+ notifyExternalCallAdded(telecomCall);
+ }
+
+ /** Stops tracking an external call and notifies listeners of the removal of the call. */
+ public void onCallRemoved(Call telecomCall) {
+ if (!mExternalCalls.contains(telecomCall)) {
+ // This can happen on M for external calls from blocked numbers
+ LogUtil.i("ExternalCallList.onCallRemoved", "attempted to remove unregistered call");
+ return;
+ }
+ mExternalCalls.remove(telecomCall);
+ telecomCall.unregisterCallback(mTelecomCallCallback);
+ notifyExternalCallRemoved(telecomCall);
+ }
+
+ /** Adds a new listener to external call events. */
+ public void addExternalCallListener(@NonNull ExternalCallListener listener) {
+ mExternalCallListeners.add(listener);
+ }
+
+ /** Removes a listener to external call events. */
+ public void removeExternalCallListener(@NonNull ExternalCallListener listener) {
+ if (!mExternalCallListeners.contains(listener)) {
+ LogUtil.i(
+ "ExternalCallList.removeExternalCallListener",
+ "attempt to remove unregistered listener.");
+ }
+ mExternalCallListeners.remove(listener);
+ }
+
+ public boolean isCallTracked(@NonNull android.telecom.Call telecomCall) {
+ return mExternalCalls.contains(telecomCall);
+ }
+
+ /** Notifies listeners of the addition of a new external call. */
+ private void notifyExternalCallAdded(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallAdded(call);
+ }
+ }
+
+ /** Notifies listeners of the removal of an external call. */
+ private void notifyExternalCallRemoved(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallRemoved(call);
+ }
+ }
+
+ /** Notifies listeners of changes to an external call. */
+ private void notifyExternalCallUpdated(Call call) {
+ if (!call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ // A previous external call has been pulled and is now a regular call, so we will remove
+ // it from the external call listener and ensure that the CallList is informed of the
+ // change.
+ onCallRemoved(call);
+
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallPulled(call);
+ }
+ } else {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallUpdated(call);
+ }
+ }
+ }
+
+ /**
+ * Defines events which the {@link ExternalCallList} exposes to interested components (e.g. {@link
+ * com.android.incallui.ExternalCallNotifier ExternalCallNotifier}).
+ */
+ public interface ExternalCallListener {
+
+ void onExternalCallAdded(Call call);
+
+ void onExternalCallRemoved(Call call);
+
+ void onExternalCallUpdated(Call call);
+
+ void onExternalCallPulled(Call call);
+ }
+}
diff --git a/java/com/android/incallui/call/InCallServiceListener.java b/java/com/android/incallui/call/InCallServiceListener.java
new file mode 100644
index 000000000..e48ce9d79
--- /dev/null
+++ b/java/com/android/incallui/call/InCallServiceListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.telecom.InCallService;
+
+/**
+ * Interface implemented by In-Call components that maintain a reference to the Telecom API {@code
+ * InCallService} object. Clarifies the expectations associated with the relevant method calls.
+ */
+public interface InCallServiceListener {
+
+ /**
+ * Called once at {@code InCallService} startup time with a valid instance. At that time, there
+ * will be no existing {@code DialerCall}s.
+ *
+ * @param inCallService The {@code InCallService} object.
+ */
+ void setInCallService(InCallService inCallService);
+
+ /**
+ * Called once at {@code InCallService} shutdown time. At that time, any {@code DialerCall}s will
+ * have transitioned through the disconnected state and will no longer exist.
+ */
+ void clearInCallService();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindings.java b/java/com/android/incallui/call/InCallUiLegacyBindings.java
new file mode 100644
index 000000000..1b0ed4542
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindings.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/**
+ * These are old bindings between InCallUi and the container application. All new bindings should be
+ * added to the bindings module and not here.
+ */
+public interface InCallUiLegacyBindings {
+
+ void logCall(DialerCall call);
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
new file mode 100644
index 000000000..8604976f7
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the in call UI
+ * module to get references to the InCallUiLegacyBindings.
+ */
+public interface InCallUiLegacyBindingsFactory {
+
+ InCallUiLegacyBindings newInCallUiLegacyBindings();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
new file mode 100644
index 000000000..8869c64b2
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+/** Default implementation for in call UI legacy bindings. */
+public class InCallUiLegacyBindingsStub implements InCallUiLegacyBindings {
+
+ @Override
+ public void logCall(DialerCall call) {}
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
new file mode 100644
index 000000000..f897ac9dd
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallback.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Implements the InCallUI VideoCall Callback. */
+public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
+
+ private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+
+ private final DialerCall call;
+ @Nullable private Handler handler;
+ @SessionModificationState private int newSessionModificationState;
+
+ public InCallVideoCallCallback(DialerCall call) {
+ this.call = call;
+ }
+
+ @Override
+ public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+ int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
+ int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
+
+ boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
+ boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
+
+ if (wasVideoCall && !isVideoCall) {
+ LogUtil.v(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived",
+ "call downgraded to " + newVideoState);
+ } else if (previousVideoState != newVideoState) {
+ InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
+ }
+ }
+
+ /**
+ * @param status Status of the session modify request. Valid values are {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+ * @param responseProfile The actual profile changes made by the peer device.
+ */
+ @Override
+ public void onSessionModifyResponseReceived(
+ int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "status: %d, "
+ + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
+ status,
+ requestedProfile,
+ responseProfile,
+ call.getSessionModificationState());
+
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+ if (handler == null) {
+ handler = new Handler();
+ } else {
+ handler.removeCallbacks(this);
+ }
+
+ newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
+ if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+ // This will update the video UI to display the error message.
+ call.setSessionModificationState(newSessionModificationState);
+ }
+
+ // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+ // to stay up so that the user can read the error message.
+ //
+ // If the other person accepted the upgrade request then this will keep the video UI up until
+ // the call's video state change. Without this we would switch to the voice call and then
+ // switch back to video UI.
+ handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+ call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "call is not waiting for " + "response, doing nothing");
+ }
+ }
+
+ @SessionModificationState
+ private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
+ switch (telecomStatus) {
+ case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+ return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+ case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+ // Check if it's already video call, which means the request is not video upgrade request.
+ if (VideoUtils.isVideoCall(call.getVideoState())) {
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ } else {
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+ }
+ case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+ default:
+ LogUtil.e(
+ "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
+ "unknown status: %d",
+ telecomStatus);
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ }
+ }
+
+ @Override
+ public void onCallSessionEvent(int event) {
+ InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
+ }
+
+ @Override
+ public void onVideoQualityChanged(int videoQuality) {
+ InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
+ }
+
+ /**
+ * Handles a change to the call data usage. No implementation as the in-call UI does not display
+ * data usage.
+ *
+ * @param dataUsage The updated data usage.
+ */
+ @Override
+ public void onCallDataUsageChanged(long dataUsage) {
+ LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
+ InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
+ }
+
+ /**
+ * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
+ * use of camera capabilities.
+ *
+ * @param cameraCapabilities The changed camera capabilities.
+ */
+ @Override
+ public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+ if (cameraCapabilities != null) {
+ InCallVideoCallCallbackNotifier.getInstance()
+ .cameraDimensionsChanged(
+ call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+ }
+ }
+
+ /**
+ * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
+ * clear the session modify state.
+ */
+ @Override
+ public void run() {
+ if (call.getSessionModificationState() == newSessionModificationState) {
+ LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "session modification state has changed, not clearing state");
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
new file mode 100644
index 000000000..4a949263c
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
+ * events.
+ */
+public class InCallVideoCallCallbackNotifier {
+
+ /** Singleton instance of this class. */
+ private static InCallVideoCallCallbackNotifier sInstance = new InCallVideoCallCallbackNotifier();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<SessionModificationListener> mSessionModificationListeners =
+ Collections.newSetFromMap(
+ new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
+
+ private final Set<VideoEventListener> mVideoEventListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
+ private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
+
+ /** Private constructor. Instance should only be acquired through getInstance(). */
+ private InCallVideoCallCallbackNotifier() {}
+
+ /** Static singleton accessor method. */
+ public static InCallVideoCallCallbackNotifier getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Adds a new {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
+ Objects.requireNonNull(listener);
+ mSessionModificationListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
+ if (listener != null) {
+ mSessionModificationListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addVideoEventListener(@NonNull VideoEventListener listener) {
+ Objects.requireNonNull(listener);
+ mVideoEventListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeVideoEventListener(@Nullable VideoEventListener listener) {
+ if (listener != null) {
+ mVideoEventListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSurfaceChangeListener(@NonNull SurfaceChangeListener listener) {
+ Objects.requireNonNull(listener);
+ mSurfaceChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSurfaceChangeListener(@Nullable SurfaceChangeListener listener) {
+ if (listener != null) {
+ mSurfaceChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Inform listeners of an upgrade to video request for a call.
+ *
+ * @param call The call.
+ * @param videoState The video state we want to upgrade to.
+ */
+ public void upgradeToVideoRequest(DialerCall call, int videoState) {
+ LogUtil.v(
+ "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
+ "call = " + call + " new video state = " + videoState);
+ for (SessionModificationListener listener : mSessionModificationListeners) {
+ listener.onUpgradeToVideoRequest(call, videoState);
+ }
+ }
+
+ /**
+ * Inform listeners of a call session event.
+ *
+ * @param event The call session event.
+ */
+ public void callSessionEvent(int event) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallSessionEvent(event);
+ }
+ }
+
+ /**
+ * Inform listeners of a downgrade to audio.
+ *
+ * @param call The call.
+ * @param paused The paused state.
+ */
+ public void peerPausedStateChanged(DialerCall call, boolean paused) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onPeerPauseStateChanged(call, paused);
+ }
+ }
+
+ /**
+ * Inform listeners of any change in the video quality of the call
+ *
+ * @param call The call.
+ * @param videoQuality The updated video quality of the call.
+ */
+ public void videoQualityChanged(DialerCall call, int videoQuality) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onVideoQualityChanged(call, videoQuality);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to peer dimensions.
+ *
+ * @param call The call.
+ * @param width New peer width.
+ * @param height New peer height.
+ */
+ public void peerDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onUpdatePeerDimensions(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to camera dimensions.
+ *
+ * @param call The call.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ public void cameraDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onCameraDimensionsChange(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to call data usage.
+ *
+ * @param dataUsage data usage value
+ */
+ public void callDataUsageChanged(long dataUsage) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallDataUsageChange(dataUsage);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of upgrade to video request. */
+ public interface SessionModificationListener {
+
+ /**
+ * Called when a peer request is received to upgrade an audio-only call to a video call.
+ *
+ * @param call The call the request was received for.
+ * @param videoState The requested video state.
+ */
+ void onUpgradeToVideoRequest(DialerCall call, int videoState);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of video events, including pause and
+ * un-pause of peer video, video quality changes.
+ */
+ public interface VideoEventListener {
+
+ /**
+ * Called when the peer pauses or un-pauses video transmission.
+ *
+ * @param call The call which paused or un-paused video transmission.
+ * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
+ */
+ void onPeerPauseStateChanged(DialerCall call, boolean paused);
+
+ /**
+ * Called when the video quality changes.
+ *
+ * @param call The call whose video quality changes.
+ * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
+ */
+ void onVideoQualityChanged(DialerCall call, int videoCallQuality);
+
+ /*
+ * Called when call data usage value is requested or when call data usage value is updated
+ * because of a call state change
+ *
+ * @param dataUsage call data usage value
+ */
+ void onCallDataUsageChange(long dataUsage);
+
+ /**
+ * Called when call session event is raised.
+ *
+ * @param event The call session event.
+ */
+ void onCallSessionEvent(int event);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of changes to the video surfaces.
+ */
+ public interface SurfaceChangeListener {
+
+ /**
+ * Called when the peer video feed changes dimensions. This can occur when the peer rotates
+ * their device, changing the aspect ratio of the video signal.
+ *
+ * @param call The call which experienced a peer video
+ */
+ void onUpdatePeerDimensions(DialerCall call, int width, int height);
+
+ /**
+ * Called when the local camera changes dimensions. This occurs when a change in camera occurs.
+ *
+ * @param call The call which experienced the camera dimension change.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ void onCameraDimensionsChange(DialerCall call, int width, int height);
+ }
+}
diff --git a/java/com/android/incallui/call/TelecomAdapter.java b/java/com/android/incallui/call/TelecomAdapter.java
new file mode 100644
index 000000000..ebf4ecf4f
--- /dev/null
+++ b/java/com/android/incallui/call/TelecomAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.telecom.InCallService;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/** Wrapper around Telecom APIs. */
+public final class TelecomAdapter implements InCallServiceListener {
+
+ private static final String ADD_CALL_MODE_KEY = "add_call_mode";
+
+ private static TelecomAdapter sInstance;
+ private InCallService mInCallService;
+
+ private TelecomAdapter() {}
+
+ @MainThread
+ public static TelecomAdapter getInstance() {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException();
+ }
+ if (sInstance == null) {
+ sInstance = new TelecomAdapter();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void setInCallService(InCallService inCallService) {
+ mInCallService = inCallService;
+ }
+
+ @Override
+ public void clearInCallService() {
+ mInCallService = null;
+ }
+
+ private android.telecom.Call getTelecomCallById(String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ return call == null ? null : call.getTelecomCall();
+ }
+
+ public void mute(boolean shouldMute) {
+ if (mInCallService != null) {
+ mInCallService.setMuted(shouldMute);
+ } else {
+ LogUtil.e("TelecomAdapter.mute", "mInCallService is null");
+ }
+ }
+
+ public void setAudioRoute(int route) {
+ if (mInCallService != null) {
+ mInCallService.setAudioRoute(route);
+ } else {
+ LogUtil.e("TelecomAdapter.setAudioRoute", "mInCallService is null");
+ }
+ }
+
+ public void merge(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ List<android.telecom.Call> conferenceable = call.getConferenceableCalls();
+ if (!conferenceable.isEmpty()) {
+ call.conference(conferenceable.get(0));
+ } else {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) {
+ call.mergeConference();
+ }
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.merge", "call not in call list " + callId);
+ }
+ }
+
+ public void swap(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) {
+ call.swapConference();
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.swap", "call not in call list " + callId);
+ }
+ }
+
+ public void addCall() {
+ if (mInCallService != null) {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // when we request the dialer come up, we also want to inform
+ // it that we're going through the "add call" option from the
+ // InCallScreen / PhoneUtils.
+ intent.putExtra(ADD_CALL_MODE_KEY, true);
+ try {
+ LogUtil.d("TelecomAdapter.addCall", "Sending the add DialerCall intent");
+ mInCallService.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // This is rather rare but possible.
+ // Note: this method is used even when the phone is encrypted. At that moment
+ // the system may not find any Activity which can accept this Intent.
+ LogUtil.e("TelecomAdapter.addCall", "Activity for adding calls isn't found.", e);
+ }
+ }
+ }
+
+ public void playDtmfTone(String callId, char digit) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.playDtmfTone(digit);
+ } else {
+ LogUtil.e("TelecomAdapter.playDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void stopDtmfTone(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.stopDtmfTone();
+ } else {
+ LogUtil.e("TelecomAdapter.stopDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void postDialContinue(String callId, boolean proceed) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.postDialContinue(proceed);
+ } else {
+ LogUtil.e("TelecomAdapter.postDialContinue", "call not in call list " + callId);
+ }
+ }
+
+ public boolean canAddCall() {
+ if (mInCallService != null) {
+ return mInCallService.canAddCall();
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
new file mode 100644
index 000000000..80fbfb1cc
--- /dev/null
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.call;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.telecom.VideoProfile;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.DialerUtils;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Objects;
+
+public class VideoUtils {
+
+ private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
+
+ public static boolean isVideoCall(@Nullable DialerCall call) {
+ return call != null && isVideoCall(call.getVideoState());
+ }
+
+ public static boolean isVideoCall(int videoState) {
+ return CompatUtils.isVideoCompatible()
+ && (VideoProfile.isTransmissionEnabled(videoState)
+ || VideoProfile.isReceptionEnabled(videoState));
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ }
+
+ public static boolean isBidirectionalVideoCall(DialerCall call) {
+ return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+ }
+
+ public static boolean isTransmissionEnabled(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ return VideoProfile.isTransmissionEnabled(call.getVideoState());
+ }
+
+ public static boolean isIncomingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+ }
+
+ public static boolean isActiveVideoCall(DialerCall call) {
+ return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static boolean isOutgoingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return DialerCall.State.isDialing(state)
+ || state == DialerCall.State.CONNECTING
+ || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+ }
+
+ public static boolean isAudioCall(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return true;
+ }
+
+ return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+ }
+
+ // TODO (ims-vt) Check if special handling is needed for CONF calls.
+ public static boolean canVideoPause(DialerCall call) {
+ return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ if (VideoProfile.isAudioOnly(call.getVideoState())) {
+ throw new IllegalStateException();
+ }
+ return new VideoProfile(getPausedVideoState(call.getVideoState()));
+ }
+
+ public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
+ }
+
+ public static int getUnPausedVideoState(int videoState) {
+ return videoState & (~VideoProfile.STATE_PAUSED);
+ }
+
+ public static int getPausedVideoState(int videoState) {
+ return videoState | VideoProfile.STATE_PAUSED;
+ }
+
+ public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
+ return isCameraAllowedByUser(context) && hasCameraPermission(context);
+ }
+
+ public static boolean hasCameraPermission(@NonNull Context context) {
+ return ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isCameraAllowedByUser(@NonNull Context context) {
+ return DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .getBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, false);
+ }
+
+ public static void setCameraAllowedByUser(@NonNull Context context) {
+ DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .edit()
+ .putBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, true)
+ .apply();
+ }
+}