/* * 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 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 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: *

*

    *
  1. If the phone supports bluetooth, use it. *
  2. If the phone doesn't support bluetooth and support speaker, use speaker *
  3. Otherwise, do nothing. Hopefully no phones won't have bt or speaker. *
*/ 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(); } }; }