blob: 87d4ebb0728b3072e36123be6a28c542b45b0742 [file] [log] [blame]
/*
* 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.car.dialer;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.car.apps.common.CircleBitmapDrawable;
import com.android.car.apps.common.FabDrawable;
import com.android.car.dialer.telecom.TelecomUtils;
import com.android.car.dialer.telecom.UiCall;
import com.android.car.dialer.telecom.UiCallManager;
import com.android.car.dialer.telecom.UiCallManager.CallListener;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* A fragment that displays information about an on-going call with options to hang up.
*/
public class OngoingCallFragment extends Fragment {
private static final String TAG = "OngoingCall";
private static final SparseArray<Character> mDialpadButtonMap = new SparseArray<>();
static {
mDialpadButtonMap.put(R.id.one, '1');
mDialpadButtonMap.put(R.id.two, '2');
mDialpadButtonMap.put(R.id.three, '3');
mDialpadButtonMap.put(R.id.four, '4');
mDialpadButtonMap.put(R.id.five, '5');
mDialpadButtonMap.put(R.id.six, '6');
mDialpadButtonMap.put(R.id.seven, '7');
mDialpadButtonMap.put(R.id.eight, '8');
mDialpadButtonMap.put(R.id.nine, '9');
mDialpadButtonMap.put(R.id.zero, '0');
mDialpadButtonMap.put(R.id.star, '*');
mDialpadButtonMap.put(R.id.pound, '#');
}
private final Handler mHandler = new Handler();
private UiCall mLastRemovedCall;
private UiCallManager mUiCallManager;
private View mRingingCallControls;
private View mActiveCallControls;
private ImageButton mEndCallButton;
private ImageButton mUnholdCallButton;
private ImageButton mMuteButton;
private ImageButton mToggleDialpadButton;
private ImageButton mSwapButton;
private ImageButton mMergeButton;
private ImageButton mAnswerCallButton;
private ImageButton mRejectCallButton;
private TextView mNameTextView;
private TextView mSecondaryNameTextView;
private TextView mStateTextView;
private TextView mSecondaryStateTextView;
private ImageView mLargeContactPhotoView;
private ImageView mSmallContactPhotoView;
private View mDialpadContainer;
private View mSecondaryCallContainer;
private View mSecondaryCallControls;
private String mLoadedNumber;
private CharSequence mCallInfoLabel;
private UiBluetoothMonitor mUiBluetoothMonitor;
static OngoingCallFragment newInstance(UiCallManager callManager,
UiBluetoothMonitor btMonitor) {
OngoingCallFragment fragment = new OngoingCallFragment();
fragment.mUiCallManager = callManager;
fragment.mUiBluetoothMonitor = btMonitor;
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mUpdateDurationRunnable);
mHandler.removeCallbacks(mStopDtmfToneRunnable);
mLoadedNumber = null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.ongoing_call, container, false);
initializeViews(view);
initializeClickListeners();
List<View> dialpadViews = Arrays.asList(
mDialpadContainer.findViewById(R.id.one),
mDialpadContainer.findViewById(R.id.two),
mDialpadContainer.findViewById(R.id.three),
mDialpadContainer.findViewById(R.id.four),
mDialpadContainer.findViewById(R.id.five),
mDialpadContainer.findViewById(R.id.six),
mDialpadContainer.findViewById(R.id.seven),
mDialpadContainer.findViewById(R.id.eight),
mDialpadContainer.findViewById(R.id.nine),
mDialpadContainer.findViewById(R.id.zero),
mDialpadContainer.findViewById(R.id.pound),
mDialpadContainer.findViewById(R.id.star));
// In touch screen, we need to adjust the InCall card for the narrow screen to show the
// full dial pad.
for (View dialpadView : dialpadViews) {
dialpadView.setOnTouchListener(mDialpadTouchListener);
dialpadView.setOnKeyListener(mDialpadKeyListener);
}
mUiCallManager.addListener(mCallListener);
updateCalls();
return view;
}
private void initializeViews(View parent) {
mRingingCallControls = parent.findViewById(R.id.ringing_call_controls);
mActiveCallControls = parent.findViewById(R.id.active_call_controls);
mEndCallButton = parent.findViewById(R.id.end_call);
mUnholdCallButton = parent.findViewById(R.id.unhold_call);
mMuteButton = parent.findViewById(R.id.mute);
mToggleDialpadButton = parent.findViewById(R.id.toggle_dialpad);
mDialpadContainer = parent.findViewById(R.id.dialpad_container);
mNameTextView = parent.findViewById(R.id.name);
mSecondaryNameTextView = parent.findViewById(R.id.name_secondary);
mStateTextView = parent.findViewById(R.id.info);
mSecondaryStateTextView = parent.findViewById(R.id.info_secondary);
mLargeContactPhotoView = parent.findViewById(R.id.large_contact_photo);
mSmallContactPhotoView = parent.findViewById(R.id.small_contact_photo);
mSecondaryCallContainer = parent.findViewById(R.id.secondary_call_container);
mSecondaryCallControls = parent.findViewById(R.id.secondary_call_controls);
mSwapButton = parent.findViewById(R.id.swap);
mMergeButton = parent.findViewById(R.id.merge);
mAnswerCallButton = parent.findViewById(R.id.answer_call_button);
mRejectCallButton = parent.findViewById(R.id.reject_call_button);
Context context = getContext();
FabDrawable drawable = new FabDrawable(context);
drawable.setFabAndStrokeColor(context.getColor(R.color.phone_call));
mAnswerCallButton.setBackground(drawable);
drawable = new FabDrawable(context);
drawable.setFabAndStrokeColor(context.getColor(R.color.phone_end_call));
mEndCallButton.setBackground(drawable);
drawable = new FabDrawable(context);
drawable.setFabAndStrokeColor(context.getColor(R.color.phone_call));
mUnholdCallButton.setBackground(drawable);
}
private void initializeClickListeners() {
mAnswerCallButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
if (call == null) {
Log.w(TAG, "There is no incoming call to answer.");
return;
}
mUiCallManager.answerCall(call);
});
mRejectCallButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
if (call == null) {
Log.w(TAG, "There is no incoming call to reject.");
return;
}
mUiCallManager.rejectCall(call, false, null);
});
mEndCallButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getPrimaryCall();
if (call == null) {
Log.w(TAG, "There is no active call to end.");
return;
}
mUiCallManager.disconnectCall(call);
});
mUnholdCallButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getPrimaryCall();
if (call == null) {
Log.w(TAG, "There is no active call to unhold.");
return;
}
mUiCallManager.unholdCall(call);
});
mMuteButton.setOnClickListener(
(unusedView) -> mUiCallManager.setMuted(!mUiCallManager.getMuted()));
mSwapButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getPrimaryCall();
if (call == null) {
Log.w(TAG, "There is no active call to hold.");
return;
}
if (call.getState() == Call.STATE_HOLDING) {
mUiCallManager.unholdCall(call);
} else {
mUiCallManager.holdCall(call);
}
});
mMergeButton.setOnClickListener((unusedView) -> {
UiCall call = mUiCallManager.getPrimaryCall();
UiCall secondaryCall = mUiCallManager.getSecondaryCall();
if (call == null || secondaryCall == null) {
Log.w(TAG, "There aren't two call to merge.");
return;
}
mUiCallManager.conference(call, secondaryCall);
});
mToggleDialpadButton.setOnClickListener((unusedView) -> {
if (mToggleDialpadButton.isActivated()) {
closeDialpad();
} else {
openDialpad(true /*animate*/);
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mUiCallManager.removeListener(mCallListener);
}
@Override
public void onStart() {
super.onStart();
trySpeakerAudioRouteIfNecessary();
}
private void updateCalls() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updateCalls(); Primary call: " + mUiCallManager.getPrimaryCall()
+ "; Secondary call:" + mUiCallManager.getSecondaryCall());
}
mHandler.removeCallbacks(mUpdateDurationRunnable);
UiCall primaryCall = mUiCallManager.getPrimaryCall();
CharSequence disconnectCauseLabel = mLastRemovedCall == null
? null : mLastRemovedCall.getDisconnectCause();
if (primaryCall == null && !TextUtils.isEmpty(disconnectCauseLabel)) {
closeDialpad();
setStateText(disconnectCauseLabel);
return;
}
if (primaryCall == null || primaryCall.getState() == Call.STATE_DISCONNECTED) {
closeDialpad();
setStateText(getString(R.string.call_state_call_ended));
mRingingCallControls.setVisibility(View.GONE);
mActiveCallControls.setVisibility(View.GONE);
return;
}
if (primaryCall.getState() == Call.STATE_RINGING) {
mRingingCallControls.setVisibility(View.VISIBLE);
mActiveCallControls.setVisibility(View.GONE);
} else {
mRingingCallControls.setVisibility(View.GONE);
mActiveCallControls.setVisibility(View.VISIBLE);
}
loadContactPhotoForPrimaryNumber(primaryCall.getNumber());
String displayName = TelecomUtils.getDisplayName(getContext(), primaryCall);
mNameTextView.setText(displayName);
mNameTextView.setVisibility(TextUtils.isEmpty(displayName) ? View.GONE : View.VISIBLE);
Context context = getContext();
switch (primaryCall.getState()) {
case Call.STATE_NEW:
// Since the content resolver call is only cached when a contact is found,
// this should only be called once on a new call to avoid jank.
// TODO: consider moving TelecomUtils.getTypeFromNumber into a CursorLoader
mCallInfoLabel = TelecomUtils.getTypeFromNumber(context, primaryCall.getNumber());
case Call.STATE_CONNECTING:
case Call.STATE_DIALING:
case Call.STATE_SELECT_PHONE_ACCOUNT:
case Call.STATE_HOLDING:
case Call.STATE_DISCONNECTED:
mHandler.removeCallbacks(mUpdateDurationRunnable);
String callInfoText = TelecomUtils.getCallInfoText(context,
primaryCall, mCallInfoLabel);
setStateText(callInfoText);
break;
case Call.STATE_ACTIVE:
if (mUiBluetoothMonitor.isHfpConnected()) {
mHandler.post(mUpdateDurationRunnable);
}
break;
case Call.STATE_RINGING:
Log.w(TAG, "There should not be a ringing call in the ongoing call fragment.");
break;
default:
Log.w(TAG, "Unhandled call state: " + primaryCall.getState());
}
// If it is a voicemail call, open the dialpad (with no animation).
if (Objects.equals(primaryCall.getNumber(), TelecomUtils.getVoicemailNumber(context))) {
openDialpad(false /*animate*/);
mToggleDialpadButton.setVisibility(View.GONE);
} else {
mToggleDialpadButton.setVisibility(View.VISIBLE);
}
// Handle the holding case.
if (primaryCall.getState() == Call.STATE_HOLDING) {
mEndCallButton.setVisibility(View.GONE);
mUnholdCallButton.setVisibility(View.VISIBLE);
mMuteButton.setVisibility(View.INVISIBLE);
mToggleDialpadButton.setVisibility(View.INVISIBLE);
} else {
mEndCallButton.setVisibility(View.VISIBLE);
mUnholdCallButton.setVisibility(View.GONE);
mMuteButton.setVisibility(View.VISIBLE);
mToggleDialpadButton.setVisibility(View.VISIBLE);
}
updateSecondaryCall(primaryCall, mUiCallManager.getSecondaryCall());
}
private void updateSecondaryCall(UiCall primaryCall, UiCall secondaryCall) {
if (primaryCall == null || secondaryCall == null) {
mSecondaryCallContainer.setVisibility(View.GONE);
mSecondaryCallControls.setVisibility(View.GONE);
return;
}
mSecondaryCallContainer.setVisibility(View.VISIBLE);
if (primaryCall.getState() == Call.STATE_ACTIVE
&& secondaryCall.getState() == Call.STATE_HOLDING) {
mSecondaryCallControls.setVisibility(View.VISIBLE);
} else {
mSecondaryCallControls.setVisibility(View.GONE);
}
Context context = getContext();
mSecondaryNameTextView.setText(TelecomUtils.getDisplayName(context, secondaryCall));
mSecondaryStateTextView.setText(
TelecomUtils.callStateToUiString(context, secondaryCall.getState()));
loadContactPhotoForSecondaryNumber(secondaryCall.getNumber());
}
/**
* Loads the contact photo associated with the given number and sets it in the views that
* correspond with a primary number.
*/
private void loadContactPhotoForPrimaryNumber(String primaryNumber) {
// Don't reload the image if the number is the same.
if (Objects.equals(primaryNumber, mLoadedNumber)) {
return;
}
final ContentResolver cr = getContext().getContentResolver();
BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() {
@Override
public void run() {
if (mBitmap != null) {
Resources r = getResources();
mSmallContactPhotoView.setImageDrawable(new CircleBitmapDrawable(r, mBitmap));
mLargeContactPhotoView.setImageBitmap(mBitmap);
mLargeContactPhotoView.clearColorFilter();
} else {
mSmallContactPhotoView.setImageResource(R.drawable.logo_avatar);
mLargeContactPhotoView.setImageResource(R.drawable.ic_avatar_bg);
}
}
};
mLoadedNumber = primaryNumber;
BitmapWorkerTask.loadBitmap(cr, mLargeContactPhotoView, primaryNumber, runnable);
}
/**
* Loads the contact photo associated with the given number and sets it in the views that
* correspond to a secondary number.
*/
private void loadContactPhotoForSecondaryNumber(String secondaryNumber) {
BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() {
@Override
public void run() {
if (mBitmap != null) {
mLargeContactPhotoView.setImageBitmap(mBitmap);
} else {
mLargeContactPhotoView.setImageResource(R.drawable.logo_avatar);
}
}
};
Context context = getContext();
BitmapWorkerTask.loadBitmap(context.getContentResolver(), mLargeContactPhotoView,
secondaryNumber, runnable);
int scrimColor = context.getColor(R.color.phone_secondary_call_scrim);
mLargeContactPhotoView.setColorFilter(scrimColor);
}
private void setStateText(CharSequence stateText) {
mStateTextView.setText(stateText);
mStateTextView.setVisibility(TextUtils.isEmpty(stateText) ? View.GONE : View.VISIBLE);
}
/**
* If the phone is using bluetooth, then do nothing. If the phone is not using bluetooth:
* <p>
* <ol>
* <li>If the phone supports bluetooth, use it.
* <li>If the phone doesn't support bluetooth and support speaker, use speaker
* <li>Otherwise, do nothing. Hopefully no phones won't have bt or speaker.
* </ol>
*/
private void trySpeakerAudioRouteIfNecessary() {
if (mUiCallManager == null) {
return;
}
int supportedAudioRouteMask = mUiCallManager.getSupportedAudioRouteMask();
boolean supportsBluetooth = (supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0;
boolean supportsSpeaker = (supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0;
boolean isUsingBluetooth =
mUiCallManager.getAudioRoute() == CallAudioState.ROUTE_BLUETOOTH;
if (supportsBluetooth && !isUsingBluetooth) {
mUiCallManager.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
} else if (!supportsBluetooth && supportsSpeaker) {
mUiCallManager.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
}
}
private void openDialpad(boolean animate) {
if (mToggleDialpadButton.isActivated()) {
return;
}
mToggleDialpadButton.setActivated(true);
// This array of of size 2 because getLocationOnScreen returns (x,y) coordinates.
int[] location = new int[2];
mToggleDialpadButton.getLocationOnScreen(location);
// The dialpad should be aligned with the right edge of mToggleDialpadButton.
int startingMargin = location[1] + mToggleDialpadButton.getWidth();
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) mDialpadContainer.getLayoutParams();
if (layoutParams.getMarginStart() != startingMargin) {
layoutParams.setMarginStart(startingMargin);
mDialpadContainer.setLayoutParams(layoutParams);
}
Animation anim = new DialpadAnimation(getContext(), false /* reverse */, animate);
mDialpadContainer.startAnimation(anim);
}
private void closeDialpad() {
if (!mToggleDialpadButton.isActivated()) {
return;
}
mToggleDialpadButton.setActivated(false);
Animation anim = new DialpadAnimation(getContext(), true /* reverse */);
mDialpadContainer.startAnimation(anim);
}
private final View.OnTouchListener mDialpadTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Character digit = mDialpadButtonMap.get(v.getId());
if (digit == null) {
Log.w(TAG, "Unknown dialpad button pressed.");
return false;
}
if (event.getAction() == MotionEvent.ACTION_DOWN) {
v.setPressed(true);
mUiCallManager.playDtmfTone(mUiCallManager.getPrimaryCall(), digit);
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
v.setPressed(false);
v.performClick();
mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
return true;
}
return false;
}
};
private final View.OnKeyListener mDialpadKeyListener = new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Character digit = mDialpadButtonMap.get(v.getId());
if (digit == null) {
Log.w(TAG, "Unknown dialpad button pressed.");
return false;
}
if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_CENTER) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
v.setPressed(true);
mUiCallManager.playDtmfTone(mUiCallManager.getPrimaryCall(), digit);
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
v.setPressed(false);
mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
return true;
}
return false;
}
};
private final Runnable mUpdateDurationRunnable = new Runnable() {
@Override
public void run() {
UiCall primaryCall = mUiCallManager.getPrimaryCall();
if (primaryCall.getState() != Call.STATE_ACTIVE) {
return;
}
String callInfoText = TelecomUtils.getCallInfoText(getContext(),
primaryCall, mCallInfoLabel);
setStateText(callInfoText);
mHandler.postDelayed(this /* runnable */, DateUtils.SECOND_IN_MILLIS);
}
};
private final Runnable mStopDtmfToneRunnable =
() -> mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
private final class DialpadAnimation extends Animation {
private static final int DURATION = 300;
private static final float MAX_SCRIM_ALPHA = 0.6f;
private final int mStartingTranslation;
private final int mScrimColor;
private final boolean mReverse;
DialpadAnimation(Context context, boolean reverse) {
this(context, reverse, true);
}
DialpadAnimation(Context context, boolean reverse, boolean animate) {
setDuration(animate ? DURATION : 0);
setInterpolator(new AccelerateDecelerateInterpolator());
mStartingTranslation = context.getResources().getDimensionPixelOffset(
R.dimen.in_call_card_dialpad_translation_x);
mScrimColor = context.getColor(R.color.phone_theme);
mReverse = reverse;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (mReverse) {
interpolatedTime = 1f - interpolatedTime;
}
int translationX = (int) (mStartingTranslation * (1f - interpolatedTime));
mDialpadContainer.setTranslationX(translationX);
mDialpadContainer.setAlpha(interpolatedTime);
if (interpolatedTime == 0f) {
mDialpadContainer.setVisibility(View.GONE);
} else {
mDialpadContainer.setVisibility(View.VISIBLE);
}
float alpha = 255f * interpolatedTime * MAX_SCRIM_ALPHA;
mLargeContactPhotoView.setColorFilter(Color.argb((int) alpha, Color.red(mScrimColor),
Color.green(mScrimColor), Color.blue(mScrimColor)));
mSecondaryNameTextView.setAlpha(1f - interpolatedTime);
mSecondaryStateTextView.setAlpha(1f - interpolatedTime);
}
}
private final CallListener mCallListener = new CallListener() {
@Override
public void onCallAdded(UiCall call) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCallAdded(); call: " + call);
}
updateCalls();
trySpeakerAudioRouteIfNecessary();
}
@Override
public void onCallRemoved(UiCall call) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCallRemoved(); call: " + call);
}
mLastRemovedCall = call;
updateCalls();
}
@Override
public void onAudioStateChanged(boolean isMuted, int audioRoute,
int supportedAudioRouteMask) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("onAudioStateChanged(); isMuted: %b, audioRoute: %d, "
+ " supportedAudioRouteMask: %d", isMuted, audioRoute,
supportedAudioRouteMask));
}
mMuteButton.setActivated(isMuted);
trySpeakerAudioRouteIfNecessary();
}
@Override
public void onStateChanged(UiCall call, int state) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onStateChanged(); call: " + call + ", state: " + state);
}
updateCalls();
}
@Override
public void onCallUpdated(UiCall call) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCallUpdated(); call: " + call);
}
updateCalls();
}
};
}