blob: 3ff1d269d0efe4a895df28aad74873df9e52790c [file] [log] [blame]
/*
* 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;
import android.app.ActivityManager;
import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.TaskDescription;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.GradientDrawable.Orientation;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.graphics.ColorUtils;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.CheckBox;
import android.widget.Toast;
import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
import com.android.dialer.animation.AnimUtils;
import com.android.dialer.animation.AnimationListenerAdapter;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.common.concurrent.ThreadUtil;
import com.android.dialer.common.concurrent.UiListener;
import com.android.dialer.configprovider.ConfigProviderComponent;
import com.android.dialer.logging.Logger;
import com.android.dialer.logging.ScreenEvent;
import com.android.dialer.metrics.Metrics;
import com.android.dialer.metrics.MetricsComponent;
import com.android.dialer.preferredsim.PreferredAccountRecorder;
import com.android.dialer.preferredsim.PreferredAccountWorker;
import com.android.dialer.preferredsim.PreferredAccountWorker.Result;
import com.android.dialer.preferredsim.PreferredSimComponent;
import com.android.dialer.util.ViewUtil;
import com.android.incallui.answer.bindings.AnswerBindings;
import com.android.incallui.answer.protocol.AnswerScreen;
import com.android.incallui.answer.protocol.AnswerScreenDelegate;
import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
import com.android.incallui.answerproximitysensor.PseudoScreenState;
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.TelecomAdapter;
import com.android.incallui.call.state.DialerCallState;
import com.android.incallui.callpending.CallPendingActivity;
import com.android.incallui.disconnectdialog.DisconnectMessage;
import com.android.incallui.incall.bindings.InCallBindings;
import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
import com.android.incallui.incall.protocol.InCallScreen;
import com.android.incallui.incall.protocol.InCallScreenDelegate;
import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
import com.android.incallui.incalluilock.InCallUiLock;
import com.android.incallui.rtt.bindings.RttBindings;
import com.android.incallui.rtt.protocol.RttCallScreen;
import com.android.incallui.rtt.protocol.RttCallScreenDelegate;
import com.android.incallui.rtt.protocol.RttCallScreenDelegateFactory;
import com.android.incallui.speakeasy.SpeakEasyCallManager;
import com.android.incallui.telecomeventui.InternationalCallOnWifiDialogFragment;
import com.android.incallui.video.bindings.VideoBindings;
import com.android.incallui.video.protocol.VideoCallScreen;
import com.android.incallui.video.protocol.VideoCallScreenDelegate;
import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** Version of {@link InCallActivity} that shows the new UI */
public class InCallActivity extends TransactionSafeFragmentActivity
implements AnswerScreenDelegateFactory,
InCallScreenDelegateFactory,
InCallButtonUiDelegateFactory,
VideoCallScreenDelegateFactory,
RttCallScreenDelegateFactory,
PseudoScreenState.StateChangedListener {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DIALPAD_REQUEST_NONE,
DIALPAD_REQUEST_SHOW,
DIALPAD_REQUEST_HIDE,
})
@interface DialpadRequestType {}
private static final int DIALPAD_REQUEST_NONE = 1;
private static final int DIALPAD_REQUEST_SHOW = 2;
private static final int DIALPAD_REQUEST_HIDE = 3;
private static Optional<Integer> audioRouteForTesting = Optional.empty();
private SelectPhoneAccountListener selectPhoneAccountListener;
private UiListener<Result> preferredAccountWorkerResultListener;
private Animation dialpadSlideInAnimation;
private Animation dialpadSlideOutAnimation;
private Dialog errorDialog;
private GradientDrawable backgroundDrawable;
private InCallOrientationEventListener inCallOrientationEventListener;
private View pseudoBlackScreenOverlay;
private SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment;
private String dtmfTextToPrepopulate;
private boolean allowOrientationChange;
private boolean animateDialpadOnShow;
private boolean didShowAnswerScreen;
private boolean didShowInCallScreen;
private boolean didShowVideoCallScreen;
private boolean didShowRttCallScreen;
private boolean didShowSpeakEasyScreen;
private String lastShownSpeakEasyScreenUniqueCallid = "";
private boolean dismissKeyguard;
private boolean isInShowMainInCallFragment;
private boolean isRecreating; // whether the activity is going to be recreated
private boolean isVisible;
private boolean needDismissPendingDialogs;
private boolean touchDownWhenPseudoScreenOff;
private int[] backgroundDrawableColors;
@DialpadRequestType private int showDialpadRequest = DIALPAD_REQUEST_NONE;
private SpeakEasyCallManager speakEasyCallManager;
private DialogFragment rttRequestDialogFragment;
public static Intent getIntent(
Context context, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(context, InCallActivity.class);
if (showDialpad) {
intent.putExtra(IntentExtraNames.SHOW_DIALPAD, true);
}
intent.putExtra(IntentExtraNames.NEW_OUTGOING_CALL, newOutgoingCall);
intent.putExtra(IntentExtraNames.FOR_FULL_SCREEN, isForFullScreen);
return intent;
}
@Override
protected void onResumeFragments() {
super.onResumeFragments();
if (needDismissPendingDialogs) {
dismissPendingDialogs();
}
}
@Override
protected void onCreate(Bundle bundle) {
Trace.beginSection("InCallActivity.onCreate");
super.onCreate(bundle);
preferredAccountWorkerResultListener =
DialerExecutorComponent.get(this)
.createUiListener(getFragmentManager(), "preferredAccountWorkerResultListener");
selectPhoneAccountListener = new SelectPhoneAccountListener(getApplicationContext());
if (bundle != null) {
didShowAnswerScreen = bundle.getBoolean(KeysForSavedInstance.DID_SHOW_ANSWER_SCREEN);
didShowInCallScreen = bundle.getBoolean(KeysForSavedInstance.DID_SHOW_IN_CALL_SCREEN);
didShowVideoCallScreen = bundle.getBoolean(KeysForSavedInstance.DID_SHOW_VIDEO_CALL_SCREEN);
didShowRttCallScreen = bundle.getBoolean(KeysForSavedInstance.DID_SHOW_RTT_CALL_SCREEN);
didShowSpeakEasyScreen = bundle.getBoolean(KeysForSavedInstance.DID_SHOW_SPEAK_EASY_SCREEN);
}
setWindowFlags();
setContentView(R.layout.incall_screen);
internalResolveIntent(getIntent());
boolean isLandscape =
getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
boolean isRtl = ViewUtil.isRtl();
if (isLandscape) {
dialpadSlideInAnimation =
AnimationUtils.loadAnimation(
this, isRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
dialpadSlideOutAnimation =
AnimationUtils.loadAnimation(
this, isRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
} else {
dialpadSlideInAnimation = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom);
dialpadSlideOutAnimation =
AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom);
}
dialpadSlideInAnimation.setInterpolator(AnimUtils.EASE_IN);
dialpadSlideOutAnimation.setInterpolator(AnimUtils.EASE_OUT);
dialpadSlideOutAnimation.setAnimationListener(
new AnimationListenerAdapter() {
@Override
public void onAnimationEnd(Animation animation) {
hideDialpadFragment();
}
});
if (bundle != null && showDialpadRequest == DIALPAD_REQUEST_NONE) {
// If the dialpad was shown before, set related variables so that it can be shown and
// populated with the previous DTMF text during onResume().
if (bundle.containsKey(IntentExtraNames.SHOW_DIALPAD)) {
boolean showDialpad = bundle.getBoolean(IntentExtraNames.SHOW_DIALPAD);
showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_HIDE;
animateDialpadOnShow = false;
}
dtmfTextToPrepopulate = bundle.getString(KeysForSavedInstance.DIALPAD_TEXT);
SelectPhoneAccountDialogFragment selectPhoneAccountDialogFragment =
(SelectPhoneAccountDialogFragment)
getFragmentManager().findFragmentByTag(Tags.SELECT_ACCOUNT_FRAGMENT);
if (selectPhoneAccountDialogFragment != null) {
selectPhoneAccountDialogFragment.setListener(selectPhoneAccountListener);
}
}
inCallOrientationEventListener = new InCallOrientationEventListener(this);
getWindow()
.getDecorView()
.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
pseudoBlackScreenOverlay = findViewById(R.id.psuedo_black_screen_overlay);
sendBroadcast(CallPendingActivity.getFinishBroadcast());
Trace.endSection();
MetricsComponent.get(this)
.metrics()
.stopTimer(Metrics.ON_CALL_ADDED_TO_ON_INCALL_UI_SHOWN_INCOMING);
MetricsComponent.get(this)
.metrics()
.stopTimer(Metrics.ON_CALL_ADDED_TO_ON_INCALL_UI_SHOWN_OUTGOING);
}
private void setWindowFlags() {
// Allow the activity to be shown when the screen is locked and filter out touch events that are
// "too fat".
int flags =
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
// When the audio stream is not via Bluetooth, turn on the screen once the activity is shown.
// When the audio stream is via Bluetooth, turn on the screen only for an incoming call.
final int audioRoute = getAudioRoute();
if (audioRoute != CallAudioState.ROUTE_BLUETOOTH
|| CallList.getInstance().getIncomingCall() != null) {
flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
}
getWindow().addFlags(flags);
}
private static int getAudioRoute() {
if (audioRouteForTesting.isPresent()) {
return audioRouteForTesting.get();
}
return AudioModeProvider.getInstance().getAudioState().getRoute();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static void setAudioRouteForTesting(int audioRoute) {
audioRouteForTesting = Optional.of(audioRoute);
}
private void internalResolveIntent(Intent intent) {
if (!intent.getAction().equals(Intent.ACTION_MAIN)) {
return;
}
if (intent.hasExtra(IntentExtraNames.SHOW_DIALPAD)) {
// IntentExtraNames.SHOW_DIALPAD can be used to specify whether the DTMF dialpad should be
// initially visible. If the extra is absent, leave the dialpad in its previous state.
boolean showDialpad = intent.getBooleanExtra(IntentExtraNames.SHOW_DIALPAD, false);
relaunchedFromDialer(showDialpad);
}
DialerCall outgoingCall = CallList.getInstance().getOutgoingCall();
if (outgoingCall == null) {
outgoingCall = CallList.getInstance().getPendingOutgoingCall();
}
if (intent.getBooleanExtra(IntentExtraNames.NEW_OUTGOING_CALL, false)) {
intent.removeExtra(IntentExtraNames.NEW_OUTGOING_CALL);
// InCallActivity is responsible for disconnecting a new outgoing call if there is no way of
// making it (i.e. no valid call capable accounts).
if (InCallPresenter.isCallWithNoValidAccounts(outgoingCall)) {
LogUtil.i(
"InCallActivity.internalResolveIntent", "Call with no valid accounts, disconnecting");
outgoingCall.disconnect();
}
dismissKeyguard(true);
}
if (showPhoneAccountSelectionDialog()) {
hideMainInCallFragment();
}
}
/**
* When relaunching from the dialer app, {@code showDialpad} indicates whether the dialpad should
* be shown on launch.
*
* @param showDialpad {@code true} to indicate the dialpad should be shown on launch, and {@code
* false} to indicate no change should be made to the dialpad visibility.
*/
private void relaunchedFromDialer(boolean showDialpad) {
showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_NONE;
animateDialpadOnShow = true;
if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
// If there's only one line in use, AND it's on hold, then we're sure the user
// wants to use the dialpad toward the exact line, so un-hold the holding line.
DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
if (call != null && call.getState() == DialerCallState.ONHOLD) {
call.unhold();
}
}
}
/**
* Show a phone account selection dialog if there is a call waiting for phone account selection.
*
* @return true if the dialog was shown.
*/
private boolean showPhoneAccountSelectionDialog() {
DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
if (waitingForAccountCall == null) {
return false;
}
PreferredAccountWorker preferredAccountWorker =
PreferredSimComponent.get(this).preferredAccountWorker();
Bundle extras = waitingForAccountCall.getIntentExtras();
List<PhoneAccountHandle> phoneAccountHandles =
extras == null
? new ArrayList<>()
: extras.getParcelableArrayList(Call.AVAILABLE_PHONE_ACCOUNTS);
ListenableFuture<PreferredAccountWorker.Result> preferredAccountFuture =
preferredAccountWorker.selectAccount(
waitingForAccountCall.getNumber(), phoneAccountHandles);
preferredAccountWorkerResultListener.listen(
this,
preferredAccountFuture,
result -> {
String callId = waitingForAccountCall.getId();
if (result.getSelectedPhoneAccountHandle().isPresent()) {
selectPhoneAccountListener.onPhoneAccountSelected(
result.getSelectedPhoneAccountHandle().get(), false, callId);
return;
}
if (!isVisible()) {
LogUtil.i(
"InCallActivity.showPhoneAccountSelectionDialog",
"activity ended before result returned");
return;
}
waitingForAccountCall.setPreferredAccountRecorder(
new PreferredAccountRecorder(
waitingForAccountCall.getNumber(),
result.getSuggestion().orNull(),
result.getDataId().orNull()));
selectPhoneAccountDialogFragment =
SelectPhoneAccountDialogFragment.newInstance(
result.getDialogOptionsBuilder().get().setCallId(callId).build(),
selectPhoneAccountListener);
selectPhoneAccountDialogFragment.show(getFragmentManager(), Tags.SELECT_ACCOUNT_FRAGMENT);
},
throwable -> {
throw new RuntimeException(throwable);
});
return true;
}
@Override
protected void onSaveInstanceState(Bundle out) {
LogUtil.enterBlock("InCallActivity.onSaveInstanceState");
// TODO: DialpadFragment should handle this as part of its own state
out.putBoolean(IntentExtraNames.SHOW_DIALPAD, isDialpadVisible());
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null) {
out.putString(KeysForSavedInstance.DIALPAD_TEXT, dialpadFragment.getDtmfText());
}
out.putBoolean(KeysForSavedInstance.DID_SHOW_ANSWER_SCREEN, didShowAnswerScreen);
out.putBoolean(KeysForSavedInstance.DID_SHOW_IN_CALL_SCREEN, didShowInCallScreen);
out.putBoolean(KeysForSavedInstance.DID_SHOW_VIDEO_CALL_SCREEN, didShowVideoCallScreen);
out.putBoolean(KeysForSavedInstance.DID_SHOW_RTT_CALL_SCREEN, didShowRttCallScreen);
out.putBoolean(KeysForSavedInstance.DID_SHOW_SPEAK_EASY_SCREEN, didShowSpeakEasyScreen);
super.onSaveInstanceState(out);
isVisible = false;
}
@Override
protected void onStart() {
Trace.beginSection("InCallActivity.onStart");
super.onStart();
isVisible = true;
showMainInCallFragment();
InCallPresenter.getInstance().setActivity(this);
enableInCallOrientationEventListener(
getRequestedOrientation()
== InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
InCallPresenter.getInstance().onActivityStarted();
if (!isRecreating) {
InCallPresenter.getInstance().onUiShowing(true);
}
if (isInMultiWindowMode() && !getResources().getBoolean(R.bool.incall_dialpad_allowed)) {
// Hide the dialpad because there may not be enough room
showDialpadFragment(false, false);
}
Trace.endSection();
}
@Override
protected void onResume() {
Trace.beginSection("InCallActivity.onResume");
super.onResume();
if (!InCallPresenter.getInstance().isReadyForTearDown()) {
updateTaskDescription();
}
// If there is a pending request to show or hide the dialpad, handle that now.
if (showDialpadRequest != DIALPAD_REQUEST_NONE) {
if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
// Exit fullscreen so that the user has access to the dialpad hide/show button.
// This is important when showing the dialpad from within dialer.
InCallPresenter.getInstance().setFullScreen(false /* isFullScreen */, true /* force */);
showDialpadFragment(true /* show */, animateDialpadOnShow /* animate */);
animateDialpadOnShow = false;
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null) {
dialpadFragment.setDtmfText(dtmfTextToPrepopulate);
dtmfTextToPrepopulate = null;
}
} else {
LogUtil.i("InCallActivity.onResume", "Force-hide the dialpad");
if (getDialpadFragment() != null) {
showDialpadFragment(false /* show */, false /* animate */);
}
}
showDialpadRequest = DIALPAD_REQUEST_NONE;
}
CallList.getInstance()
.onInCallUiShown(getIntent().getBooleanExtra(IntentExtraNames.FOR_FULL_SCREEN, false));
PseudoScreenState pseudoScreenState = InCallPresenter.getInstance().getPseudoScreenState();
pseudoScreenState.addListener(this);
onPseudoScreenStateChanged(pseudoScreenState.isOn());
Trace.endSection();
// add 1 sec delay to get memory snapshot so that dialer wont react slowly on resume.
ThreadUtil.postDelayedOnUiThread(
() ->
MetricsComponent.get(this)
.metrics()
.recordMemory(Metrics.INCALL_ACTIVITY_ON_RESUME_MEMORY_EVENT_NAME),
1000);
}
@Override
protected void onPause() {
Trace.beginSection("InCallActivity.onPause");
super.onPause();
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null) {
dialpadFragment.onDialerKeyUp(null);
}
InCallPresenter.getInstance().getPseudoScreenState().removeListener(this);
Trace.endSection();
}
@Override
protected void onStop() {
Trace.beginSection("InCallActivity.onStop");
isVisible = false;
super.onStop();
// Disconnects the call waiting for a phone account when the activity is hidden (e.g., after the
// user presses the home button).
// Without this the pending call will get stuck on phone account selection and new calls can't
// be created.
// Skip this when the screen is locked since the activity may complete its current life cycle
// and restart.
if (!isRecreating && !getSystemService(KeyguardManager.class).isKeyguardLocked()) {
DialerCall waitingForAccountCall = CallList.getInstance().getWaitingForAccountCall();
if (waitingForAccountCall != null) {
waitingForAccountCall.disconnect();
}
}
enableInCallOrientationEventListener(false);
InCallPresenter.getInstance().updateIsChangingConfigurations();
InCallPresenter.getInstance().onActivityStopped();
if (!isRecreating) {
InCallPresenter.getInstance().onUiShowing(false);
}
if (errorDialog != null) {
errorDialog.dismiss();
}
if (isFinishing()) {
InCallPresenter.getInstance().unsetActivity(this);
}
Trace.endSection();
}
@Override
protected void onDestroy() {
Trace.beginSection("InCallActivity.onDestroy");
super.onDestroy();
InCallPresenter.getInstance().unsetActivity(this);
InCallPresenter.getInstance().updateIsChangingConfigurations();
Trace.endSection();
}
@Override
public void finish() {
if (shouldCloseActivityOnFinish()) {
// When user select incall ui from recents after the call is disconnected, it tries to launch
// a new InCallActivity but InCallPresenter is already teared down at this point, which causes
// crash.
// By calling finishAndRemoveTask() instead of finish() the task associated with
// InCallActivity is cleared completely. So system won't try to create a new InCallActivity in
// this case.
//
// Calling finish won't clear the task and normally when an activity finishes it shouldn't
// clear the task since there could be parent activity in the same task that's still alive.
// But InCallActivity is special since it's singleInstance which means it's root activity and
// only instance of activity in the task. So it should be safe to also remove task when
// finishing.
// It's also necessary in the sense of it's excluded from recents. So whenever the activity
// finishes, the task should also be removed since it doesn't make sense to go back to it in
// anyway anymore.
super.finishAndRemoveTask();
}
}
private boolean shouldCloseActivityOnFinish() {
if (!isVisible) {
LogUtil.i(
"InCallActivity.shouldCloseActivityOnFinish",
"allowing activity to be closed because it's not visible");
return true;
}
if (InCallPresenter.getInstance().isInCallUiLocked()) {
LogUtil.i(
"InCallActivity.shouldCloseActivityOnFinish",
"in call ui is locked, not closing activity");
return false;
}
LogUtil.i(
"InCallActivity.shouldCloseActivityOnFinish",
"activity is visible and has no locks, allowing activity to close");
return true;
}
@Override
protected void onNewIntent(Intent intent) {
LogUtil.enterBlock("InCallActivity.onNewIntent");
// If the screen is off, we need to make sure it gets turned on for incoming calls.
// This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
// when the activity is first created. Therefore, to ensure the screen is turned on
// for the call waiting case, we recreate() the current activity. There should be no jank from
// this since the screen is already off and will remain so until our new activity is up.
if (!isVisible) {
onNewIntent(intent, true /* isRecreating */);
LogUtil.i("InCallActivity.onNewIntent", "Restarting InCallActivity to force screen on.");
recreate();
} else {
onNewIntent(intent, false /* isRecreating */);
}
}
@VisibleForTesting
void onNewIntent(Intent intent, boolean isRecreating) {
this.isRecreating = isRecreating;
// We're being re-launched with a new Intent. Since it's possible for a single InCallActivity
// instance to persist indefinitely (even if we finish() ourselves), this sequence can
// happen any time the InCallActivity needs to be displayed.
// Stash away the new intent so that we can get it in the future by calling getIntent().
// Otherwise getIntent() will return the original Intent from when we first got created.
setIntent(intent);
// Activities are always paused before receiving a new intent, so we can count on our onResume()
// method being called next.
// Just like in onCreate(), handle the intent.
// Skip if InCallActivity is going to be recreated since this will be called in onCreate().
if (!isRecreating) {
internalResolveIntent(intent);
}
}
@Override
public void onBackPressed() {
LogUtil.enterBlock("InCallActivity.onBackPressed");
if (!isVisible) {
return;
}
if (!getCallCardFragmentVisible()) {
return;
}
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null && dialpadFragment.isVisible()) {
showDialpadFragment(false /* show */, true /* animate */);
return;
}
if (CallList.getInstance().getIncomingCall() != null) {
LogUtil.i(
"InCallActivity.onBackPressed",
"Ignore the press of the back key when an incoming call is ringing");
return;
}
// Nothing special to do. Fall back to the default behavior.
super.onBackPressed();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
LogUtil.i("InCallActivity.onOptionsItemSelected", "item: " + item);
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null
&& dialpadFragment.isVisible()
&& dialpadFragment.onDialerKeyUp(event)) {
return true;
}
if (keyCode == KeyEvent.KEYCODE_CALL) {
// Always consume KEYCODE_CALL to ensure the PhoneWindow won't do anything with it.
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_CALL:
if (!InCallPresenter.getInstance().handleCallKey()) {
LogUtil.e(
"InCallActivity.onKeyDown",
"InCallPresenter should always handle KEYCODE_CALL in onKeyDown");
}
// Always consume KEYCODE_CALL to ensure the PhoneWindow won't do anything with it.
return true;
// Note that KEYCODE_ENDCALL isn't handled here as the standard system-wide handling of it
// is exactly what's needed, namely
// (1) "hang up" if there's an active call, or
// (2) "don't answer" if there's an incoming call.
// (See PhoneWindowManager for implementation details.)
case KeyEvent.KEYCODE_CAMERA:
// Consume KEYCODE_CAMERA since it's easy to accidentally press the camera button.
return true;
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE:
// Ringer silencing handled by PhoneWindowManager.
break;
case KeyEvent.KEYCODE_MUTE:
TelecomAdapter.getInstance()
.mute(!AudioModeProvider.getInstance().getAudioState().isMuted());
return true;
case KeyEvent.KEYCODE_SLASH:
// When verbose logging is enabled, dump the view for debugging/testing purposes.
if (LogUtil.isVerboseEnabled()) {
View decorView = getWindow().getDecorView();
LogUtil.v("InCallActivity.onKeyDown", "View dump:\n%s", decorView);
return true;
}
break;
case KeyEvent.KEYCODE_EQUALS:
break;
default: // fall out
}
// Pass other key events to DialpadFragment's "onDialerKeyDown" method in case the user types
// in DTMF (Dual-tone multi-frequency signaling) code.
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null
&& dialpadFragment.isVisible()
&& dialpadFragment.onDialerKeyDown(event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
public boolean isInCallScreenAnimating() {
return false;
}
public void showConferenceFragment(boolean show) {
if (show) {
startActivity(new Intent(this, ManageConferenceActivity.class));
}
}
public void showDialpadFragment(boolean show, boolean animate) {
if (show == isDialpadVisible()) {
return;
}
FragmentManager dialpadFragmentManager = getDialpadFragmentManager();
if (dialpadFragmentManager == null) {
LogUtil.i("InCallActivity.showDialpadFragment", "Unable to obtain a FragmentManager");
return;
}
if (!animate) {
if (show) {
showDialpadFragment();
} else {
hideDialpadFragment();
}
} else {
if (show) {
showDialpadFragment();
getDialpadFragment().animateShowDialpad();
}
getDialpadFragment()
.getView()
.startAnimation(show ? dialpadSlideInAnimation : dialpadSlideOutAnimation);
}
ProximitySensor sensor = InCallPresenter.getInstance().getProximitySensor();
if (sensor != null) {
sensor.onDialpadVisible(show);
}
showDialpadRequest = DIALPAD_REQUEST_NONE;
}
private void showDialpadFragment() {
FragmentManager dialpadFragmentManager = getDialpadFragmentManager();
if (dialpadFragmentManager == null) {
return;
}
FragmentTransaction transaction = dialpadFragmentManager.beginTransaction();
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment == null) {
dialpadFragment = new DialpadFragment();
transaction.add(getDialpadContainerId(), dialpadFragment, Tags.DIALPAD_FRAGMENT);
} else {
transaction.show(dialpadFragment);
dialpadFragment.setUserVisibleHint(true);
}
// RTT call screen doesn't show end call button inside dialpad, thus the space reserved for end
// call button should be removed.
dialpadFragment.setShouldShowEndCallSpace(didShowInCallScreen);
transaction.commitAllowingStateLoss();
dialpadFragmentManager.executePendingTransactions();
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL_DIALPAD, this);
getInCallOrRttCallScreen().onInCallScreenDialpadVisibilityChange(true);
}
private void hideDialpadFragment() {
FragmentManager dialpadFragmentManager = getDialpadFragmentManager();
if (dialpadFragmentManager == null) {
return;
}
DialpadFragment dialpadFragment = getDialpadFragment();
if (dialpadFragment != null) {
FragmentTransaction transaction = dialpadFragmentManager.beginTransaction();
transaction.hide(dialpadFragment);
transaction.commitAllowingStateLoss();
dialpadFragmentManager.executePendingTransactions();
dialpadFragment.setUserVisibleHint(false);
getInCallOrRttCallScreen().onInCallScreenDialpadVisibilityChange(false);
}
}
public boolean isDialpadVisible() {
DialpadFragment dialpadFragment = getDialpadFragment();
return dialpadFragment != null
&& dialpadFragment.isAdded()
&& !dialpadFragment.isHidden()
&& dialpadFragment.getView() != null
&& dialpadFragment.getUserVisibleHint();
}
/** Returns the {@link DialpadFragment} that's shown by this activity, or {@code null} */
@Nullable
private DialpadFragment getDialpadFragment() {
FragmentManager fragmentManager = getDialpadFragmentManager();
if (fragmentManager == null) {
return null;
}
return (DialpadFragment) fragmentManager.findFragmentByTag(Tags.DIALPAD_FRAGMENT);
}
public void onForegroundCallChanged(DialerCall newForegroundCall) {
updateTaskDescription();
if (newForegroundCall == null || !didShowAnswerScreen) {
LogUtil.v("InCallActivity.onForegroundCallChanged", "resetting background color");
updateWindowBackgroundColor(0 /* progress */);
}
}
private void updateTaskDescription() {
int color =
getResources().getBoolean(R.bool.is_layout_landscape)
? ResourcesCompat.getColor(
getResources(), R.color.statusbar_background_color, getTheme())
: InCallPresenter.getInstance().getThemeColorManager().getSecondaryColor();
setTaskDescription(
new TaskDescription(
getResources().getString(R.string.notification_ongoing_call), null /* icon */, color));
}
public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {
ThemeColorManager themeColorManager = InCallPresenter.getInstance().getThemeColorManager();
@ColorInt int top;
@ColorInt int middle;
@ColorInt int bottom;
@ColorInt int gray = 0x66000000;
if (isInMultiWindowMode()) {
top = themeColorManager.getBackgroundColorSolid();
middle = themeColorManager.getBackgroundColorSolid();
bottom = themeColorManager.getBackgroundColorSolid();
} else {
top = themeColorManager.getBackgroundColorTop();
middle = themeColorManager.getBackgroundColorMiddle();
bottom = themeColorManager.getBackgroundColorBottom();
}
if (progress < 0) {
float correctedProgress = Math.abs(progress);
top = ColorUtils.blendARGB(top, gray, correctedProgress);
middle = ColorUtils.blendARGB(middle, gray, correctedProgress);
bottom = ColorUtils.blendARGB(bottom, gray, correctedProgress);
}
boolean backgroundDirty = false;
if (backgroundDrawable == null) {
backgroundDrawableColors = new int[] {top, middle, bottom};
backgroundDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, backgroundDrawableColors);
backgroundDirty = true;
} else {
if (backgroundDrawableColors[0] != top) {
backgroundDrawableColors[0] = top;
backgroundDirty = true;
}
if (backgroundDrawableColors[1] != middle) {
backgroundDrawableColors[1] = middle;
backgroundDirty = true;
}
if (backgroundDrawableColors[2] != bottom) {
backgroundDrawableColors[2] = bottom;
backgroundDirty = true;
}
if (backgroundDirty) {
backgroundDrawable.setColors(backgroundDrawableColors);
}
}
if (backgroundDirty) {
getWindow().setBackgroundDrawable(backgroundDrawable);
}
}
public boolean isVisible() {
return isVisible;
}
public boolean getCallCardFragmentVisible() {
return didShowInCallScreen
|| didShowVideoCallScreen
|| didShowRttCallScreen
|| didShowSpeakEasyScreen;
}
public void dismissKeyguard(boolean dismiss) {
if (dismissKeyguard == dismiss) {
return;
}
dismissKeyguard = dismiss;
if (dismiss) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
}
}
public void showDialogForPostCharWait(String callId, String chars) {
PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars);
fragment.show(getSupportFragmentManager(), Tags.POST_CHAR_DIALOG_FRAGMENT);
}
public void showDialogOrToastForDisconnectedCall(DisconnectMessage disconnectMessage) {
LogUtil.i(
"InCallActivity.showDialogOrToastForDisconnectedCall",
"disconnect cause: %s",
disconnectMessage);
if (disconnectMessage.dialog == null || isFinishing()) {
return;
}
dismissPendingDialogs();
// Show a toast if the app is in background when a dialog can't be visible.
if (!isVisible()) {
Toast.makeText(getApplicationContext(), disconnectMessage.toastMessage, Toast.LENGTH_LONG)
.show();
return;
}
// Show the dialog.
errorDialog = disconnectMessage.dialog;
InCallUiLock lock = InCallPresenter.getInstance().acquireInCallUiLock("showErrorDialog");
disconnectMessage.dialog.setOnDismissListener(
dialogInterface -> {
lock.release();
onDialogDismissed();
});
disconnectMessage.dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
disconnectMessage.dialog.show();
}
private void onDialogDismissed() {
errorDialog = null;
CallList.getInstance().onErrorDialogDismissed();
}
public void dismissPendingDialogs() {
LogUtil.enterBlock("InCallActivity.dismissPendingDialogs");
if (!isVisible) {
// Defer the dismissing action as the activity is not visible and onSaveInstanceState may have
// been called.
LogUtil.i(
"InCallActivity.dismissPendingDialogs", "defer actions since activity is not visible");
needDismissPendingDialogs = true;
return;
}
// Dismiss the error dialog
if (errorDialog != null) {
errorDialog.dismiss();
errorDialog = null;
}
// Dismiss the phone account selection dialog
if (selectPhoneAccountDialogFragment != null) {
selectPhoneAccountDialogFragment.dismiss();
selectPhoneAccountDialogFragment = null;
}
// Dismiss the dialog for international call on WiFi
InternationalCallOnWifiDialogFragment internationalCallOnWifiFragment =
(InternationalCallOnWifiDialogFragment)
getSupportFragmentManager().findFragmentByTag(Tags.INTERNATIONAL_CALL_ON_WIFI);
if (internationalCallOnWifiFragment != null) {
internationalCallOnWifiFragment.dismiss();
}
// Dismiss the answer screen
AnswerScreen answerScreen = getAnswerScreen();
if (answerScreen != null) {
answerScreen.dismissPendingDialogs();
}
needDismissPendingDialogs = false;
}
private void enableInCallOrientationEventListener(boolean enable) {
if (enable) {
inCallOrientationEventListener.enable(true /* notifyDeviceOrientationChange */);
} else {
inCallOrientationEventListener.disable();
}
}
public void setExcludeFromRecents(boolean exclude) {
int taskId = getTaskId();
List<AppTask> tasks = getSystemService(ActivityManager.class).getAppTasks();
for (AppTask task : tasks) {
try {
if (task.getTaskInfo().id == taskId) {
task.setExcludeFromRecents(exclude);
}
} catch (RuntimeException e) {
LogUtil.e("InCallActivity.setExcludeFromRecents", "RuntimeException:\n%s", e);
}
}
}
@Nullable
public FragmentManager getDialpadFragmentManager() {
InCallScreen inCallScreen = getInCallOrRttCallScreen();
if (inCallScreen != null) {
return inCallScreen.getInCallScreenFragment().getChildFragmentManager();
}
return null;
}
public int getDialpadContainerId() {
return getInCallOrRttCallScreen().getAnswerAndDialpadContainerResourceId();
}
@Override
public AnswerScreenDelegate newAnswerScreenDelegate(AnswerScreen answerScreen) {
DialerCall call = CallList.getInstance().getCallById(answerScreen.getCallId());
if (call == null) {
// This is a work around for a bug where we attempt to create a new delegate after the call
// has already been removed. An example of when this can happen is:
// 1. incoming video call in landscape mode
// 2. remote party hangs up
// 3. activity switches from landscape to portrait
// At step #3 the answer fragment will try to create a new answer delegate but the call won't
// exist. In this case we'll simply return a stub delegate that does nothing. This is ok
// because this new state is transient and the activity will be destroyed soon.
LogUtil.i("InCallActivity.onPrimaryCallStateChanged", "call doesn't exist, using stub");
return new AnswerScreenPresenterStub();
} else {
return new AnswerScreenPresenter(
this, answerScreen, CallList.getInstance().getCallById(answerScreen.getCallId()));
}
}
@Override
public InCallScreenDelegate newInCallScreenDelegate() {
return new CallCardPresenter(this);
}
@Override
public InCallButtonUiDelegate newInCallButtonUiDelegate() {
return new CallButtonPresenter(this);
}
@Override
public VideoCallScreenDelegate newVideoCallScreenDelegate(VideoCallScreen videoCallScreen) {
DialerCall dialerCall = CallList.getInstance().getCallById(videoCallScreen.getCallId());
if (dialerCall != null && dialerCall.getVideoTech().shouldUseSurfaceView()) {
return dialerCall.getVideoTech().createVideoCallScreenDelegate(this, videoCallScreen);
}
return new VideoCallPresenter();
}
public void onPrimaryCallStateChanged() {
Trace.beginSection("InCallActivity.onPrimaryCallStateChanged");
showMainInCallFragment();
Trace.endSection();
}
public void showDialogOrToastForWifiHandoverFailure(DialerCall call) {
if (call.showWifiHandoverAlertAsToast()) {
Toast.makeText(this, R.string.video_call_lte_to_wifi_failed_message, Toast.LENGTH_SHORT)
.show();
return;
}
dismissPendingDialogs();
AlertDialog.Builder builder =
new AlertDialog.Builder(this).setTitle(R.string.video_call_lte_to_wifi_failed_title);
// This allows us to use the theme of the dialog instead of the activity
View dialogCheckBoxView =
View.inflate(builder.getContext(), R.layout.video_call_lte_to_wifi_failed, null /* root */);
CheckBox wifiHandoverFailureCheckbox =
(CheckBox) dialogCheckBoxView.findViewById(R.id.video_call_lte_to_wifi_failed_checkbox);
wifiHandoverFailureCheckbox.setChecked(false);
InCallUiLock lock = InCallPresenter.getInstance().acquireInCallUiLock("WifiFailedDialog");
errorDialog =
builder
.setView(dialogCheckBoxView)
.setMessage(R.string.video_call_lte_to_wifi_failed_message)
.setOnCancelListener(dialogInterface -> onDialogDismissed())
.setPositiveButton(
android.R.string.ok,
(dialogInterface, id) -> {
call.setDoNotShowDialogForHandoffToWifiFailure(
wifiHandoverFailureCheckbox.isChecked());
dialogInterface.cancel();
onDialogDismissed();
})
.setOnDismissListener(dialogInterface -> lock.release())
.create();
errorDialog.show();
}
public void showDialogForInternationalCallOnWifi(@NonNull DialerCall call) {
InternationalCallOnWifiDialogFragment fragment =
InternationalCallOnWifiDialogFragment.newInstance(call.getId());
fragment.show(getSupportFragmentManager(), Tags.INTERNATIONAL_CALL_ON_WIFI);
}
public void showDialogForRttRequest(DialerCall call, int rttRequestId) {
LogUtil.enterBlock("InCallActivity.showDialogForRttRequest");
rttRequestDialogFragment = RttRequestDialogFragment.newInstance(call.getId(), rttRequestId);
rttRequestDialogFragment.show(getSupportFragmentManager(), Tags.RTT_REQUEST_DIALOG);
}
public void setAllowOrientationChange(boolean allowOrientationChange) {
if (this.allowOrientationChange == allowOrientationChange) {
return;
}
this.allowOrientationChange = allowOrientationChange;
if (!allowOrientationChange) {
setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_DISALLOW_ROTATION);
} else {
setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
}
enableInCallOrientationEventListener(allowOrientationChange);
}
public void hideMainInCallFragment() {
LogUtil.enterBlock("InCallActivity.hideMainInCallFragment");
if (getCallCardFragmentVisible()) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
hideInCallScreenFragment(transaction);
hideVideoCallScreenFragment(transaction);
transaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
}
}
private void showMainInCallFragment() {
Trace.beginSection("InCallActivity.showMainInCallFragment");
// If the activity's onStart method hasn't been called yet then defer doing any work.
if (!isVisible) {
LogUtil.i("InCallActivity.showMainInCallFragment", "not visible yet/anymore");
Trace.endSection();
return;
}
// Don't let this be reentrant.
if (isInShowMainInCallFragment) {
LogUtil.i("InCallActivity.showMainInCallFragment", "already in method, bailing");
Trace.endSection();
return;
}
isInShowMainInCallFragment = true;
ShouldShowUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
ShouldShowUiResult shouldShowVideoUi = getShouldShowVideoUi();
ShouldShowUiResult shouldShowRttUi = getShouldShowRttUi();
ShouldShowUiResult shouldShowSpeakEasyUi = getShouldShowSpeakEasyUi();
LogUtil.i(
"InCallActivity.showMainInCallFragment",
"shouldShowAnswerUi: %b, shouldShowRttUi: %b, shouldShowVideoUi: %b, "
+ "shouldShowSpeakEasyUi: %b, didShowAnswerScreen: %b, didShowInCallScreen: %b, "
+ "didShowRttCallScreen: %b, didShowVideoCallScreen: %b, didShowSpeakEasyScreen: %b",
shouldShowAnswerUi.shouldShow,
shouldShowRttUi.shouldShow,
shouldShowVideoUi.shouldShow,
shouldShowSpeakEasyUi.shouldShow,
didShowAnswerScreen,
didShowInCallScreen,
didShowRttCallScreen,
didShowVideoCallScreen,
didShowSpeakEasyScreen);
// Only video call ui allows orientation change.
setAllowOrientationChange(shouldShowVideoUi.shouldShow);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
boolean didChange;
if (shouldShowAnswerUi.shouldShow) {
didChange = hideInCallScreenFragment(transaction);
didChange |= hideVideoCallScreenFragment(transaction);
didChange |= hideRttCallScreenFragment(transaction);
didChange |= hideSpeakEasyFragment(transaction);
didChange |= showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
} else if (shouldShowVideoUi.shouldShow) {
didChange = hideInCallScreenFragment(transaction);
didChange |= showVideoCallScreenFragment(transaction, shouldShowVideoUi.call);
didChange |= hideRttCallScreenFragment(transaction);
didChange |= hideSpeakEasyFragment(transaction);
didChange |= hideAnswerScreenFragment(transaction);
} else if (shouldShowRttUi.shouldShow) {
didChange = hideInCallScreenFragment(transaction);
didChange |= hideVideoCallScreenFragment(transaction);
didChange |= hideAnswerScreenFragment(transaction);
didChange |= hideSpeakEasyFragment(transaction);
didChange |= showRttCallScreenFragment(transaction, shouldShowRttUi.call);
} else if (shouldShowSpeakEasyUi.shouldShow) {
didChange = hideInCallScreenFragment(transaction);
didChange |= hideVideoCallScreenFragment(transaction);
didChange |= hideAnswerScreenFragment(transaction);
didChange |= hideRttCallScreenFragment(transaction);
didChange |= showSpeakEasyFragment(transaction, shouldShowSpeakEasyUi.call);
} else {
didChange = showInCallScreenFragment(transaction);
didChange |= hideVideoCallScreenFragment(transaction);
didChange |= hideRttCallScreenFragment(transaction);
didChange |= hideSpeakEasyFragment(transaction);
didChange |= hideAnswerScreenFragment(transaction);
}
if (didChange) {
Trace.beginSection("InCallActivity.commitTransaction");
transaction.commitNow();
Trace.endSection();
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
}
isInShowMainInCallFragment = false;
Trace.endSection();
}
private boolean showSpeakEasyFragment(FragmentTransaction transaction, DialerCall call) {
if (didShowSpeakEasyScreen) {
if (lastShownSpeakEasyScreenUniqueCallid.equals(call.getUniqueCallId())) {
LogUtil.i("InCallActivity.showSpeakEasyFragment", "found existing fragment");
return false;
}
hideSpeakEasyFragment(transaction);
LogUtil.i("InCallActivity.showSpeakEasyFragment", "hid existing fragment");
}
Optional<Fragment> speakEasyFragment = speakEasyCallManager.getSpeakEasyFragment(call);
if (speakEasyFragment.isPresent()) {
transaction.add(R.id.main, speakEasyFragment.get(), Tags.SPEAK_EASY_SCREEN);
didShowSpeakEasyScreen = true;
lastShownSpeakEasyScreenUniqueCallid = call.getUniqueCallId();
LogUtil.i(
"InCallActivity.showSpeakEasyFragment",
"set fragment for call %s",
lastShownSpeakEasyScreenUniqueCallid);
return true;
}
return false;
}
private Fragment getSpeakEasyScreen() {
return getSupportFragmentManager().findFragmentByTag(Tags.SPEAK_EASY_SCREEN);
}
private boolean hideSpeakEasyFragment(FragmentTransaction transaction) {
if (!didShowSpeakEasyScreen) {
return false;
}
Fragment speakEasyFragment = getSpeakEasyScreen();
if (speakEasyFragment != null) {
transaction.remove(speakEasyFragment);
didShowSpeakEasyScreen = false;
return true;
}
return false;
}
@VisibleForTesting
public void setSpeakEasyCallManager(SpeakEasyCallManager speakEasyCallManager) {
this.speakEasyCallManager = speakEasyCallManager;
}
@Nullable
public SpeakEasyCallManager getSpeakEasyCallManager() {
if (this.speakEasyCallManager == null) {
this.speakEasyCallManager = InCallPresenter.getInstance().getSpeakEasyCallManager();
}
return speakEasyCallManager;
}
private ShouldShowUiResult getShouldShowSpeakEasyUi() {
SpeakEasyCallManager speakEasyCallManager = getSpeakEasyCallManager();
if (speakEasyCallManager == null) {
return new ShouldShowUiResult(false, null);
}
DialerCall call =
CallList.getInstance().getIncomingCall() != null
? CallList.getInstance().getIncomingCall()
: CallList.getInstance().getActiveCall();
if (call == null) {
// This is a special case where the first call is not automatically resumed
// after the second active call is remotely disconnected.
DialerCall backgroundCall = CallList.getInstance().getBackgroundCall();
if (backgroundCall != null && backgroundCall.isSpeakEasyCall()) {
LogUtil.i("InCallActivity.getShouldShowSpeakEasyUi", "taking call off hold");
backgroundCall.unhold();
return new ShouldShowUiResult(true, backgroundCall);
}
return new ShouldShowUiResult(false, call);
}
if (!call.isSpeakEasyCall() || !call.isSpeakEasyEligible()) {
return new ShouldShowUiResult(false, call);
}
Optional<Fragment> speakEasyFragment = speakEasyCallManager.getSpeakEasyFragment(call);
if (!speakEasyFragment.isPresent()) {
return new ShouldShowUiResult(false, call);
}
return new ShouldShowUiResult(true, call);
}
private ShouldShowUiResult getShouldShowAnswerUi() {
DialerCall call = CallList.getInstance().getIncomingCall();
if (call != null && !call.isSpeakEasyCall()) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
return new ShouldShowUiResult(true, call);
}
call = CallList.getInstance().getVideoUpgradeRequestCall();
if (call != null) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
return new ShouldShowUiResult(true, call);
}
// Check if we're showing the answer screen and the call is disconnected. If this condition is
// true then we won't switch from the answer UI to the in call UI. This prevents flicker when
// the user rejects an incoming call.
call = CallList.getInstance().getFirstCall();
if (call == null) {
call = CallList.getInstance().getBackgroundCall();
}
if (didShowAnswerScreen && (call == null || call.getState() == DialerCallState.DISCONNECTED)) {
LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
return new ShouldShowUiResult(true, call);
}
return new ShouldShowUiResult(false, null);
}
private static ShouldShowUiResult getShouldShowVideoUi() {
DialerCall call = CallList.getInstance().getFirstCall();
if (call == null) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
return new ShouldShowUiResult(false, null);
}
if (call.isVideoCall()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
return new ShouldShowUiResult(true, call);
}
if (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest()) {
LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
return new ShouldShowUiResult(true, call);
}
return new ShouldShowUiResult(false, null);
}
private static ShouldShowUiResult getShouldShowRttUi() {
DialerCall call = CallList.getInstance().getFirstCall();
if (call == null) {
LogUtil.i("InCallActivity.getShouldShowRttUi", "null call");
return new ShouldShowUiResult(false, null);
}
if (call.isActiveRttCall()) {
LogUtil.i("InCallActivity.getShouldShowRttUi", "found rtt call");
return new ShouldShowUiResult(true, call);
}
if (call.hasSentRttUpgradeRequest()) {
LogUtil.i("InCallActivity.getShouldShowRttUi", "upgrading to rtt");
return new ShouldShowUiResult(true, call);
}
return new ShouldShowUiResult(false, null);
}
private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
// When rejecting a call the active call can become null in which case we should continue
// showing the answer screen.
if (didShowAnswerScreen && call == null) {
return false;
}
Assert.checkArgument(call != null, "didShowAnswerScreen was false but call was still null");
boolean isVideoUpgradeRequest = call.hasReceivedVideoUpgradeRequest();
// Check if we're already showing an answer screen for this call.
if (didShowAnswerScreen) {
AnswerScreen answerScreen = getAnswerScreen();
if (answerScreen.getCallId().equals(call.getId())
&& answerScreen.isVideoCall() == call.isVideoCall()
&& answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest
&& !answerScreen.isActionTimeout()) {
LogUtil.d(
"InCallActivity.showAnswerScreenFragment",
"answer fragment exists for same call and has NOT been accepted/rejected/timed out");
return false;
}
if (answerScreen.isActionTimeout()) {
LogUtil.i(
"InCallActivity.showAnswerScreenFragment",
"answer fragment exists but has been accepted/rejected and timed out");
} else {
LogUtil.i(
"InCallActivity.showAnswerScreenFragment",
"answer fragment exists but arguments do not match");
}
hideAnswerScreenFragment(transaction);
}
// Show a new answer screen.
AnswerScreen answerScreen =
AnswerBindings.createAnswerScreen(
call.getId(),
call.isActiveRttCall(),
call.isVideoCall(),
isVideoUpgradeRequest,
call.getVideoTech().isSelfManagedCamera(),
shouldAllowAnswerAndRelease(call),
CallList.getInstance().getBackgroundCall() != null,
getSpeakEasyCallManager().isAvailable(getApplicationContext())
&& call.isSpeakEasyEligible());
transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), Tags.ANSWER_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
didShowAnswerScreen = true;
return true;
}
private boolean shouldAllowAnswerAndRelease(DialerCall call) {
if (CallList.getInstance().getActiveCall() == null) {
LogUtil.i("InCallActivity.shouldAllowAnswerAndRelease", "no active call");
return false;
}
if (getSystemService(TelephonyManager.class).getPhoneType()
== TelephonyManager.PHONE_TYPE_CDMA) {
LogUtil.i("InCallActivity.shouldAllowAnswerAndRelease", "PHONE_TYPE_CDMA not supported");
return false;
}
if (call.isVideoCall() || call.hasReceivedVideoUpgradeRequest()) {
LogUtil.i("InCallActivity.shouldAllowAnswerAndRelease", "video call");
return false;
}
if (!ConfigProviderComponent.get(this)
.getConfigProvider()
.getBoolean(ConfigNames.ANSWER_AND_RELEASE_ENABLED, true)) {
LogUtil.i("InCallActivity.shouldAllowAnswerAndRelease", "disabled by config");
return false;
}
return true;
}
private boolean hideAnswerScreenFragment(FragmentTransaction transaction) {
if (!didShowAnswerScreen) {
return false;
}
AnswerScreen answerScreen = getAnswerScreen();
if (answerScreen != null) {
transaction.remove(answerScreen.getAnswerScreenFragment());
}
didShowAnswerScreen = false;
return true;
}
private boolean showInCallScreenFragment(FragmentTransaction transaction) {
if (didShowInCallScreen) {
return false;
}
InCallScreen inCallScreen = InCallBindings.createInCallScreen();
transaction.add(R.id.main, inCallScreen.getInCallScreenFragment(), Tags.IN_CALL_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
didShowInCallScreen = true;
return true;
}
private boolean hideInCallScreenFragment(FragmentTransaction transaction) {
if (!didShowInCallScreen) {
return false;
}
InCallScreen inCallScreen = getInCallScreen();
if (inCallScreen != null) {
transaction.remove(inCallScreen.getInCallScreenFragment());
}
didShowInCallScreen = false;
return true;
}
private boolean showRttCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
if (didShowRttCallScreen) {
if (getRttCallScreen().getCallId().equals(call.getId())) {
return false;
}
LogUtil.i("InCallActivity.showRttCallScreenFragment", "RTT call id doesn't match");
hideRttCallScreenFragment(transaction);
}
RttCallScreen rttCallScreen = RttBindings.createRttCallScreen(call.getId());
transaction.add(R.id.main, rttCallScreen.getRttCallScreenFragment(), Tags.RTT_CALL_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
didShowRttCallScreen = true;
// In some cases such as VZW, RTT request will be automatically accepted by modem. So the dialog
// won't make any sense and should be dismissed if it's already switched to RTT.
if (rttRequestDialogFragment != null) {
LogUtil.i("InCallActivity.showRttCallScreenFragment", "dismiss RTT request dialog");
rttRequestDialogFragment.dismiss();
rttRequestDialogFragment = null;
}
return true;
}
private boolean hideRttCallScreenFragment(FragmentTransaction transaction) {
if (!didShowRttCallScreen) {
return false;
}
RttCallScreen rttCallScreen = getRttCallScreen();
if (rttCallScreen != null) {
transaction.remove(rttCallScreen.getRttCallScreenFragment());
}
didShowRttCallScreen = false;
return true;
}
private boolean showVideoCallScreenFragment(FragmentTransaction transaction, DialerCall call) {
if (didShowVideoCallScreen) {
VideoCallScreen videoCallScreen = getVideoCallScreen();
if (videoCallScreen.getCallId().equals(call.getId())) {
return false;
}
LogUtil.i(
"InCallActivity.showVideoCallScreenFragment",
"video call fragment exists but arguments do not match");
hideVideoCallScreenFragment(transaction);
}
LogUtil.i("InCallActivity.showVideoCallScreenFragment", "call: %s", call);
VideoCallScreen videoCallScreen =
VideoBindings.createVideoCallScreen(
call.getId(), call.getVideoTech().shouldUseSurfaceView());
transaction.add(
R.id.main, videoCallScreen.getVideoCallScreenFragment(), Tags.VIDEO_CALL_SCREEN);
Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
didShowVideoCallScreen = true;
return true;
}
private boolean hideVideoCallScreenFragment(FragmentTransaction transaction) {
if (!didShowVideoCallScreen) {
return false;
}
VideoCallScreen videoCallScreen = getVideoCallScreen();
if (videoCallScreen != null) {
transaction.remove(videoCallScreen.getVideoCallScreenFragment());
}
didShowVideoCallScreen = false;
return true;
}
private AnswerScreen getAnswerScreen() {
return (AnswerScreen) getSupportFragmentManager().findFragmentByTag(Tags.ANSWER_SCREEN);
}
private InCallScreen getInCallScreen() {
return (InCallScreen) getSupportFragmentManager().findFragmentByTag(Tags.IN_CALL_SCREEN);
}
private VideoCallScreen getVideoCallScreen() {
return (VideoCallScreen) getSupportFragmentManager().findFragmentByTag(Tags.VIDEO_CALL_SCREEN);
}
private RttCallScreen getRttCallScreen() {
return (RttCallScreen) getSupportFragmentManager().findFragmentByTag(Tags.RTT_CALL_SCREEN);
}
private InCallScreen getInCallOrRttCallScreen() {
InCallScreen inCallScreen = null;
if (didShowInCallScreen) {
inCallScreen = getInCallScreen();
}
if (didShowRttCallScreen) {
inCallScreen = getRttCallScreen();
}
return inCallScreen;
}
@Override
public void onPseudoScreenStateChanged(boolean isOn) {
LogUtil.i("InCallActivity.onPseudoScreenStateChanged", "isOn: " + isOn);
pseudoBlackScreenOverlay.setVisibility(isOn ? View.GONE : View.VISIBLE);
}
/**
* For some touch related issue, turning off the screen can be faked by drawing a black view over
* the activity. All touch events started when the screen is "off" is rejected.
*
* @see PseudoScreenState
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// Reject any gesture that started when the screen is in the fake off state.
if (touchDownWhenPseudoScreenOff) {
if (event.getAction() == MotionEvent.ACTION_UP) {
touchDownWhenPseudoScreenOff = false;
}
return true;
}
// Reject all touch event when the screen is in the fake off state.
if (!InCallPresenter.getInstance().getPseudoScreenState().isOn()) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
touchDownWhenPseudoScreenOff = true;
LogUtil.i("InCallActivity.dispatchTouchEvent", "touchDownWhenPseudoScreenOff");
}
return true;
}
return super.dispatchTouchEvent(event);
}
@Override
public RttCallScreenDelegate newRttCallScreenDelegate(RttCallScreen videoCallScreen) {
return new RttCallPresenter();
}
private static class ShouldShowUiResult {
public final boolean shouldShow;
public final DialerCall call;
ShouldShowUiResult(boolean shouldShow, DialerCall call) {
this.shouldShow = shouldShow;
this.call = call;
}
}
private static final class IntentExtraNames {
static final String FOR_FULL_SCREEN = "InCallActivity.for_full_screen_intent";
static final String NEW_OUTGOING_CALL = "InCallActivity.new_outgoing_call";
static final String SHOW_DIALPAD = "InCallActivity.show_dialpad";
}
private static final class KeysForSavedInstance {
static final String DIALPAD_TEXT = "InCallActivity.dialpad_text";
static final String DID_SHOW_ANSWER_SCREEN = "did_show_answer_screen";
static final String DID_SHOW_IN_CALL_SCREEN = "did_show_in_call_screen";
static final String DID_SHOW_VIDEO_CALL_SCREEN = "did_show_video_call_screen";
static final String DID_SHOW_RTT_CALL_SCREEN = "did_show_rtt_call_screen";
static final String DID_SHOW_SPEAK_EASY_SCREEN = "did_show_speak_easy_screen";
}
/** Request codes for pending intents. */
public static final class PendingIntentRequestCodes {
static final int NON_FULL_SCREEN = 0;
static final int FULL_SCREEN = 1;
static final int BUBBLE = 2;
}
private static final class Tags {
static final String ANSWER_SCREEN = "tag_answer_screen";
static final String DIALPAD_FRAGMENT = "tag_dialpad_fragment";
static final String IN_CALL_SCREEN = "tag_in_call_screen";
static final String INTERNATIONAL_CALL_ON_WIFI = "tag_international_call_on_wifi";
static final String SELECT_ACCOUNT_FRAGMENT = "tag_select_account_fragment";
static final String VIDEO_CALL_SCREEN = "tag_video_call_screen";
static final String RTT_CALL_SCREEN = "tag_rtt_call_screen";
static final String POST_CHAR_DIALOG_FRAGMENT = "tag_post_char_dialog_fragment";
static final String SPEAK_EASY_SCREEN = "tag_speak_easy_screen";
static final String RTT_REQUEST_DIALOG = "tag_rtt_request_dialog";
}
private static final class ConfigNames {
static final String ANSWER_AND_RELEASE_ENABLED = "answer_and_release_enabled";
}
private static final class SelectPhoneAccountListener
extends SelectPhoneAccountDialogFragment.SelectPhoneAccountListener {
private static final String TAG = SelectPhoneAccountListener.class.getCanonicalName();
private final Context appContext;
SelectPhoneAccountListener(Context appContext) {
this.appContext = appContext;
}
@Override
public void onPhoneAccountSelected(
PhoneAccountHandle selectedAccountHandle, boolean setDefault, String callId) {
DialerCall call = CallList.getInstance().getCallById(callId);
LogUtil.i(TAG, "Phone account select with call:\n%s", call);
if (call != null) {
call.phoneAccountSelected(selectedAccountHandle, false);
if (call.getPreferredAccountRecorder() != null) {
call.getPreferredAccountRecorder().record(appContext, selectedAccountHandle, setDefault);
}
}
}
@Override
public void onDialogDismissed(String callId) {
DialerCall call = CallList.getInstance().getCallById(callId);
LogUtil.i(TAG, "Disconnecting call:\n%s" + call);
if (call != null) {
call.disconnect();
}
}
}
}