diff options
Diffstat (limited to 'src')
32 files changed, 4666 insertions, 73 deletions
diff --git a/src/com/android/incallui/AnswerFragment.java b/src/com/android/incallui/AnswerFragment.java index c76c5071..1a5e8b6e 100644 --- a/src/com/android/incallui/AnswerFragment.java +++ b/src/com/android/incallui/AnswerFragment.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -136,6 +140,28 @@ public class AnswerFragment extends BaseFragment<AnswerPresenter, AnswerPresente } @Override + public void showVideoButtons() { + Log.d(this, "ims video "); + final int targetResourceId = R.array.incoming_call_widget_6way_ims_targets; + + if (targetResourceId != mGlowpad.getTargetResourceId()) { + // Answer, Decline, Respond via SMS, and Video options + // (VT,VoLTE,VT-TX,VT-RX) + mGlowpad.setTargetResources(R.array.incoming_call_widget_6way_ims_targets); + mGlowpad.setTargetDescriptionsResourceId( + R.array.incoming_call_widget_6way_ims_target_descriptions); + mGlowpad.setDirectionDescriptionsResourceId( + R.array.incoming_call_widget_6way_ims_direction_descriptions); + + mGlowpad.reset(false); + } + } + + public boolean isMessageDialogueShowing() { + return mCannedResponsePopup != null && mCannedResponsePopup.isShowing(); + } + + @Override public void showMessageDialog() { final ListView lv = new ListView(getActivity()); @@ -238,6 +264,13 @@ public class AnswerFragment extends BaseFragment<AnswerPresenter, AnswerPresente getPresenter().onDismissDialog(); } }) + .setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + dismissCustomMessagePopup(); + getPresenter().onDismissDialog(); + } + }) .setTitle(R.string.respond_via_sms_custom_message); mCustomMessagePopup = builder.create(); @@ -281,8 +314,8 @@ public class AnswerFragment extends BaseFragment<AnswerPresenter, AnswerPresente } @Override - public void onAnswer() { - getPresenter().onAnswer(); + public void onAnswer(int callType) { + getPresenter().onAnswer(callType); } @Override diff --git a/src/com/android/incallui/AnswerPresenter.java b/src/com/android/incallui/AnswerPresenter.java index dd4deeb6..a90685f4 100644 --- a/src/com/android/incallui/AnswerPresenter.java +++ b/src/com/android/incallui/AnswerPresenter.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -94,7 +98,11 @@ public class AnswerPresenter extends Presenter<AnswerPresenter.AnswerUi> call.getCallId()); getUi().showAnswerUi(true); - if (call.can(Call.Capabilities.RESPOND_VIA_TEXT) && textMsgs != null) { + if(CallUtils.isVideoCall(call)) { + getUi().showVideoButtons(); + if (textMsgs != null) + getUi().configureMessageDialog(textMsgs); + } else if (call.can(Call.Capabilities.RESPOND_VIA_TEXT) && textMsgs != null) { getUi().showTextButton(true); getUi().configureMessageDialog(textMsgs); } else { @@ -118,14 +126,14 @@ public class AnswerPresenter extends Presenter<AnswerPresenter.AnswerUi> } } - public void onAnswer() { + public void onAnswer(int callType) { if (mCallId == Call.INVALID_CALL_ID) { return; } - Log.d(this, "onAnswer " + mCallId); + Log.d(this, "onAnswer: callId=" + mCallId + "callType=" + callType); - CallCommandClient.getInstance().answerCall(mCallId); + CallCommandClient.getInstance().answerCallWithCallType(mCallId, callType); } public void onDecline() { @@ -154,6 +162,7 @@ public class AnswerPresenter extends Presenter<AnswerPresenter.AnswerUi> interface AnswerUi extends Ui { public void showAnswerUi(boolean show); + public void showVideoButtons(); public void showTextButton(boolean show); public void showMessageDialog(); public void configureMessageDialog(ArrayList<String> textResponses); diff --git a/src/com/android/incallui/CallButtonFragment.java b/src/com/android/incallui/CallButtonFragment.java index ed769033..a5f3c837 100644 --- a/src/com/android/incallui/CallButtonFragment.java +++ b/src/com/android/incallui/CallButtonFragment.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,6 +35,7 @@ import android.widget.PopupMenu.OnDismissListener; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.ToggleButton; +import com.android.internal.telephony.util.BlacklistUtils; import com.android.services.telephony.common.AudioMode; /** @@ -48,6 +53,9 @@ public class CallButtonFragment private ImageButton mMergeButton; private ImageButton mAddCallButton; private ImageButton mSwapButton; + private ImageButton mBlacklistButton; + private ImageButton mAddParticipantButton; + private ImageButton mModifyCallButton; private PopupMenu mAudioModePopup; private boolean mAudioModePopupVisible; @@ -141,6 +149,20 @@ public class CallButtonFragment mMergeButton.setOnClickListener(this); mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton); mSwapButton.setOnClickListener(this); + mAddParticipantButton = (ImageButton) parent.findViewById(R.id.addParticipant); + mAddParticipantButton.setOnClickListener(this); + + // "Add to black list" button + mBlacklistButton = (ImageButton) parent.findViewById(R.id.addBlacklistButton); + if (BlacklistUtils.isBlacklistEnabled(getActivity())) { + mBlacklistButton.setVisibility(View.VISIBLE); + mBlacklistButton.setOnClickListener(this); + } else { + mBlacklistButton.setVisibility(View.GONE); + } + + mModifyCallButton = (ImageButton) parent.findViewById(R.id.modifyCallButton); + mModifyCallButton.setOnClickListener(this); return parent; } @@ -183,6 +205,15 @@ public class CallButtonFragment case R.id.dialpadButton: getPresenter().showDialpadClicked(mShowDialpadButton.isChecked()); break; + case R.id.addBlacklistButton: + getPresenter().blacklistClicked(getActivity()); + break; + case R.id.addParticipant: + getPresenter().addParticipantClicked(); + break; + case R.id.modifyCallButton: + getPresenter().modifyCallButtonClicked(); + break; default: Log.wtf(this, "onClick: unexpected"); break; @@ -190,11 +221,8 @@ public class CallButtonFragment } @Override - public void setEnabled(boolean isEnabled) { - View view = getView(); - if (view.getVisibility() != View.VISIBLE) { - view.setVisibility(View.VISIBLE); - } + public void setEnabled(boolean isEnabled, boolean isVisible) { + getView().setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); // The main end-call button spanning across the screen. mEndCallButton.setEnabled(isEnabled); @@ -207,6 +235,8 @@ public class CallButtonFragment mMergeButton.setEnabled(isEnabled); mAddCallButton.setEnabled(isEnabled); mSwapButton.setEnabled(isEnabled); + mBlacklistButton.setEnabled(isEnabled); + mAddParticipantButton.setEnabled(isEnabled); } @Override @@ -254,6 +284,10 @@ public class CallButtonFragment mAddCallButton.setEnabled(enabled); } + public void enableAddParticipant(boolean show) { + mAddParticipantButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + @Override public void setAudio(int mode) { updateAudioButtons(getPresenter().getSupportedAudio()); @@ -298,6 +332,23 @@ public class CallButtonFragment return true; } + @Override + public void displayModifyCallOptions(int callId) { + if (getActivity() != null && getActivity() instanceof InCallActivity) { + ((InCallActivity) getActivity()).displayModifyCallOptions(callId); + } + } + + @Override + public void enableModifyCall(boolean enabled) { + mModifyCallButton.setEnabled(enabled); + } + + @Override + public void showModifyCall(boolean show) { + mModifyCallButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + // PopupMenu.OnDismissListener implementation; see showAudioModePopup(). // This gets called when the PopupMenu gets dismissed for *any* reason, like // the user tapping outside its bounds, or pressing Back, or selecting one diff --git a/src/com/android/incallui/CallButtonPresenter.java b/src/com/android/incallui/CallButtonPresenter.java index be257378..2bd72bab 100644 --- a/src/com/android/incallui/CallButtonPresenter.java +++ b/src/com/android/incallui/CallButtonPresenter.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,15 +26,20 @@ import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.incallui.InCallPresenter.IncomingCallListener; import com.android.services.telephony.common.AudioMode; import com.android.services.telephony.common.Call; +import com.android.services.telephony.common.CallDetails; import com.android.services.telephony.common.Call.Capabilities; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; import android.telephony.PhoneNumberUtils; /** * Logic for call buttons. */ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButtonUi> - implements InCallStateListener, AudioModeListener, IncomingCallListener { + implements InCallStateListener, AudioModeListener, IncomingCallListener, + CallList.ActiveSubChangeListener { private Call mCall; private boolean mAutomaticallyMuted = false; @@ -53,6 +62,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto // register for call state changes last InCallPresenter.getInstance().addListener(this); InCallPresenter.getInstance().addIncomingCallListener(this); + CallList.getInstance().addActiveSubChangeListener(this); } @Override @@ -62,6 +72,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto InCallPresenter.getInstance().removeListener(this); AudioModeProvider.getInstance().removeListener(this); InCallPresenter.getInstance().removeIncomingCallListener(this); + CallList.getInstance().removeActiveSubChangeListener(this); } @Override @@ -190,6 +201,10 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto CallCommandClient.getInstance().merge(); } + public void addParticipantClicked() { + InCallPresenter.getInstance().sendAddParticipantIntent(); + } + public void addCallClicked() { // Automatically mute the current call mAutomaticallyMuted = true; @@ -204,12 +219,41 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto CallCommandClient.getInstance().swap(); } + public void blacklistClicked(final Context context) { + if (mCall == null) { + return; + } + + final String number = mCall.getNumber(); + final String message = context.getString(R.string.blacklist_dialog_message, number); + + new AlertDialog.Builder(context) + .setTitle(R.string.blacklist_dialog_title) + .setMessage(message) + .setPositiveButton(R.string.alert_dialog_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(this, "hanging up due to blacklist: " + mCall.getCallId()); + CallCommandClient.getInstance().blacklistAndHangup(mCall.getCallId()); + } + }) + .setNegativeButton(R.string.alert_dialog_no, null) + .show(); + } + public void showDialpadClicked(boolean checked) { Log.v(this, "Show dialpad " + String.valueOf(checked)); getUi().displayDialpad(checked); updateExtraButtonRow(); } + public void modifyCallButtonClicked() { + Call call = CallList.getInstance().getActiveCall(); + if (call != null) { + getUi().displayModifyCallOptions(call.getCallId()); + } + } + private void updateUi(InCallState state, Call call) { final CallButtonUi ui = getUi(); if (ui == null) { @@ -219,7 +263,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto final boolean isEnabled = state.isConnectingOrConnected() && !state.isIncoming() && call != null; - ui.setEnabled(isEnabled); + ui.setEnabled(isEnabled, !state.isIncoming()); Log.d(this, "Updating call UI for call: ", call); @@ -230,10 +274,12 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto Log.v(this, "Show swap ", call.can(Capabilities.SWAP_CALLS)); Log.v(this, "Show add call ", call.can(Capabilities.ADD_CALL)); Log.v(this, "Show mute ", call.can(Capabilities.MUTE)); + Log.v(this, "Show modify call ", call.can(Capabilities.MODIFY_CALL)); final boolean canMerge = call.can(Capabilities.MERGE_CALLS); final boolean canAdd = call.can(Capabilities.ADD_CALL); final boolean isGenericConference = call.can(Capabilities.GENERIC_CONFERENCE); + final boolean canModifyCall = call.can(Capabilities.MODIFY_CALL); final boolean showMerge = !isGenericConference && canMerge; @@ -281,8 +327,13 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto } } + ui.enableAddParticipant(call.can(Capabilities.ADD_PARTICIPANT)); + ui.enableMute(call.can(Capabilities.MUTE)); + ui.enableModifyCall(canModifyCall); + ui.showModifyCall(canModifyCall); + // Finally, update the "extra button row": It's displayed above the // "End" button, but only if necessary. Also, it's never displayed // while the dialpad is visible (since it would overlap.) @@ -330,7 +381,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto } public interface CallButtonUi extends Ui { - void setEnabled(boolean on); + void setEnabled(boolean on, boolean visible); void setMute(boolean on); void enableMute(boolean enabled); void setHold(boolean on); @@ -340,6 +391,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto void showSwap(boolean show); void showAddCall(boolean show); void enableAddCall(boolean enabled); + void enableAddParticipant(boolean show); void displayDialpad(boolean on); boolean isDialpadVisible(); void setAudio(int mode); @@ -348,5 +400,16 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto void showGenericMergeButton(); void hideExtraRow(); void displayManageConferencePanel(boolean on); + void displayModifyCallOptions(int callId); + void enableModifyCall(boolean enabled); + void showModifyCall(boolean show); + } + + @Override + public void onActiveSubChanged(int subscription) { + InCallState state = InCallPresenter.getInstance() + .getPotentialStateFromCallList(CallList.getInstance()); + + onStateChange(state, CallList.getInstance()); } } diff --git a/src/com/android/incallui/CallCardFragment.java b/src/com/android/incallui/CallCardFragment.java index 0d26b82c..02faa24e 100644 --- a/src/com/android/incallui/CallCardFragment.java +++ b/src/com/android/incallui/CallCardFragment.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +26,9 @@ import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; +import static android.telephony.TelephonyManager.SIM_STATE_ABSENT; +import android.telephony.MSimTelephonyManager; +import android.os.SystemProperties; import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; @@ -33,6 +40,7 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.TextView; +import com.android.services.telephony.common.AudioMode; import com.android.services.telephony.common.Call; import java.util.List; @@ -54,6 +62,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr private View mProviderInfo; private TextView mProviderLabel; private TextView mProviderNumber; + private TextView mSubscriptionId; private ViewGroup mSupplementaryInfoContainer; // Secondary caller info @@ -65,6 +74,25 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr // Cached DisplayMetrics density. private float mDensity; + private VideoCallPanel mVideoCallPanel; + private boolean mAudioDeviceInitialized = false; + + // Constants for TelephonyProperties.PROPERTY_IMS_AUDIO_OUTPUT property. + // Currently, the default audio output is headset if connected, bluetooth + // if connected, speaker/earpiece for video/voice call. + private static final int IMS_AUDIO_OUTPUT_DEFAULT = 0; + private static final int IMS_AUDIO_OUTPUT_DISABLE_SPEAKER = 1; + + /** + * Controls audio route for VT calls. + * 0 - Use the default audio routing strategy. + * 1 - Disable the speaker. Route the audio to Headset or Bloutooth + * or Earpiece, based on the default audio routing strategy. + * This property is for testing purpose only. + */ + static final String PROPERTY_IMS_AUDIO_OUTPUT = + "persist.radio.ims.audio.output"; + @Override CallCardPresenter.CallCardUi getUi() { return this; @@ -115,8 +143,10 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr mProviderInfo = view.findViewById(R.id.providerInfo); mProviderLabel = (TextView) view.findViewById(R.id.providerLabel); mProviderNumber = (TextView) view.findViewById(R.id.providerAddress); + mSubscriptionId = (TextView) view.findViewById(R.id.subId); mSupplementaryInfoContainer = (ViewGroup) view.findViewById(R.id.supplementary_info_container); + mVideoCallPanel = (VideoCallPanel) view.findViewById(R.id.videoCallPanel); } @Override @@ -172,14 +202,15 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr } else { mNumberLabel.setVisibility(View.GONE); } - } @Override public void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall) { + Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall, + boolean isForwarded, boolean isVideo) { Log.d(this, "Setting primary call"); + if (isConference) { name = getConferenceString(isGeneric); photo = getConferencePhoto(isGeneric); @@ -194,9 +225,26 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr // Set the label (Mobile, Work, etc) setPrimaryLabel(label); - showInternetCallLabel(isSipCall); + showCallTypeLabel(isSipCall, isForwarded); + MSimTelephonyManager tm = MSimTelephonyManager.getDefault(); + int numPhones = tm.getPhoneCount(); + + if (tm.isMultiSimEnabled() && !(tm.getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA)) { + int subscription = getPresenter().getActiveSubscription(); + String operatorName = tm.getSimState(subscription) != SIM_STATE_ABSENT + ? tm.getNetworkOperatorName(subscription) : getString(R.string.sub_no_sim); + String sub = getString(R.string.multi_sim_entry_format, operatorName, + subscription + 1); + + if (subscription != -1) { + showSubscriptionInfo(sub); + } + } - setDrawableToImageView(mPhoto, photo); + if (! isVideo) { + setDrawableToImageView(mPhoto, photo); + } } @Override @@ -234,11 +282,20 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr @Override public void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn, - String gatewayLabel, String gatewayNumber) { + String gatewayLabel, String gatewayNumber, boolean isHeldRemotely, int callType) { String callStateLabel = null; + // If this is a video call then update the state of the VideoCallPanel + if (CallUtils.isVideoCall(callType)) { + updateVideoCallState(state, callType); + } else { + // This will hide the VideoCallPanel for any non VT/ non VS call or + // downgrade scenarios + hideVideoCallWidgets(); + } + // States other than disconnected not yet supported - callStateLabel = getCallStateLabelFromState(state, cause); + callStateLabel = getCallStateLabelFromState(state, cause, isHeldRemotely); Log.v(this, "setCallState " + callStateLabel); Log.v(this, "DisconnectCause " + cause); @@ -290,12 +347,13 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr } } - private void showInternetCallLabel(boolean show) { - if (show) { - final String label = getView().getContext().getString( - R.string.incall_call_type_label_sip); + private void showCallTypeLabel(boolean isSipCall, boolean isForwarded) { + if (isSipCall) { + mCallTypeLabel.setVisibility(View.VISIBLE); + mCallTypeLabel.setText(R.string.incall_call_type_label_sip); + } else if (isForwarded) { mCallTypeLabel.setVisibility(View.VISIBLE); - mCallTypeLabel.setText(label); + mCallTypeLabel.setText(R.string.incall_call_type_label_forwarded); } else { mCallTypeLabel.setVisibility(View.GONE); } @@ -314,6 +372,15 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr } } + private void showSubscriptionInfo(String subString) { + if (!TextUtils.isEmpty(subString)) { + mSubscriptionId.setText(subString); + mSubscriptionId.setVisibility(View.VISIBLE); + } else { + mSubscriptionId.setVisibility(View.GONE); + } + } + private void setDrawableToImageView(ImageView view, Drawable photo) { if (photo == null) { photo = view.getResources().getDrawable(R.drawable.picture_unknown); @@ -360,7 +427,8 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr * Gets the call state label based on the state of the call and * cause of disconnect */ - private String getCallStateLabelFromState(int state, Call.DisconnectCause cause) { + private String getCallStateLabelFromState(int state, Call.DisconnectCause cause, + boolean isHeldRemotely) { final Context context = getView().getContext(); String callStateLabel = null; // Label to display as part of the call banner @@ -370,11 +438,14 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr } else if (Call.State.ACTIVE == state) { // We normally don't show a "call state label" at all in // this state (but see below for some special cases). - + if (isHeldRemotely) { + callStateLabel = context.getString(R.string.card_title_waiting_call); + } } else if (Call.State.ONHOLD == state) { callStateLabel = context.getString(R.string.card_title_on_hold); } else if (Call.State.DIALING == state) { - callStateLabel = context.getString(R.string.card_title_dialing); + callStateLabel = context.getString(isHeldRemotely + ? R.string.card_title_dialing_waiting : R.string.card_title_dialing); } else if (Call.State.REDIALING == state) { callStateLabel = context.getString(R.string.card_title_redialing); } else if (Call.State.INCOMING == state || Call.State.CALL_WAITING == state) { @@ -509,6 +580,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr dispatchPopulateAccessibilityEvent(event, mPrimaryName); dispatchPopulateAccessibilityEvent(event, mPhoneNumber); dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); + dispatchPopulateAccessibilityEvent(event, mSubscriptionId); dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); return; @@ -524,4 +596,149 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr eventText.add(null); } } + + /** + * Updates the VideoCallPanel based on the current state of the call + * TODO: Move to a separate file. + * @param call + */ + private void updateVideoCallState(int callState, int callType) { + log(" - Videocall.state: " + callState); + + // Null check + if (mVideoCallPanel == null) { + loge("VideocallPanel is null"); + return; + } + switch (callState) { + case Call.State.INCOMING: + break; + + case Call.State.DIALING: + case Call.State.REDIALING: + case Call.State.ACTIVE: + initVideoCall(callType); + showVideoCallWidgets(callType); + break; + + case Call.State.DISCONNECTING: + case Call.State.DISCONNECTED: + case Call.State.ONHOLD: + case Call.State.IDLE: + case Call.State.CALL_WAITING: + hideVideoCallWidgets(); + break; + + default: + Log.e(this, "videocall: updateVideoCallState in bad state:" + callState); + hideVideoCallWidgets(); + break; + } + } + + /** + * If this is a video call then hide the photo widget and show the video + * call panel + */ + private void showVideoCallWidgets(int callType) { + + if (isPhotoVisible()) { + log("show videocall widget"); + mPhoto.setVisibility(View.GONE); + } + + mVideoCallPanel.setVisibility(View.VISIBLE); + mVideoCallPanel.setPanelElementsVisibility(callType); + mVideoCallPanel.startOrientationListener(true); + } + + /** + * Hide the video call widget and restore the photo widget and reset + * mAudioDeviceInitialized + */ + private void hideVideoCallWidgets() { + mAudioDeviceInitialized = false; + + if ((mVideoCallPanel != null) && (mVideoCallPanel.getVisibility() == View.VISIBLE)) { + log("Hide videocall widget"); + + mPhoto.setVisibility(View.VISIBLE); + mVideoCallPanel.setVisibility(View.GONE); + mVideoCallPanel.setCameraNeeded(false); + mVideoCallPanel.startOrientationListener(false); + } + } + + /** + * Initializes the video call widgets if not already initialized + */ + private void initVideoCall(int callType) { + /* + * 1. Speaker state is updated only at the beginning of a video call 2. + * For MO video call, speaker update happens in dialing state 3. For MT + * video call, it happens in active state 4. Speaker state not changed + * during a call when VOLTE<->VT call type change happens. + */ + log("initVideoCall mAudioDeviceInitialized: " + mAudioDeviceInitialized); + if (!mAudioDeviceInitialized ) { + switchInVideoCallAudio(); // Set audio to speaker by default + mAudioDeviceInitialized = true; + } + // Choose camera direction based on call type + mVideoCallPanel.onCallInitiating(callType); + } + + /** + * Switches the current routing of in-call audio for the video call + */ + private void switchInVideoCallAudio() { + Log.d(this,"In switchInVideoCallAudio"); + + // If the wired headset is connected then the AudioService takes care of + // routing audio to the headset + int mode = AudioModeProvider.getInstance().getAudioMode(); + CallCommandClient.getInstance().setAudioMode(mode); + if (mode == AudioMode.WIRED_HEADSET) { + Log.d(this,"Wired headset connected, not routing audio to speaker"); + return; + } + + // If the bluetooth is available then BluetoothHandsfree class takes + // care of making sure that the audio is routed to Bluetooth by default. + // However if the audio is not connected to Bluetooth because user wanted + // audio off then continue to turn on the speaker + if (mode == AudioMode.BLUETOOTH ) { + Log.d(this, "Bluetooth connected, not routing audio to speaker"); + return; + } + + // If the speaker is explicitly disabled then do not enable it. + if (SystemProperties.getInt(PROPERTY_IMS_AUDIO_OUTPUT, + IMS_AUDIO_OUTPUT_DEFAULT) == IMS_AUDIO_OUTPUT_DISABLE_SPEAKER) { + Log.d(this, "Speaker disabled, not routing audio to speaker"); + return; + } + + // If the bluetooth headset or the wired headset is not connected and + // the speaker is not disabled then turn on speaker by default + // for the VT call + CallCommandClient.getInstance().setAudioMode(AudioMode.SPEAKER); + } + + /** + * Return true if mPhoto is available and is visible + * + * @return + */ + private boolean isPhotoVisible() { + return ((mPhoto != null) && (mPhoto.getVisibility() == View.VISIBLE)); + } + + private void log(String msg) { + Log.d(this, msg); + } + + private void loge(String msg) { + Log.e(this, msg); + } } diff --git a/src/com/android/incallui/CallCardPresenter.java b/src/com/android/incallui/CallCardPresenter.java index 949d7189..5697aaca 100644 --- a/src/com/android/incallui/CallCardPresenter.java +++ b/src/com/android/incallui/CallCardPresenter.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +25,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.graphics.Bitmap; +import android.provider.MediaStore.Audio; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.text.format.DateUtils; @@ -36,6 +41,7 @@ import com.android.services.telephony.common.Call; import com.android.services.telephony.common.Call.Capabilities; import com.android.services.telephony.common.CallIdentification; import com.google.common.base.Preconditions; +import com.android.incallui.CallUtils; /** * Presenter for the Call Card Fragment. @@ -150,6 +156,7 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> Log.d(this, "Secondary call: " + secondary); final boolean primaryChanged = !areCallsSame(mPrimary, primary); + final boolean primaryForwardedChanged = isForwarded(mPrimary) != isForwarded(primary); final boolean secondaryChanged = !areCallsSame(mSecondary, secondary); mSecondary = secondary; mPrimary = primary; @@ -158,9 +165,11 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> // primary call has changed mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary.getIdentification(), mPrimary.getState() == Call.State.INCOMING); - updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary)); maybeStartSearch(mPrimary, true); } + if ((primaryChanged || primaryForwardedChanged) && mPrimary != null) { + updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary)); + } if (mSecondary == null) { // Secondary call may have ended. Update the ui. @@ -185,13 +194,16 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> } // Set the call state + final int callType = CallUtils.getCallType(mPrimary); + if (mPrimary != null) { final boolean bluetoothOn = (AudioModeProvider.getInstance().getAudioMode() == AudioMode.BLUETOOTH); ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn, - getGatewayLabel(), getGatewayNumber()); + getGatewayLabel(), getGatewayNumber(), mPrimary.isHeldRemotely(), callType); } else { - ui.setCallState(Call.State.IDLE, Call.DisconnectCause.UNKNOWN, false, null, null); + ui.setCallState(Call.State.IDLE, Call.DisconnectCause.UNKNOWN, + false, null, null, false, callType); } } @@ -201,7 +213,8 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> final boolean bluetoothOn = (AudioMode.BLUETOOTH == mode); getUi().setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn, - getGatewayLabel(), getGatewayNumber()); + getGatewayLabel(), getGatewayNumber(), mPrimary.isHeldRemotely(), + CallUtils.getCallType(mPrimary)); } } @@ -236,7 +249,8 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> } // otherwise compare call Ids - return call1.getCallId() == call2.getCallId(); + return (call1.getCallId() == call2.getCallId()) && + call1.getCallDetails().isMpty() == call2.getCallDetails().isMpty(); } private void maybeStartSearch(Call call, boolean isPrimary) { @@ -272,7 +286,8 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> return; } if (entry.photo != null) { - if (mPrimary != null && callId == mPrimary.getCallId()) { + if (mPrimary != null && !CallUtils.isVideoCall(mPrimary) && + callId == mPrimary.getCallId()) { getUi().setPrimaryImage(entry.photo); } else if (mSecondary != null && callId == mSecondary.getCallId()) { getUi().setSecondaryImage(entry.photo); @@ -290,6 +305,10 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> return call != null && call.can(Capabilities.GENERIC_CONFERENCE); } + private static boolean isForwarded(Call call) { + return call != null && call.isForwarded(); + } + private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary, boolean isConference) { if (isPrimary) { @@ -354,14 +373,18 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> } final boolean isGenericConf = isGenericConference(mPrimary); + final boolean isForwarded = isForwarded(mPrimary); + final boolean isVideo = CallUtils.isVideoCall(mPrimary); if (entry != null) { final String name = getNameForCall(entry); final String number = getNumberForCall(entry); final boolean nameIsNumber = name != null && name.equals(entry.number); ui.setPrimary(number, name, nameIsNumber, entry.label, - entry.photo, isConference, isGenericConf, entry.isSipCall); + entry.photo, isConference, isGenericConf, + entry.isSipCall, isForwarded, isVideo); } else { - ui.setPrimary(null, null, false, null, null, isConference, isGenericConf, false); + ui.setPrimary(null, null, false, null, null, isConference, + isGenericConf, false, isForwarded, isVideo); } } @@ -458,16 +481,21 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> public interface CallCardUi extends Ui { void setVisible(boolean on); void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall); + Drawable photo, boolean isConference, boolean isGeneric, + boolean isSipCall, boolean isForwarded, boolean isVideo); void setSecondary(boolean show, String name, boolean nameIsNumber, String label, Drawable photo, boolean isConference, boolean isGeneric); void setSecondaryImage(Drawable image); void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn, - String gatewayLabel, String gatewayNumber); + String gatewayLabel, String gatewayNumber, boolean isHeldRemotely, int callType); void setPrimaryCallElapsedTime(boolean show, String duration); void setPrimaryName(String name, boolean nameIsNumber); void setPrimaryImage(Drawable image); void setPrimaryPhoneNumber(String phoneNumber); void setPrimaryLabel(String label); } + + public int getActiveSubscription() { + return CallCommandClient.getInstance().getActiveSubscription(); + } } diff --git a/src/com/android/incallui/CallCommandClient.java b/src/com/android/incallui/CallCommandClient.java index 52d2100c..3102c3e5 100644 --- a/src/com/android/incallui/CallCommandClient.java +++ b/src/com/android/incallui/CallCommandClient.java @@ -18,6 +18,7 @@ package com.android.incallui; import android.os.RemoteException; +import com.android.internal.telephony.MSimConstants; import com.android.services.telephony.common.AudioMode; import com.android.services.telephony.common.ICallCommandService; @@ -230,6 +231,60 @@ public class CallCommandClient { } } + public void hangupWithReason(int callId, String userUri, boolean mpty, + int failCause, String errorInfo) { + if (mCommandService == null) { + Log.e(this, "Cannot hangupWithReason(); CallCommandService == null"); + return; + } + try { + Log.v(this, "hangupWithReason() "); + mCommandService.hangupWithReason(callId, userUri, mpty, + failCause, errorInfo); + } catch (RemoteException e) { + Log.e(this, "Error on hangupWithReason().", e); + } + } + + public void answerCallWithCallType(int callId,int callType){ + if (mCommandService == null) { + Log.e(this, "Cannot acceptCall(); CallCommandService == null"); + return; + } + try { + Log.v(this, "acceptCall() " ); + mCommandService.answerCallWithCallType(callId,callType); + } catch (RemoteException e) { + Log.e(this, "Error on acceptCall().", e); + } + } + + public void modifyCallInitiate(int callId, int callType) { + if (mCommandService == null) { + Log.e(this, "Cannot modifyCall(); CallCommandService == null"); + return; + } + try { + Log.v(this, "modifyCall(), callId=" + callId + " callType=" + callType); + mCommandService.modifyCallInitiate(callId, callType); + } catch (RemoteException e) { + Log.e(this, "Error on modifyCall()."); + } + } + + public void modifyCallConfirm(boolean responseType, int callId) { + if (mCommandService == null) { + Log.e(this, "Cannot modifyCallConfirm(); CallCommandService == null" + responseType); + return; + } + try { + Log.v(this, "modifyCallConfirm() "); + mCommandService.modifyCallConfirm(responseType, callId); + } catch (RemoteException e) { + Log.e(this, "Error on modifyCallConfirm()."); + } + } + public void setSystemBarNavigationEnabled(boolean enable) { if (mCommandService == null) { Log.e(this, "Cannot setSystemBarNavigationEnabled(); CallCommandService == null"); @@ -243,4 +298,44 @@ public class CallCommandClient { } } + public void blacklistAndHangup(int callId) { + if (mCommandService == null) { + Log.e(this, "Cannot blacklistAndHangup(); CallCommandService == null"); + return; + } + try { + mCommandService.blacklistAndHangup(callId); + } catch (RemoteException e) { + Log.e(this, "Error on blacklistAndHangup().", e); + } + } + + public void setActiveSubscription(int subscriptionId) { + Log.i(this, "set active sub = " + subscriptionId); + if (mCommandService == null) { + Log.e(this, "Cannot set active Sub; CallCommandService == null"); + return; + } + try { + mCommandService.setActiveSubscription(subscriptionId); + } catch (RemoteException e) { + Log.e(this, "Error setActiveSub.", e); + } + } + + public int getActiveSubscription() { + int subscriptionId = MSimConstants.INVALID_SUBSCRIPTION; + + if (mCommandService == null) { + Log.e(this, "Cannot get active sub; CallCommandService == null"); + return subscriptionId; + } + try { + subscriptionId = mCommandService.getActiveSubscription(); + } catch (RemoteException e) { + Log.e(this, "Error getActiveSub.", e); + } + Log.i(this, "get active sub " + subscriptionId); + return subscriptionId; + } } diff --git a/src/com/android/incallui/CallHandlerService.java b/src/com/android/incallui/CallHandlerService.java index 06b10ab2..7a206317 100644 --- a/src/com/android/incallui/CallHandlerService.java +++ b/src/com/android/incallui/CallHandlerService.java @@ -48,8 +48,10 @@ public class CallHandlerService extends Service { private static final int ON_POST_CHAR_WAIT = 8; private static final int ON_START = 9; private static final int ON_DESTROY = 10; + private static final int ON_ACTIVE_SUB_CHANGE = 11; + private static final int ON_UNSOL_CALLMODIFY = 12; - private static final int LARGEST_MSG_ID = ON_DESTROY; + private static final int LARGEST_MSG_ID = ON_ACTIVE_SUB_CHANGE; private CallList mCallList; @@ -59,6 +61,8 @@ public class CallHandlerService extends Service { private AudioModeProvider mAudioModeProvider; private boolean mServiceStarted = false; + private final String LOG_TAG = "CallHandlerService"; + @Override public void onCreate() { Log.i(TAG, "onCreate"); @@ -184,6 +188,22 @@ public class CallHandlerService extends Service { mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_POST_CHAR_WAIT, callId, 0, chars)); } + + @Override + public void onModifyCall(Call call) { + try { + Log.i(TAG, "onModifyCallResponse: " + call); + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UNSOL_CALLMODIFY, call)); + } catch (Exception e) { + Log.e(TAG, "Error processing onDisconnect() call.", e); + } + } + + @Override + public void onActiveSubChanged(int activeSub) { + mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_ACTIVE_SUB_CHANGE, activeSub)); + } + }; private void doStart(ICallCommandService service) { @@ -227,6 +247,19 @@ public class CallHandlerService extends Service { mAudioModeProvider = null; } + public void doModifyCall(Call call) { + Log.d(TAG, "doModifyCall: Call:" + call); + if (call != null && mInCallPresenter != null && mCallList != null) { + Log.d(TAG, "doModifyCall: Updating CallList:" + mCallList.getCall(call.getCallId())); + mCallList.onUpdate(call); + mInCallPresenter.onModifyCallRequest(call); + } else { + Log.e(TAG, "doModifyCall: isCallValid=" + (call != null)); + Log.e(TAG, "doModifyCall: isInCallPresenterValid=" + (mInCallPresenter != null)); + Log.e(TAG, "doModifyCall: isCallListValid=" + (mCallList != null)); + } + } + /** * Handles messages from the service so that they get executed on the main thread, where they * can interact with UI. @@ -302,6 +335,16 @@ public class CallHandlerService extends Service { case ON_DESTROY: doStop(); break; + case ON_UNSOL_CALLMODIFY: + Call call = (Call) msg.obj; + Log.i(TAG, "ON_UNSOL_CALLMODIFY: Call=" + call); + doModifyCall(call); + break; + case ON_ACTIVE_SUB_CHANGE: + Log.i(TAG, "ON_ACTIVE_SUB_CHANGE: " + msg.obj); + mCallList.onActiveSubChanged((Integer) msg.obj); + break; + default: break; } diff --git a/src/com/android/incallui/CallList.java b/src/com/android/incallui/CallList.java index dee27557..9b942ed1 100644 --- a/src/com/android/incallui/CallList.java +++ b/src/com/android/incallui/CallList.java @@ -24,6 +24,8 @@ import com.google.common.base.Preconditions; import android.os.Handler; import android.os.Message; +import android.telephony.MSimTelephonyManager; +import com.android.internal.telephony.MSimConstants; import com.android.services.telephony.common.Call; import com.android.services.telephony.common.Call.DisconnectCause; @@ -44,6 +46,7 @@ public class CallList { private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000; private static final int EVENT_DISCONNECTED_TIMEOUT = 1; + private static final int EVENT_NOTIFY_CHANGE = 2; private static CallList sInstance = new CallList(); @@ -54,6 +57,9 @@ public class CallList { private final HashMap<Integer, List<CallUpdateListener>> mCallUpdateListenerMap = Maps .newHashMap(); + private int mSubscription = 0; + private final ArrayList<ActiveSubChangeListener> mActiveSubChangeListeners = + Lists.newArrayList(); /** * Static singleton accessor method. @@ -74,6 +80,8 @@ public class CallList { public void onUpdate(Call call) { Log.d(this, "onUpdate - ", call); + updateActiveSuscription(); + updateCallInMap(call); notifyListenersOfChange(); } @@ -101,6 +109,8 @@ public class CallList { public void onIncoming(Call call, List<String> textMessages) { Log.d(this, "onIncoming - " + call); + updateActiveSuscription(); + updateCallInMap(call); updateCallTextMap(call, textMessages); @@ -115,6 +125,8 @@ public class CallList { public void onUpdate(List<Call> callsToUpdate) { Log.d(this, "onUpdate(...)"); + updateActiveSuscription(); + Preconditions.checkNotNull(callsToUpdate); for (Call call : callsToUpdate) { Log.d(this, "\t" + call); @@ -283,6 +295,11 @@ public class CallList { * TODO: Improve this logic to sort by call time. */ public Call getCallWithState(int state, int positionToFind) { + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA) { + return getCallWithState(state, positionToFind, getActiveSubscription()); + } + Call retval = null; int position = 0; for (Call call : mCallMap.values()) { @@ -350,16 +367,27 @@ public class CallList { if (call.getState() == Call.State.DISCONNECTED) { // update existing (but do not add!!) disconnected calls if (mCallMap.containsKey(id)) { + final Call.DisconnectCause disconnCause = call.getDisconnectCause(); + Log.d(this, "disconnect cause: " + disconnCause); + if (disconnCause == Call.DisconnectCause.SRVCC_CALL_DROP) { + Log.d(this, "SRVCC call so silently removing call entry"); + //silently remove the call entry + call.setState(Call.State.IDLE); + mCallMap.remove(id); + updated = false; + } else { - // For disconnected calls, we want to keep them alive for a few seconds so that the - // UI has a chance to display anything it needs when a call is disconnected. + // For disconnected calls, we want to keep them alive for a few seconds + // so that the UI has a chance to display anything it needs when a + // call is disconnected. - // Set up a timer to destroy the call after X seconds. - final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); - mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); + // Set up a timer to destroy the call after X seconds. + final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); + mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); - mCallMap.put(id, call); - updated = true; + mCallMap.put(id, call); + updated = true; + } } } else if (!isCallDead(call)) { mCallMap.put(id, call); @@ -438,6 +466,13 @@ public class CallList { Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); finishDisconnectedCall((Call) msg.obj); break; + case EVENT_NOTIFY_CHANGE: + Log.d(this, "EVENT_NOTIFY_CHANGE: "); + notifyListenersOfChange(); + for (ActiveSubChangeListener listener : mActiveSubChangeListeners) { + listener.onActiveSubChanged(getActiveSubscription()); + } + break; default: Log.wtf(this, "Message not expected: " + msg.what); break; @@ -478,4 +513,124 @@ public class CallList { // TODO: refactor and limit arg to be call state. Caller info is not needed. public void onCallStateChanged(Call call); } + + /** + * Called when active subscription changes. + */ + public void onActiveSubChanged(int activeSub) { + Log.d(this, "onActiveSubChanged = " + activeSub); + if (existsLiveCall(activeSub)) { + setActiveSubscription(activeSub); + } + } + + public int getActiveSubscription() { + return mSubscription; + } + + /** + * Called to update the latest active subscription id, and also it + * notifies the registred clients about subscription change information. + */ + public void setActiveSubscription(int subscription) { + if (subscription != mSubscription) { + Log.i(this, "setActiveSubscription, old = " + mSubscription + " new = " + subscription); + mSubscription = subscription; + final Message msg = mHandler.obtainMessage(EVENT_NOTIFY_CHANGE, null); + mHandler.sendMessage(msg); + } + } + + /** + * Returns true, if any voice call in ACTIVE on the provided subscription. + */ + public boolean existsLiveCall(int subscription) { + for (Call call : mCallMap.values()) { + if (!isCallDead(call) && (call.getSubscription() == subscription)) { + return true; + } + } + return false; + } + + /** + * This method checks whether any other subscription currently has active voice + * call other than current active subscription, if yes it makes that other + * subscription as active subscription i.e user visible subscription. + */ + public boolean switchToOtherActiveSubscription() { + int activeSub = getActiveSubscription(); + boolean subSwitched = false; + + for (int i = 0; i < MSimTelephonyManager.getDefault().getPhoneCount(); i++) { + if ((i != activeSub) && existsLiveCall(i)) { + Log.i(this, "switchToOtherActiveSubscription, sub = " + i); + subSwitched = true; + setActiveSubscription(i); + break; + } + } + return subSwitched; + } + + /** + * Method to check if there is any live call in a sub other than the one supplied. + * @param currentSub The subscription to exclude while checking for active calls. + */ + public boolean isAnyOtherSubActive(int currentSub) { + boolean result = false; + for (int i = 0; i < MSimTelephonyManager.getDefault().getPhoneCount(); i++) { + if ((i != currentSub) && existsLiveCall(i)) { + Log.d(this, "Live call found on another sub = " + i); + result = true; + break; + } + } + return result; + } + + /** + * Its a utility, gets the current active subscription from TeleService and + * updates the mSubscription member variable. + */ + public void updateActiveSuscription() { + if (!MSimTelephonyManager.getDefault().isMultiSimEnabled()) { + return; + } + setActiveSubscription(CallCommandClient.getInstance().getActiveSubscription()); + } + + /** + * Returns the [position]th call which belongs to provided subscription and + * found in the call map with the specified state. + */ + public Call getCallWithState(int state, int positionToFind, int subscription) { + Call retval = null; + int position = 0; + for (Call call : mCallMap.values()) { + if ((call.getState() == state) && (call.getSubscription() == subscription)) { + if (position >= positionToFind) { + retval = call; + break; + } else { + position++; + } + } + } + return retval; + } + + public void addActiveSubChangeListener(ActiveSubChangeListener listener) { + Preconditions.checkNotNull(listener); + mActiveSubChangeListeners.add(listener); + } + + public void removeActiveSubChangeListener(ActiveSubChangeListener listener) { + Preconditions.checkNotNull(listener); + mActiveSubChangeListeners.remove(listener); + } + + public interface ActiveSubChangeListener { + public void onActiveSubChanged(int subscription); + } } diff --git a/src/com/android/incallui/CallUtils.java b/src/com/android/incallui/CallUtils.java new file mode 100644 index 00000000..9fb488fd --- /dev/null +++ b/src/com/android/incallui/CallUtils.java @@ -0,0 +1,119 @@ +/* Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import com.android.services.telephony.common.Call; +import com.android.services.telephony.common.CallDetails; +import com.google.common.base.Preconditions; + +public class CallUtils { + + public static boolean isVideoCall(int callType) { + return callType == CallDetails.CALL_TYPE_VT || + callType == CallDetails.CALL_TYPE_VT_TX || + callType == CallDetails.CALL_TYPE_VT_RX; + } + + public static int getCallType(Call call) { + final CallDetails cd = getCallDetails(call); + return cd != null ? cd.getCallType() : CallDetails.CALL_TYPE_UNKNOWN; + } + + public static int getProposedCallType(Call call) { + final CallDetails cd = getCallModifyDetails(call); + return cd != null ? cd.getCallType() : CallDetails.CALL_TYPE_UNKNOWN; + } + + public static boolean hasCallModifyFailed(Call call) { + final CallDetails modifyCallDetails = getCallModifyDetails(call); + boolean hasError = false; + try { + if (modifyCallDetails != null && modifyCallDetails.getErrorInfo() != null) { + hasError = !modifyCallDetails.getErrorInfo().isEmpty() + && Integer.parseInt(modifyCallDetails.getErrorInfo()) != 0; + } + } catch (Exception e) { + hasError = true; + } + return hasError; + } + + private static CallDetails getCallDetails(Call call) { + return call != null ? call.getCallDetails() : null; + } + + private static CallDetails getCallModifyDetails(Call call) { + return call != null ? call.getCallModifyDetails() : null; + } + + public static boolean isVideoCall(Call call) { + if (call == null || call.getCallDetails() == null) { + return false; + } + return isVideoCall(call.getCallDetails().getCallType()); + } + + public static String fromCallType(int callType) { + String str = ""; + switch (callType) { + case CallDetails.CALL_TYPE_VT: + str = "VT"; + break; + case CallDetails.CALL_TYPE_VT_TX: + str = "VT_TX"; + break; + case CallDetails.CALL_TYPE_VT_RX: + str = "VT_RX"; + break; + } + return str; + } + + public static boolean isImsCall(Call call) { + if (call == null) return false; + Preconditions.checkNotNull(call.getCallDetails()); + final int callType = call.getCallDetails().getCallType(); + final boolean isImsVideoCall = isVideoCall(call) || + (callType == CallDetails.CALL_TYPE_VT_NODIR); + final boolean isImsVoiceCall = (callType == CallDetails.CALL_TYPE_VOICE + && call.getCallDetails().getCallDomain() == CallDetails.CALL_DOMAIN_PS); + return isImsVideoCall || isImsVoiceCall; + } + + public static boolean hasImsCall(CallList callList) { + Preconditions.checkNotNull(callList); + return isImsCall(callList.getIncomingCall()) + || isImsCall(callList.getOutgoingCall()) + || isImsCall(callList.getActiveCall()) + || isImsCall(callList.getBackgroundCall()) + || isImsCall(callList.getDisconnectingCall()) + || isImsCall(callList.getDisconnectedCall()); + } + +} diff --git a/src/com/android/incallui/CameraHandler.java b/src/com/android/incallui/CameraHandler.java new file mode 100644 index 00000000..7c0a063e --- /dev/null +++ b/src/com/android/incallui/CameraHandler.java @@ -0,0 +1,350 @@ +/* Copyright (c) 2012, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import java.io.IOException; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.util.Log; +import android.view.Surface; +import android.view.TextureView; +import android.view.WindowManager; + +/** + * The class is used to hold an {@code android.hardware.Camera} instance. + * <p> + * The {@code open()} and {@code release()} calls are similar to the ones in + * {@code android.hardware.Camera}. + */ + +public class CameraHandler { + public static final int CAMERA_UNKNOWN = -1; + private static final String TAG = "VideoCallCameraHandler"; + private static final boolean DBG = true; + private ImsCamera mCameraDevice; + private int mNumberOfCameras; + private int mCameraId = CAMERA_UNKNOWN; // current camera id + private int mBackCameraId = CAMERA_UNKNOWN, mFrontCameraId = CAMERA_UNKNOWN; + private CameraInfo[] mInfo; + private CameraState mCameraState = CameraState.CAMERA_CLOSED; + private Context mContext; + + // Use a singleton. + private static CameraHandler mInstance; + + // Check if device policy has disabled the camera. + private DevicePolicyManager mDpm; + // Get display rotation + WindowManager mWindowManager; + + /** + * Enum that defines the various camera states + */ + public enum CameraState { + CAMERA_CLOSED, // Camera is not yet opened or is closed + PREVIEW_STOPPED, // Camera is open and preview not started + PREVIEW_STARTED, // Preview is active + }; + + /** + * This method returns the single instance of CameraManager object + * @param mContext + */ + public static synchronized CameraHandler getInstance(Context context) { + if (mInstance == null) { + mInstance = new CameraHandler(context); + } + return mInstance; + } + + /** + * Private constructor for CameraManager + * @param mContext + */ + private CameraHandler(Context context) { + mContext = context; + mNumberOfCameras = android.hardware.Camera.getNumberOfCameras(); + log("Number of cameras supported is: " + mNumberOfCameras); + mInfo = new CameraInfo[mNumberOfCameras]; + for (int i = 0; i < mNumberOfCameras; i++) { + mInfo[i] = new CameraInfo(); + android.hardware.Camera.getCameraInfo(i, mInfo[i]); + if (mBackCameraId == CAMERA_UNKNOWN + && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) { + mBackCameraId = i; + log("Back camera ID is: " + mBackCameraId); + } + if (mFrontCameraId == CAMERA_UNKNOWN + && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) { + mFrontCameraId = i; + log("Front camera ID is: " + mFrontCameraId); + } + } + mDpm = (DevicePolicyManager) mContext.getSystemService( + Context.DEVICE_POLICY_SERVICE); + // Get display rotation + mWindowManager = (WindowManager) mContext.getSystemService( + Context.WINDOW_SERVICE); + } + + /** + * Return the number of cameras supported by the device + * + * @return number of cameras + */ + public int getNumberOfCameras() { + return mNumberOfCameras; + } + + /** + * Open the camera hardware + * + * @param cameraId front or the back camera to open + * @return true if the camera was opened successfully + * @throws Exception + */ + public synchronized boolean open(int cameraId) + throws Exception { + if (mDpm == null) { + throw new Exception("DevicePolicyManager not available"); + } + + if (mDpm.getCameraDisabled(null)) { + throw new Exception("Camera is disabled"); + } + + if (mCameraDevice != null && mCameraId != cameraId) { + mCameraDevice.release(); + mCameraDevice = null; + mCameraId = CAMERA_UNKNOWN; + } + if (mCameraDevice == null) { + try { + if (DBG) log("opening camera " + cameraId); + mCameraDevice = ImsCamera.open(cameraId); + mCameraId = cameraId; + } catch (Exception e) { + loge("fail to connect Camera" + e); + throw e; + } + } + mCameraState = CameraState.PREVIEW_STOPPED; + return true; + } + + /** + * Start the camera preview if camera was opened previously + * + * @param mSurfaceTexture Surface on which to draw the camera preview + * @throws IOException + */ + public void startPreview(SurfaceTexture mSurfaceTexture) throws IOException { + if (mCameraState != CameraState.PREVIEW_STOPPED) { + loge("startPreview: Camera state " + mCameraState + + " is not the right camera state for this operation"); + return; + } + if (mCameraDevice != null) { + if (DBG) log("starting preview"); + + // Set the SurfaceTexture to be used for preview + mCameraDevice.setPreviewTexture(mSurfaceTexture); + + setDisplayOrientation(); + mCameraDevice.startPreview(); + mCameraState = CameraState.PREVIEW_STARTED; + } + } + + /** + * Close the camera hardware if the camera was opened previously + */ + public synchronized void close() { + if (mCameraState == CameraState.CAMERA_CLOSED) { + loge("close: Camera state " + mCameraState + + " is not the right camera state for this operation"); + return; + } + + if (mCameraDevice != null) { + if (DBG) log("closing camera"); + if (mCameraState == CameraState.PREVIEW_STARTED) { + mCameraDevice.stopPreview(); + } + mCameraDevice.release(); + } + mCameraDevice = null; + mCameraId = CAMERA_UNKNOWN; + mCameraState = CameraState.CAMERA_CLOSED; + } + + /** + * Stop the camera preview if the camera is open and the preview is not + * already started + */ + public void stopPreview() { + if (mCameraState != CameraState.PREVIEW_STARTED) { + loge("stopPreview: Camera state " + mCameraState + + " is not the right camera state for this operation"); + return; + } + if (mCameraDevice != null) { + if (DBG) log("stopping preview"); + mCameraDevice.stopPreview(); + } + mCameraState = CameraState.PREVIEW_STOPPED; + } + + public void startCameraRecording() { + if (mCameraDevice != null && mCameraState == CameraState.PREVIEW_STARTED) { + mCameraDevice.startRecording(); + } + } + + public void stopCameraRecording() { + if (mCameraDevice != null) { + mCameraDevice.stopRecording(); + } + } + + /** + * Get the camera ID for the back camera + * + * @return camera ID + */ + public int getBackCameraId() { + return mBackCameraId; + } + + /** + * Get the camera ID for the front camera + * + * @return camera ID + */ + public int getFrontCameraId() { + return mFrontCameraId; + } + + /** + * Return the current camera state + * + * @return current state of the camera state machine + */ + public CameraState getCameraState() { + return mCameraState; + } + + /** + * Set the display texture for the camera + * + * @param surfaceTexture + */ + public void setDisplay(SurfaceTexture surfaceTexture) { + // Set the SurfaceTexture to be used for preview + if (mCameraDevice == null) return; + mCameraDevice.setPreviewTexture(surfaceTexture); + + } + + /** + * Set the texture view for the camera + * + * @param textureView + */ + public void setDisplay(TextureView textureView) { + setDisplay(textureView.getSurfaceTexture()); + } + + /** + * Returns the direction of the currently open camera + * + * @return one of the following possible values + * - CameraInfo.CAMERA_FACING_FRONT + * - CameraInfo.CAMERA_FACING_BACK + * - CAMERA_UNKNOWN - No Camera active + */ + public int getCameraDirection() { + if (mCameraDevice == null) return CAMERA_UNKNOWN; + + return (mCameraId == mFrontCameraId) ? CameraInfo.CAMERA_FACING_FRONT + : CameraInfo.CAMERA_FACING_BACK; + } + + /** + * Set the camera display orientation based on the screen rotation + * and the camera direction + */ + public void setDisplayOrientation() { + android.hardware.Camera.CameraInfo info = + new android.hardware.Camera.CameraInfo(); + int result; + int degrees = 0; + int rotation = 0; + + if (mWindowManager == null) { + loge("WindowManager not available"); + return; + } + + rotation = mWindowManager.getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_0: degrees = 0; break; + case Surface.ROTATION_90: degrees = 90; break; + case Surface.ROTATION_180: degrees = 180; break; + case Surface.ROTATION_270: degrees = 270; break; + default: + loge("setDisplayOrientation: Unexpected rotation: " + rotation); + } + + android.hardware.Camera.getCameraInfo(mCameraId, info); + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + mCameraDevice.setDisplayOrientation(result); + } + + public ImsCamera getImsCameraInstance() { + return mCameraDevice; + } + + private void log(String msg) { + Log.d(TAG, msg); + } + + private void loge(String msg) { + Log.e(TAG, msg); + } +} diff --git a/src/com/android/incallui/ConferenceManagerFragment.java b/src/com/android/incallui/ConferenceManagerFragment.java index 75ff1769..119b5f35 100644 --- a/src/com/android/incallui/ConferenceManagerFragment.java +++ b/src/com/android/incallui/ConferenceManagerFragment.java @@ -155,6 +155,17 @@ public class ConferenceManagerFragment }); } + @Override + public void setupEndButtonForRowWithUrl(final int rowId, final String url) { + View endButton = mConferenceCallList[rowId].findViewById(R.id.conferenceCallerDisconnect); + endButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getPresenter().endConferenceConnectionUrl(rowId, url); + } + }); + } + @Override public final void setCanSeparateButtonForRow(final int rowId, boolean canSeparate) { final View separateButton = mConferenceCallList[rowId].findViewById( diff --git a/src/com/android/incallui/ConferenceManagerPresenter.java b/src/com/android/incallui/ConferenceManagerPresenter.java index 1ba88cbd..0535bda1 100644 --- a/src/com/android/incallui/ConferenceManagerPresenter.java +++ b/src/com/android/incallui/ConferenceManagerPresenter.java @@ -22,6 +22,7 @@ import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.InCallPresenter.InCallState; import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.services.telephony.common.Call; +import com.android.services.telephony.common.CallDetails; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSortedSet; @@ -37,7 +38,9 @@ public class ConferenceManagerPresenter private int mNumCallersInConference; private Integer[] mCallerIds; + private String[] mParticipantList; private Context mContext; + private static String LOG_TAG = "ConferenceManagerPresenter"; @Override public void onUiReady(ConferenceManagerUi ui) { @@ -79,10 +82,34 @@ public class ConferenceManagerPresenter update(callList); } - private void update(CallList callList) { - mCallerIds = null; + private boolean isImsCall(Call call) { + return call != null && call.getCallDetails() != null + && call.getCallDetails().getCallDomain() == CallDetails.CALL_DOMAIN_PS; + } + + private void initParticipantList(CallList callList) { + mParticipantList = null; + Call call = callList.getActiveOrBackgroundCall(); + + if (isImsCall(call)) { + String[] confParticipantList = call.getCallDetails().getConfParticipantList(); + // If conference refresh info xml is present use that information + if (confParticipantList != null + && confParticipantList.length > 0) { + mParticipantList = confParticipantList; + mNumCallersInConference = mParticipantList.length; + return; + } + } mCallerIds = callList.getActiveOrBackgroundCall().getChildCallIds().toArray(new Integer[0]); mNumCallersInConference = mCallerIds.length; + } + + private void update(CallList callList) { + mCallerIds = null; + // set mNumCallersInConference and mParticipantList + initParticipantList(callList); + Log.v(this, "Number of calls is " + String.valueOf(mNumCallersInConference)); // Users can split out a call from the conference call if there either the active call @@ -95,10 +122,13 @@ public class ConferenceManagerPresenter for (int i = 0; i < MAX_CALLERS_IN_CONFERENCE; i++) { if (i < mNumCallersInConference) { // Fill in the row in the UI for this caller. - - final ContactCacheEntry contactCache = ContactInfoCache.getInstance(mContext). - getInfo(mCallerIds[i]); - updateManageConferenceRow(i, contactCache, canSeparate); + if (mParticipantList == null) { + final ContactCacheEntry contactCache = ContactInfoCache.getInstance(mContext). + getInfo(mCallerIds[i]); + updateManageConferenceRow(i, contactCache, canSeparate); + } else { + updateManageConferenceRow(i, mParticipantList[i]); + } } else { // Blank out this row in the UI updateManageConferenceRow(i, null, false); @@ -140,6 +170,17 @@ public class ConferenceManagerPresenter } } + public void updateManageConferenceRow(final int i, final String url) { + if (url != null) { + getUi().setRowVisible(i, true); + getUi().setupEndButtonForRowWithUrl(i, url); + getUi().displayCallerInfoForConferenceRow(i, "", url, ""); + } else { + // Disable this row of the Manage conference panel: + getUi().setRowVisible(i, false); + } + } + public void manageConferenceDoneClicked() { getUi().setVisible(false); } @@ -156,6 +197,11 @@ public class ConferenceManagerPresenter CallCommandClient.getInstance().disconnectCall(mCallerIds[rowId]); } + public void endConferenceConnectionUrl(int rowId , String url) { + CallCommandClient.getInstance().hangupWithReason(-1, url, + true, Call.DisconnectCause.NORMAL.ordinal(), ""); + } + public interface ConferenceManagerUi extends Ui { void setVisible(boolean on); boolean isFragmentVisible(); @@ -164,6 +210,8 @@ public class ConferenceManagerPresenter String callerNumberType); void setCanSeparateButtonForRow(int rowId, boolean canSeparate); void setupEndButtonForRow(int rowId); + + void setupEndButtonForRowWithUrl(int rowId, String url); void startConferenceTime(long base); void stopConferenceTime(); } diff --git a/src/com/android/incallui/ContactInfoCache.java b/src/com/android/incallui/ContactInfoCache.java index 448de7fe..4bbf8fad 100644 --- a/src/com/android/incallui/ContactInfoCache.java +++ b/src/com/android/incallui/ContactInfoCache.java @@ -28,6 +28,8 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.lookup.ReverseLookupThread; import com.android.incallui.service.PhoneNumberService; import com.android.incalluibind.ServiceFactory; import com.android.services.telephony.common.Call; @@ -169,12 +171,17 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete sendInfoNotifications(callId, cacheEntry); if (didLocalLookup) { - if (!callerInfo.contactExists && cacheEntry.name == null && - mPhoneNumberService != null) { + if (!callerInfo.contactExists && cacheEntry.name == null) { Log.d(TAG, "Contact lookup. Local contacts miss, checking remote"); - final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId); - mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, - isIncoming); + if (mPhoneNumberService != null) { + final PhoneNumberServiceListener listener = + new PhoneNumberServiceListener(callId); + mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, + isIncoming); + } else { + final ReverseLookupListener listener = new ReverseLookupListener(callId); + ReverseLookupThread.performLookup(mContext, cacheEntry.number, listener); + } } else if (cacheEntry.personUri != null) { Log.d(TAG, "Contact lookup. Local contact found, starting image load"); // Load the image with a callback to update the image state. @@ -195,6 +202,57 @@ public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadComplete } } + public class ReverseLookupListener { + private final int mCallId; + + ReverseLookupListener(int callId) { + mCallId = callId; + } + + public void onLookupComplete(final ContactInfo info) { + if (info == null) { + Log.d(TAG, "Reverse lookup returned no result."); + clearCallbacks(mCallId); + return; + } + + ContactCacheEntry entry = new ContactCacheEntry(); + entry.name = info.name; + entry.number = info.number; + if (info.type == Phone.TYPE_CUSTOM) { + entry.label = info.label; + } else { + final CharSequence typeStr = Phone.getTypeLabel( + mContext.getResources(), info.type, info.label); + entry.label = typeStr == null ? null : typeStr.toString(); + } + + final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); + if (oldEntry != null) { + // Location is only obtained from local lookup so persist + // the value for remote lookups. Once we have a name this + // field is no longer used; it is persisted here in case + // the UI is ever changed to use it. + entry.location = oldEntry.location; + } + + // Add the contact info to the cache. + mInfoMap.put(mCallId, entry); + sendInfoNotifications(mCallId, entry); + + // If there is no image then we should not expect another callback. + if (info.photoUri == null) { + // We're done, so clear callbacks + clearCallbacks(mCallId); + } + } + + public void onImageFetchComplete(Bitmap bitmap) { + onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, + bitmap, (Integer) mCallId); + } + } + class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener { private final int mCallId; diff --git a/src/com/android/incallui/CvoHandler.java b/src/com/android/incallui/CvoHandler.java new file mode 100644 index 00000000..c6968352 --- /dev/null +++ b/src/com/android/incallui/CvoHandler.java @@ -0,0 +1,237 @@ +/* Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import android.content.Context; +import android.hardware.SensorManager; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Registrant; +import android.os.RegistrantList; +import android.util.Log; +import android.view.OrientationEventListener; +import android.view.WindowManager; + +/** + * Provides an interface to handle the CVO - Coordinated Video Orientation part + * of the video telephony call + */ +public class CvoHandler extends Handler { + + private static final String TAG = "VideoCall_CvoHandler"; + private static final boolean DBG = true; + + private static final int ORIENTATION_ANGLE_0 = 0; + private static final int ORIENTATION_ANGLE_90 = 1; + private static final int ORIENTATION_ANGLE_180 = 2; + private static final int ORIENTATION_ANGLE_270 = 3; + private static final int ORIENTATION_MODE_THRESHOLD = 45; + + /** + * Phone orientation angle which can take one of the 4 values + * ORIENTATION_ANGLE_0, ORIENTATION_ANGLE_90, ORIENTATION_ANGLE_180, + * ORIENTATION_ANGLE_270 + */ + private int mCurrentOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + private RegistrantList mCvoRegistrants = new RegistrantList(); + + private Context mContext; + private WindowManager mWindowManager; // Used to get display rotation. + + private OrientationEventListener mOrientationEventListener; + + public CvoHandler(Context context) { + mContext = context; + + mOrientationEventListener = createOrientationListener(); + startOrientationListener(false); + + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + log("CvoHandler created"); + } + + + public interface CvoEventListener { + /** + * This callback method will be invoked when the device orientation changes. + */ + void onDeviceOrientationChanged(int rotation); + } + + /** + * Register for CVO device orientation changed event + */ + public void registerForCvoInfoChange(Handler h, int what, Object obj) { + log("registerForCvoInfoChange handler= " + h + " what= " + what + " obj= " + obj); + Registrant r = new Registrant(h, what, obj); + mCvoRegistrants.add(r); + } + + public void unregisterForCvoInfoChange(Handler h) { + log("unregisterForCvoInfoChange handler= " + h); + mCvoRegistrants.remove(h); + } + + /** + * Enable sensor to listen for device orientation changes + */ + public void startOrientationListener(boolean start) { + log("startOrientationListener " + start); + if (start) { + if (mOrientationEventListener.canDetectOrientation()) { + notifyInitialOrientation(); + mOrientationEventListener.enable(); + } else { + log("Cannot detect orientation"); + } + } else { + mOrientationEventListener.disable(); + mCurrentOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; + } + } + + /* Protected to facilitate unittesting.*/ + protected void doOnOrientationChanged(int angle) { + final int newOrientation = calculateDeviceOrientation(angle); + if (hasDeviceOrientationChanged(newOrientation)) { + notifyCvoClient(newOrientation); + } + } + + /** + * For CVO mode handling, phone is expected to have only 4 orientations The + * orientation sensor gives every degree change angle. This needs to be + * categorized to one of the 4 angles. This method does this calculation. + * @param angle + * @return one of the 4 orientation angles ORIENTATION_ANGLE_0, + * ORIENTATION_ANGLE_90, ORIENTATION_ANGLE_180, + * ORIENTATION_ANGLE_270 + */ + protected static int calculateDeviceOrientation(int angle) { + int newOrientation = ORIENTATION_ANGLE_0; + if ((angle >= 0 + && angle < 0 + ORIENTATION_MODE_THRESHOLD) || + (angle >= 360 - ORIENTATION_MODE_THRESHOLD && + angle < 360)) { + newOrientation = ORIENTATION_ANGLE_0; + } else if (angle >= 90 - ORIENTATION_MODE_THRESHOLD + && angle < 90 + ORIENTATION_MODE_THRESHOLD) { + newOrientation = ORIENTATION_ANGLE_90; + } else if (angle >= 180 - ORIENTATION_MODE_THRESHOLD + && angle < 180 + ORIENTATION_MODE_THRESHOLD) { + newOrientation = ORIENTATION_ANGLE_180; + } else if (angle >= 270 - ORIENTATION_MODE_THRESHOLD + && angle < 270 + ORIENTATION_MODE_THRESHOLD) { + newOrientation = ORIENTATION_ANGLE_270; + } + return newOrientation; + } + + /** + * Detect change in device orientation + */ + private boolean hasDeviceOrientationChanged(int newOrientation) { + if (DBG) { + log("hasDeviceOrientationChanged mCurrentOrientation= " + + mCurrentOrientation + " newOrientation= " + newOrientation); + } + if (newOrientation != mCurrentOrientation) { + mCurrentOrientation = newOrientation; + return true; + } + return false; + } + + /** + * Send newOrientation to client + */ + private void notifyCvoClient(int newOrientation) { + AsyncResult ar = new AsyncResult(null, mCurrentOrientation, null); + mCvoRegistrants.notifyRegistrants(ar); + } + + static public int convertMediaOrientationToActualAngle(int newOrientation) { + int angle = 0; + switch (newOrientation) { + case ORIENTATION_ANGLE_0: + angle = 0; + break; + case ORIENTATION_ANGLE_90: + angle = 90; + break; + case ORIENTATION_ANGLE_180: + angle = 180; + break; + case ORIENTATION_ANGLE_270: + angle = 270; + break; + default: + loge("getAngleFromOrientation: Undefined orientation"); + } + return angle; + } + + private void notifyInitialOrientation() { + final int angle = getCurrentOrientation(); + log("Current orientation is: " + angle); + if ( angle != OrientationEventListener.ORIENTATION_UNKNOWN ) { + doOnOrientationChanged( convertMediaOrientationToActualAngle(angle) ); + } else { + log("Initial orientation is ORIENTATION_UNKNOWN"); + } + } + + private int getCurrentOrientation() { + if (mWindowManager != null) { + return mWindowManager.getDefaultDisplay().getRotation(); + } else { + loge("WindowManager not available."); + return OrientationEventListener.ORIENTATION_UNKNOWN; + } + } + + private OrientationEventListener createOrientationListener() { + return new OrientationEventListener( + mContext, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + doOnOrientationChanged(angle); + } + }; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + private static void loge(String msg) { + Log.e(TAG, msg); + } + +} diff --git a/src/com/android/incallui/GlowPadWrapper.java b/src/com/android/incallui/GlowPadWrapper.java index 28ccb956..074f8971 100644 --- a/src/com/android/incallui/GlowPadWrapper.java +++ b/src/com/android/incallui/GlowPadWrapper.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +27,7 @@ import android.util.AttributeSet; import android.view.View; import com.android.incallui.widget.multiwaveview.GlowPadView; +import com.android.services.telephony.common.CallDetails; /** * @@ -105,13 +110,38 @@ public class GlowPadWrapper extends GlowPadView implements GlowPadView.OnTrigger } } + private int toCallType(int resId) { + int callType = CallDetails.CALL_TYPE_VOICE; + switch (resId) { + case R.drawable.ic_lockscreen_answer_video: + callType = CallDetails.CALL_TYPE_VT; + break; + case R.drawable.ic_lockscreen_answer_tx_video: + callType = CallDetails.CALL_TYPE_VT_TX; + break; + case R.drawable.ic_lockscreen_answer_rx_video: + callType = CallDetails.CALL_TYPE_VT_RX; + break; + case R.drawable.ic_lockscreen_answer: + callType = CallDetails.CALL_TYPE_VOICE; + break; + default: + Log.wtf(this, "Unknown resource id, resId=" + resId); + break; + } + return callType; + } + @Override public void onTrigger(View v, int target) { Log.d(this, "onTrigger()"); final int resId = getResourceIdForTarget(target); switch (resId) { + case R.drawable.ic_lockscreen_answer_video: + case R.drawable.ic_lockscreen_answer_tx_video: + case R.drawable.ic_lockscreen_answer_rx_video: case R.drawable.ic_lockscreen_answer: - mAnswerListener.onAnswer(); + mAnswerListener.onAnswer(toCallType(resId)); mTargetTriggered = true; break; case R.drawable.ic_lockscreen_decline: @@ -143,7 +173,7 @@ public class GlowPadWrapper extends GlowPadView implements GlowPadView.OnTrigger } public interface AnswerListener { - void onAnswer(); + void onAnswer(int callType); void onDecline(); void onText(); } diff --git a/src/com/android/incallui/ImsCamera.java b/src/com/android/incallui/ImsCamera.java new file mode 100644 index 00000000..de8d9ee3 --- /dev/null +++ b/src/com/android/incallui/ImsCamera.java @@ -0,0 +1,180 @@ +/* Copyright (c) 2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import android.graphics.SurfaceTexture; +import android.util.Log; + +/** + * The class is used to hold an {@code android.hardware.Camera} instance. + * <p> + * The {@code open()} and {@code release()} calls are similar to the ones in + * {@code android.hardware.Camera}. + */ + +public class ImsCamera { + private static final String TAG = "VideoCallImsCamera"; + private static final boolean DBG = true; + private static final short IMS_CAMERA_OPERATION_SUCCESS = 0; + + static { + System.loadLibrary("imscamera_jni"); + } + + public static native short native_open(int cameraId); + + public native short native_release(); + + public native short native_startPreview(); + + public native short native_stopPreview(); + + public native short native_startRecording(); + + public native short native_stopRecording(); + + public native short native_setPreviewTexture(SurfaceTexture st); + + public native short native_setDisplayOrientation(int rotation); + + public native boolean native_isZoomSupported(); + + public native int native_getMaxZoom(); + + public native void native_setZoom(int zoomValue); + + public native short native_setPreviewSize(int width, int height); + + public native short native_setPreviewFpsRange(short fps); + + public static ImsCamera open(int cameraId) throws Exception { + Log.d(TAG, "open cameraId=" + cameraId); + short error = native_open(cameraId); + if (error != IMS_CAMERA_OPERATION_SUCCESS) { + Log.e(TAG, "open cameraId=" + cameraId + " failed with error=" + error); + throw new Exception(); + } else { + return new ImsCamera(); + } + } + + public short release() { + if(DBG) log("release"); + short error = native_release(); + logIfError("release", error); + return error; + } + + public short startPreview() { + if (DBG) log("startPreview"); + short error = native_startPreview(); + logIfError("startPreview", error); + return error; + } + + public short stopPreview() { + if(DBG) log("stopPreview"); + short error = native_stopPreview(); + logIfError("stopPreview", error); + return error; + } + + public short startRecording() { + if(DBG) log("startRecording"); + short error = native_startRecording(); + logIfError("startRecording", error); + return error; + } + + public short stopRecording() { + if(DBG) log("stopRecording"); + short error = native_stopRecording(); + logIfError("stopRecording", error); + return error; + } + + public short setPreviewTexture(SurfaceTexture st) { + if(DBG) log("setPreviewTexture"); + short error = native_setPreviewTexture(st); + logIfError("setPreviewTexture", error); + return error; + } + + public short setDisplayOrientation(int rotation) { + if(DBG) log("setDisplayOrientation rotation=" + rotation); + short error = native_setDisplayOrientation(rotation); + logIfError("setDisplayOrientation", error); + return error; + } + + public boolean isZoomSupported() { + boolean result = native_isZoomSupported(); + if(DBG) log("isZoomSupported result=" + result); + return result; + } + + public int getMaxZoom() { + int result = native_getMaxZoom(); + if(DBG) log("getMaxZoom result = " + result); + return result; + } + + public void setZoom(int zoomValue) { + if (DBG) log("setZoom " + zoomValue); + native_setZoom(zoomValue); + } + + public short setPreviewSize(int width, int height) { + if(DBG) log("setPreviewSize"); + short error = native_setPreviewSize(width, height); + logIfError("setPreviewSize", error); + return error; + } + + public short setPreviewFpsRange(short fps) { + if(DBG) log("setPreviewFpsRange"); + short error = native_setPreviewFpsRange(fps); + logIfError("setPreviewFpsRange", error); + return error; + } + + private void log(String msg) { + Log.d(TAG, msg); + } + + private void loge(String msg) { + Log.e(TAG, msg); + } + + private void logIfError(String methodName, short error) { + if (error != IMS_CAMERA_OPERATION_SUCCESS) { + loge(methodName + " failed with error=" + error); + } + } +} diff --git a/src/com/android/incallui/InCallActivity.java b/src/com/android/incallui/InCallActivity.java index c34d8547..8e80c9ff 100644 --- a/src/com/android/incallui/InCallActivity.java +++ b/src/com/android/incallui/InCallActivity.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +20,12 @@ package com.android.incallui; +import java.lang.reflect.Array; +import java.util.ArrayList; + import com.android.services.telephony.common.Call; import com.android.services.telephony.common.Call.State; +import com.android.services.telephony.common.CallDetails; import android.app.Activity; import android.app.AlertDialog; @@ -27,8 +35,11 @@ import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Bundle; +import android.telephony.MSimTelephonyManager; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; @@ -44,13 +55,14 @@ public class InCallActivity extends Activity { private static final int INVALID_RES_ID = -1; - private CallButtonFragment mCallButtonFragment; - private CallCardFragment mCallCardFragment; + protected CallButtonFragment mCallButtonFragment; + protected CallCardFragment mCallCardFragment; private AnswerFragment mAnswerFragment; - private DialpadFragment mDialpadFragment; - private ConferenceManagerFragment mConferenceManagerFragment; + protected DialpadFragment mDialpadFragment; + protected ConferenceManagerFragment mConferenceManagerFragment; private boolean mIsForegroundActivity; - private AlertDialog mDialog; + protected AlertDialog mDialog; + private AlertDialog mModifyCallPromptDialog; /** Use to pass 'showDialpad' from {@link #onNewIntent} to {@link #onResume} */ private boolean mShowDialpadRequested; @@ -61,6 +73,11 @@ public class InCallActivity extends Activity { super.onCreate(icicle); + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA) { + return; + } + // set this flag so this activity will stay in front of the keyguard // Have the WindowManager filter out touch events that are "too fat". int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED @@ -86,6 +103,11 @@ public class InCallActivity extends Activity { Log.d(this, "onStart()..."); super.onStart(); + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA) { + return; + } + // setting activity should be last thing in setup process InCallPresenter.getInstance().setActivity(this); } @@ -140,7 +162,7 @@ public class InCallActivity extends Activity { return mIsForegroundActivity; } - private boolean hasPendingErrorDialog() { + protected boolean hasPendingErrorDialog() { return mDialog != null; } /** @@ -160,6 +182,11 @@ public class InCallActivity extends Activity { */ @Override public void finish() { + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA) { + super.finish(); + return; + } Log.i(this, "finish(). Dialog showing: " + (mDialog != null)); // skip finish if we are still showing a dialog. @@ -193,6 +220,17 @@ public class InCallActivity extends Activity { public void onBackPressed() { Log.d(this, "onBackPressed()..."); + if (mAnswerFragment.isVisible()) { + // The Back key, just like the Home key, is always disabled + // while an incoming call is ringing. (The user *must* either + // answer or reject the call before leaving the incoming-call + // screen.) + Log.d(this, "BACK key while ringing: ignored"); + + // And consume this event; *don't* call super.onBackPressed(). + return; + } + // BACK is also used to exit out of any "special modes" of the // in-call UI: @@ -309,6 +347,14 @@ public class InCallActivity extends Activity { InCallPresenter.getInstance().getProximitySensor().onConfigurationChanged(config); } + @Override + public boolean dispatchTouchEvent(MotionEvent event) { // On touch. + if (InCallPresenter.getInstance().getProximitySensor().isScreenOffByProximity()) + return true; + + return super.dispatchTouchEvent(event); + } + public CallButtonFragment getCallButtonFragment() { return mCallButtonFragment; } @@ -350,7 +396,7 @@ public class InCallActivity extends Activity { } } - private void initializeInCall() { + protected void initializeInCall() { if (mCallButtonFragment == null) { mCallButtonFragment = (CallButtonFragment) getFragmentManager() .findFragmentById(R.id.callButtonFragment); @@ -426,6 +472,133 @@ public class InCallActivity extends Activity { } } + // The function is called when Modify Call button gets pressed. + // The function creates and displays modify call options. + public void displayModifyCallOptions(final int callId) { + final ArrayList<CharSequence> items = new ArrayList<CharSequence>(); + final ArrayList<Integer> itemToCallType = new ArrayList<Integer>(); + final Resources res = getResources(); + + // Prepare the string array and mapping. + items.add(res.getText(R.string.modify_call_option_voice)); + itemToCallType.add(CallDetails.CALL_TYPE_VOICE); + + items.add(res.getText(R.string.modify_call_option_vt_rx)); + itemToCallType.add(CallDetails.CALL_TYPE_VT_RX); + + items.add(res.getText(R.string.modify_call_option_vt_tx)); + itemToCallType.add(CallDetails.CALL_TYPE_VT_TX); + + items.add(res.getText(R.string.modify_call_option_vt)); + itemToCallType.add(CallDetails.CALL_TYPE_VT); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.modify_call_option_title); + final AlertDialog alert; + + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + Toast.makeText(getApplicationContext(), items.get(item), Toast.LENGTH_SHORT).show(); + final int selCallType = itemToCallType.get(item); + log("Videocall: ModifyCall: upgrade/downgrade to " + + CallUtils.fromCallType(selCallType)); + InCallPresenter.getInstance().sendModifyCallRequest(callId, selCallType); + dialog.dismiss(); + } + }; + int currCallType = CallUtils.getCallType(CallList.getInstance().getCall(callId)); + int index = itemToCallType.indexOf(currCallType); + builder.setSingleChoiceItems(items.toArray(new CharSequence[0]), index, listener); + alert = builder.create(); + alert.show(); + } + + public void displayModifyCallConsentDialog(Call call) { + log("VideoCall: displayModifyCallConsentDialog"); + + if (mModifyCallPromptDialog != null) { + log("VideoCall: - DISMISSING mModifyCallPromptDialog."); + mModifyCallPromptDialog.dismiss(); // safe even if already dismissed + mModifyCallPromptDialog = null; + } + + boolean error = CallUtils.hasCallModifyFailed(call); + int callType = CallUtils.getProposedCallType(call); + if (!error) { + String str = getResources().getString(R.string.accept_modify_call_request_prompt); + if (callType == CallDetails.CALL_TYPE_VT) { + str = getResources().getString(R.string.upgrade_vt_prompt); + } else if (callType == CallDetails.CALL_TYPE_VT_TX) { + str = getResources().getString(R.string.upgrade_vt_tx_prompt); + } else if (callType == CallDetails.CALL_TYPE_VT_RX) { + str = getResources().getString(R.string.upgrade_vt_rx_prompt); + } + + final ModifyCallConsentListener onConsentListener = + new ModifyCallConsentListener(call); + mModifyCallPromptDialog = new AlertDialog.Builder(this) + .setMessage(str) + .setPositiveButton(R.string.modify_call_prompt_yes, + onConsentListener) + .setNegativeButton(R.string.modify_call_prompt_no, + onConsentListener) + .setOnDismissListener(onConsentListener) + .create(); + mModifyCallPromptDialog.getWindow().addFlags( + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + mModifyCallPromptDialog.show(); + + } else { + log("VideoCall: Modify Call request failed."); + String errorMsg = getResources().getString(R.string.modify_call_failure_str); + toast(errorMsg); + // We are not explicitly dismissing mModifyCallPromptDialog + // here since it is dismissed at the beginning of this function. + // Note, connection type change will be rejected by + // the Modify Call Consent dialog. + } + } + + private class ModifyCallConsentListener implements DialogInterface.OnClickListener, + DialogInterface.OnDismissListener { + private boolean mClicked = false; + private Call mCall; + + public ModifyCallConsentListener(Call call) { + mCall = call; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + log("VideoCall: ConsentDialog: Clicked on button with ID: " + which); + mClicked = true; + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + InCallPresenter.getInstance().modifyCallConfirm(true, mCall); + break; + case DialogInterface.BUTTON_NEGATIVE: + InCallPresenter.getInstance().modifyCallConfirm(false, mCall); + break; + default: + loge("videocall: No handler for this button, ID:" + which); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (!mClicked) { + log("VideoCall: ConsentDialog: Dismissing the dialog"); + InCallPresenter.getInstance().modifyCallConfirm(false, mCall); + } + } + } + + public void onAvpUpgradeFailure(String errorString) { + Log.e(this,"VideoCall: onAvpUpgradeFailure: errorString: " + errorString); + toast(getResources().getString(R.string.modify_call_failure_str)); + } + public void showPostCharWaitDialog(int callId, String chars) { final PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars); fragment.show(getFragmentManager(), "postCharWait"); @@ -455,7 +628,12 @@ public class InCallActivity extends Activity { mDialog.dismiss(); mDialog = null; } + mAnswerFragment.dismissPendingDialogues(); + if (mModifyCallPromptDialog != null) { + mModifyCallPromptDialog.dismiss(); + mModifyCallPromptDialog = null; + } } /** @@ -498,6 +676,12 @@ public class InCallActivity extends Activity { resId = R.string.callFailed_dsac_restricted_emergency; } else if (cause == Call.DisconnectCause.CS_RESTRICTED_NORMAL) { resId = R.string.callFailed_dsac_restricted_normal; + } else if (cause == Call.DisconnectCause.DIAL_MODIFIED_TO_USSD) { + resId = R.string.callFailed_dialToUssd; + } else if (cause == Call.DisconnectCause.DIAL_MODIFIED_TO_SS) { + resId = R.string.callFailed_dialToSs; + } else if (cause == Call.DisconnectCause.DIAL_MODIFIED_TO_DIAL) { + resId = R.string.callFailed_dialToDial; } return resId; @@ -507,4 +691,16 @@ public class InCallActivity extends Activity { mDialog = null; InCallPresenter.getInstance().onDismissDialog(); } + + private void log(String msg) { + Log.d(this, msg); + } + + private void loge(String msg) { + Log.e(this, msg); + } + + public void updateDsdaTab() { + Log.e(this, "updateDsdaTab : Not supported "); + } } diff --git a/src/com/android/incallui/InCallApp.java b/src/com/android/incallui/InCallApp.java index 7d276bce..ea7b1381 100644 --- a/src/com/android/incallui/InCallApp.java +++ b/src/com/android/incallui/InCallApp.java @@ -34,6 +34,8 @@ public class InCallApp extends Application { */ public static final String ACTION_HANG_UP_ONGOING_CALL = "com.android.incallui.ACTION_HANG_UP_ONGOING_CALL"; + public static final String ADD_CALL_MODE_KEY = "add_call_mode"; + public static final String ADD_PARTICIPANT_KEY = "add_participant"; public InCallApp() { } diff --git a/src/com/android/incallui/InCallPresenter.java b/src/com/android/incallui/InCallPresenter.java index 14026212..ff1b963f 100644 --- a/src/com/android/incallui/InCallPresenter.java +++ b/src/com/android/incallui/InCallPresenter.java @@ -1,4 +1,8 @@ /* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,15 +20,19 @@ package com.android.incallui; +import android.telephony.MSimTelephonyManager; + import com.android.incallui.service.PhoneNumberService; import com.google.android.collect.Sets; import com.google.common.base.Preconditions; import android.content.Context; import android.content.Intent; +import android.content.ActivityNotFoundException; import com.android.services.telephony.common.Call; import com.android.services.telephony.common.Call.Capabilities; +import com.android.services.telephony.common.CallDetails; import com.google.common.collect.Lists; import java.util.ArrayList; @@ -55,6 +63,31 @@ public class InCallPresenter implements CallList.Listener { private InCallState mInCallState = InCallState.NO_CALLS; private ProximitySensor mProximitySensor; private boolean mServiceConnected = false; + private static String LOG_TAG = "InCallPresenter"; + + /** + * This table is for deciding whether consent is + * required while upgrade/downgrade from one calltype + * to other + * Read calltype transition from row to column + * 1 => Consent of user is required + * 0 => No consent required + * eg. from VOLTE to VT-TX, consent is needed so + * row 0, col 1 is set to 1 + * + * User consent is needed for all upgrades and not + * needed for downgrades + * + * VOLTE VT-TX VT-RX VT + * VOLTE | 0 | 1 | 1 | 1 + * VT-TX | 0 | 0 | 1 | 1 + * VT-RX | 0 | 1 | 0 | 1 + * VT | 0 | 0 | 0 | 0 + */ + private int[][] mVideoConsentTable = {{0, 1, 1, 1}, + {0, 0, 1, 1}, + {0, 1, 0, 1}, + {0, 0, 0, 0}}; /** * Is true when the activity has been previously started. Some code needs to know not just if @@ -64,6 +97,8 @@ public class InCallPresenter implements CallList.Listener { */ private boolean mIsActivityPreviouslyStarted = false; + private boolean isImsMediaInitialized = false; + public static synchronized InCallPresenter getInstance() { if (sInCallPresenter == null) { sInCallPresenter = new InCallPresenter(); @@ -111,6 +146,9 @@ public class InCallPresenter implements CallList.Listener { // will kick off an update and the whole process can start. mCallList.addListener(this); + // Initialize VideoCallManager. Instantiates the singleton. + VideoCallManager.getInstance(mContext); + Log.d(this, "Finished InCallPresenter.setUp"); } @@ -132,12 +170,82 @@ public class InCallPresenter implements CallList.Listener { final boolean doFinish = (mInCallActivity != null && isActivityStarted()); Log.i(this, "Hide in call UI: " + doFinish); + if ((mCallList != null) && !(mCallList.existsLiveCall(mCallList.getActiveSubscription())) + && mCallList.switchToOtherActiveSubscription()) { + return; + } + if (doFinish) { mInCallActivity.finish(); } } /** + * Sends modify call request to the other party. + * + * @param callId id of the call to modify. + * @param callType Proposed call type. + */ + public void sendModifyCallRequest(int callId, int callType) { + log("VideoCall: Sending modify call request, callId=" + callId + " callType=" + callType); + Call call = CallList.getInstance().getCall(callId); + if (call != null && call.getCallModifyDetails() != null) { + CallDetails cd = call.getCallModifyDetails(); + cd.setCallType(callType); + CallCommandClient.getInstance().modifyCallInitiate(callId, callType); + } else { + loge("VideoCall: Sending modify call request failed: call=" + call); + } + } + + /** + * Accepts/Rejects modify call request. + * + * @param accept true if the proposed call type is accepted, false otherwise. + * @param call Call which call type change to be confirmed/rejected. + */ + public void modifyCallConfirm(boolean accept, Call call) { + log("VideoCall: ModifyCallConfirm: accept=" + accept + " call=" + call); + CallCommandClient.getInstance().modifyCallConfirm(accept, call.getCallId()); + } + + /** + * Handles modify call request and shows dialog to user for accepting or + * rejecting the modify call + */ + public void onModifyCallRequest(Call call) { + Preconditions.checkNotNull(call); + final int callId = call.getCallId(); + final int currCallType = CallUtils.getCallType(call); + final int proposedCallType = CallUtils.getProposedCallType(call); + final boolean error = CallUtils.hasCallModifyFailed(call); + + log("VideoCall onMoifyCallRequest: CallId =" + callId + " currCallType=" + + currCallType + + " proposedCallType= " + proposedCallType + " error=" + error); + try { + if (isUserConsentRequired(proposedCallType, currCallType)) { + if (mInCallActivity != null) { + mInCallActivity.displayModifyCallConsentDialog(call); + } else { + Log.e(this, "VideoCall: onMoifyCallRequest: InCallActivity is null."); + } + } + } catch (ArrayIndexOutOfBoundsException e) { + Log.e(this, "VideoCall: onModifyCallRequest failed. ", e); + } + } + + public void onAvpUpgradeFailure(String errorString) { + if (mInCallActivity != null) { + mInCallActivity.onAvpUpgradeFailure(errorString); + } else { + Log.e(this, "VideoCall: onAvpUpgradeFailure: InCallActivity is null."); + Log.e(this, "VideoCall: onAvpUpgradeFailure: error=" + errorString); + } + } + + /** * Called when the UI begins or ends. Starts the callstate callbacks if the UI just began. * Attempts to tear down everything if the UI just ended. See #tearDown for more insight on * the tear-down process. @@ -232,6 +340,8 @@ public class InCallPresenter implements CallList.Listener { CallCommandClient.getInstance().setSystemBarNavigationEnabled(true); } + onPhoneStateChange(newState, mInCallState); + // Set the new state before announcing it to the world Log.i(this, "Phone switching state: " + mInCallState + " -> " + newState); mInCallState = newState; @@ -242,6 +352,11 @@ public class InCallPresenter implements CallList.Listener { listener.onStateChange(mInCallState, callList); } + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA && (mInCallActivity != null)) { + mInCallActivity.updateDsdaTab(); + } + if (isActivityStarted()) { final boolean hasCall = callList.getActiveOrBackgroundCall() != null || callList.getOutgoingCall() != null; @@ -258,6 +373,8 @@ public class InCallPresenter implements CallList.Listener { public void onIncomingCall(Call call) { InCallState newState = startOrFinishUi(InCallState.INCOMING); + onPhoneStateChange(newState, mInCallState); + Log.i(this, "Phone switching state: " + mInCallState + " -> " + newState); mInCallState = newState; @@ -269,6 +386,11 @@ public class InCallPresenter implements CallList.Listener { for (IncomingCallListener listener : mIncomingCallListeners) { listener.onIncomingCall(mInCallState, call); } + + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA && (mInCallActivity != null)) { + mInCallActivity.updateDsdaTab(); + } } /** @@ -635,7 +757,14 @@ public class InCallPresenter implements CallList.Listener { // for the call waiting case, we finish() the current activity and start a new one. // There should be no jank from this since the screen is already off and will remain so // until our new activity is up. - if (mProximitySensor.isScreenReallyOff() && isCallWaiting) { + + // In addition to call waiting scenario, we need to force finish() in case of DSDA when + // we get an incoming call on one sub and there is a live call in other sub and screen + // is off. + boolean anyOtherSubActive = (incomingCall != null && mCallList.isAnyOtherSubActive( + mCallList.getActiveSubscription())); + + if (mProximitySensor.isScreenReallyOff() && (isCallWaiting || anyOtherSubActive)) { if (isActivityStarted()) { mInCallActivity.finish(); } @@ -701,7 +830,12 @@ public class InCallPresenter implements CallList.Listener { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_NO_USER_ACTION); - intent.setClass(mContext, InCallActivity.class); + if (MSimTelephonyManager.getDefault().getMultiSimConfiguration() + == MSimTelephonyManager.MultiSimVariants.DSDA) { + intent.setClass(mContext, MSimInCallActivity.class); + } else { + intent.setClass(mContext, InCallActivity.class); + } if (showDialpad) { intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true); } @@ -709,6 +843,26 @@ public class InCallPresenter implements CallList.Listener { return intent; } + public void sendAddParticipantIntent() { + Intent intent = new Intent(Intent.ACTION_DIAL); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // when we request the dialer come up, we also want to inform + // it that we're going through the "add participant" option from the + // InCallScreen. + intent.putExtra(InCallApp.ADD_CALL_MODE_KEY, true); + intent.putExtra(InCallApp.ADD_PARTICIPANT_KEY, true); + try { + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + // This is rather rare but possible. + // Note: this method is used even when the phone is encrypted. At + // that moment + // the system may not find any Activity which can accept this Intent + Log.e(LOG_TAG, "Activity for adding calls isn't found."); + } + } + /** * Private constructor. Must use getInstance() to get this singleton. */ @@ -753,4 +907,36 @@ public class InCallPresenter implements CallList.Listener { public interface IncomingCallListener { public void onIncomingCall(InCallState state, Call call); } + + private void onPhoneStateChange(InCallState newState, InCallState oldState) { + if ( newState != oldState) { + initMediaHandler(newState); + } + } + + private void initMediaHandler(InCallState newState) { + boolean hasImsCall = CallUtils.hasImsCall(CallList.getInstance()); + Log.i(this, "initMediaHandler: hasImsCall: " + hasImsCall + " isImsMediaInitialized: " + + isImsMediaInitialized); + + if (hasImsCall && !isImsMediaInitialized) { + isImsMediaInitialized = true; + VideoCallManager.getInstance(mContext).onMediaRequest(isImsMediaInitialized); + } else if (isImsMediaInitialized && !hasImsCall) { + isImsMediaInitialized = false; + VideoCallManager.getInstance(mContext).onMediaRequest(isImsMediaInitialized); + } + } + + private boolean isUserConsentRequired(int callType, int prevCallType) { + return mVideoConsentTable[prevCallType][callType] == 1; + } + + private void log(String msg) { + Log.d(this, msg); + } + + private void loge(String msg) { + Log.e(this, msg); + } } diff --git a/src/com/android/incallui/Log.java b/src/com/android/incallui/Log.java index c859e5c6..8d3e0043 100644 --- a/src/com/android/incallui/Log.java +++ b/src/com/android/incallui/Log.java @@ -24,9 +24,8 @@ public class Log { // Generic tag for all In Call logging private static final String TAG = "InCall"; - public static final boolean DEBUG = android.util.Log.isLoggable(TAG, android.util.Log.DEBUG); - public static final boolean VERBOSE = android.util.Log.isLoggable(TAG, - android.util.Log.VERBOSE); + public static final boolean DEBUG = true; + public static final boolean VERBOSE = true; public static final String TAG_DELIMETER = " - "; public static void d(String tag, String msg) { diff --git a/src/com/android/incallui/MSimAnswerFragment.java b/src/com/android/incallui/MSimAnswerFragment.java new file mode 100644 index 00000000..92ff5c2c --- /dev/null +++ b/src/com/android/incallui/MSimAnswerFragment.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2013 The Linux Foundation. All rights reserved. + * Not a Contribution. + * + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; + +import com.google.common.base.Preconditions; + +import java.util.ArrayList; + +/** + * + */ +public class MSimAnswerFragment extends BaseFragment<MSimAnswerPresenter, + MSimAnswerPresenter.AnswerUi> + implements GlowPadWrapper.AnswerListener, MSimAnswerPresenter.AnswerUi { + + /** + * The popup showing the list of canned responses. + * + * This is an AlertDialog containing a ListView showing the possible choices. This may be null + * if the InCallScreen hasn't ever called showRespondViaSmsPopup() yet, or if the popup was + * visible once but then got dismissed. + */ + private Dialog mCannedResponsePopup = null; + + /** + * The popup showing a text field for users to type in their custom message. + */ + private AlertDialog mCustomMessagePopup = null; + + private ArrayAdapter<String> mTextResponsesAdapter = null; + + private GlowPadWrapper mGlowpad; + + public MSimAnswerFragment() { + } + + @Override + public MSimAnswerPresenter createPresenter() { + return new MSimAnswerPresenter(); + } + + @Override + MSimAnswerPresenter.AnswerUi getUi() { + return this; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mGlowpad = (GlowPadWrapper) inflater.inflate(R.layout.answer_fragment, + container, false); + + Log.d(this, "Creating view for answer fragment ", this); + Log.d(this, "Created from activity", getActivity()); + mGlowpad.setAnswerListener(this); + + return mGlowpad; + } + + @Override + public void onDestroyView() { + Log.d(this, "onDestroyView"); + if (mGlowpad != null) { + mGlowpad.stopPing(); + mGlowpad = null; + } + super.onDestroyView(); + } + + @Override + public void showAnswerUi(boolean show) { + getView().setVisibility(show ? View.VISIBLE : View.GONE); + + Log.d(this, "Show answer UI: " + show); + if (show) { + mGlowpad.startPing(); + } else { + mGlowpad.stopPing(); + } + } + + @Override + public void showTextButton(boolean show) { + final int targetResourceId = show + ? R.array.incoming_call_widget_3way_targets + : R.array.incoming_call_widget_2way_targets; + + if (targetResourceId != mGlowpad.getTargetResourceId()) { + if (show) { + // Answer, Decline, and Respond via SMS. + mGlowpad.setTargetResources(targetResourceId); + mGlowpad.setTargetDescriptionsResourceId( + R.array.incoming_call_widget_3way_target_descriptions); + mGlowpad.setDirectionDescriptionsResourceId( + R.array.incoming_call_widget_3way_direction_descriptions); + } else { + // Answer or Decline. + mGlowpad.setTargetResources(targetResourceId); + mGlowpad.setTargetDescriptionsResourceId( + R.array.incoming_call_widget_2way_target_descriptions); + mGlowpad.setDirectionDescriptionsResourceId( + R.array.incoming_call_widget_2way_direction_descriptions); + } + + mGlowpad.reset(false); + } + } + + @Override + public void showMessageDialog() { + final ListView lv = new ListView(getActivity()); + + Preconditions.checkNotNull(mTextResponsesAdapter); + lv.setAdapter(mTextResponsesAdapter); + lv.setOnItemClickListener(new RespondViaSmsItemClickListener()); + + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setCancelable( + true).setView(lv); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + if (mGlowpad != null) { + mGlowpad.startPing(); + } + } + }); + mCannedResponsePopup = builder.create(); + mCannedResponsePopup.show(); + } + + private boolean isCannedResponsePopupShowing() { + if (mCannedResponsePopup != null) { + return mCannedResponsePopup.isShowing(); + } + return false; + } + + private boolean isCustomMessagePopupShowing() { + if (mCustomMessagePopup != null) { + return mCustomMessagePopup.isShowing(); + } + return false; + } + + /** + * Dismiss the canned response list popup. + * + * This is safe to call even if the popup is already dismissed, and even if you never called + * showRespondViaSmsPopup() in the first place. + */ + private void dismissCannedResponsePopup() { + if (mCannedResponsePopup != null) { + mCannedResponsePopup.dismiss(); // safe even if already dismissed + mCannedResponsePopup = null; + } + } + + /** + * Dismiss the custom compose message popup. + */ + private void dismissCustomMessagePopup() { + if (mCustomMessagePopup != null) { + mCustomMessagePopup.dismiss(); + mCustomMessagePopup = null; + } + } + + public void dismissPendingDialogues() { + if (isCannedResponsePopupShowing()) { + dismissCannedResponsePopup(); + } + + if (isCustomMessagePopupShowing()) { + dismissCustomMessagePopup(); + } + } + + public boolean hasPendingDialogs() { + return !(mCannedResponsePopup == null && mCustomMessagePopup == null); + } + + /** + * Shows the custom message entry dialog. + */ + public void showCustomMessageDialog() { + // Create an alert dialog containing an EditText + final EditText et = new EditText(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setCancelable( + true).setView(et) + .setPositiveButton(R.string.custom_message_send, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The order is arranged in a way that the popup will be destroyed when the + // InCallActivity is about to finish. + final String textMessage = et.getText().toString().trim(); + dismissCustomMessagePopup(); + getPresenter().rejectCallWithMessage(textMessage); + } + }) + .setNegativeButton(R.string.custom_message_cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismissCustomMessagePopup(); + getPresenter().onDismissDialog(); + } + }) + .setTitle(R.string.respond_via_sms_custom_message); + mCustomMessagePopup = builder.create(); + + // Enable/disable the send button based on whether there is a message in the EditText + et.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + final Button sendButton = mCustomMessagePopup.getButton( + DialogInterface.BUTTON_POSITIVE); + sendButton.setEnabled(s != null && s.toString().trim().length() != 0); + } + }); + + // Keyboard up, show the dialog + mCustomMessagePopup.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + mCustomMessagePopup.show(); + + // Send button starts out disabled + final Button sendButton = mCustomMessagePopup.getButton(DialogInterface.BUTTON_POSITIVE); + sendButton.setEnabled(false); + } + + @Override + public void configureMessageDialog(ArrayList<String> textResponses) { + final ArrayList<String> textResponsesForDisplay = new ArrayList<String>(textResponses); + + textResponsesForDisplay.add(getResources().getString( + R.string.respond_via_sms_custom_message)); + mTextResponsesAdapter = new ArrayAdapter<String>(getActivity(), + android.R.layout.simple_list_item_1, android.R.id.text1, textResponsesForDisplay); + } + + @Override + public void onAnswer(int callType) { + getPresenter().onAnswer(callType); + } + + @Override + public void onDecline() { + getPresenter().onDecline(); + } + + @Override + public void onText() { + getPresenter().onText(); + } + + /** + * OnItemClickListener for the "Respond via SMS" popup. + */ + public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { + + /** + * Handles the user selecting an item from the popup. + */ + @Override + public void onItemClick(AdapterView<?> parent, // The ListView + View view, // The TextView that was clicked + int position, long id) { + Log.d(this, "RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); + final String message = (String) parent.getItemAtPosition(position); + Log.v(this, "- message: '" + message + "'"); + dismissCannedResponsePopup(); + + // The "Custom" choice is a special case. + // (For now, it's guaranteed to be the last item.) + if (position == (parent.getCount() - 1)) { + // Show the custom message dialog + showCustomMessageDialog(); + } else { + getPresenter().rejectCallWithMessage(message); + } + } + } +} diff --git a/src/com/android/incallui/MSimAnswerPresenter.java b/src/com/android/incallui/MSimAnswerPresenter.java new file mode 100644 index 00000000..375b05de --- /dev/null +++ b/src/com/android/incallui/MSimAnswerPresenter.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2013 The Linux Foundation. All rights reserved. + * Not a Contribution. + * + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui; + +import android.telephony.MSimTelephonyManager; +import com.android.services.telephony.common.Call; + +import java.util.ArrayList; + +/** + * Presenter for the Incoming call widget. + */ +public class MSimAnswerPresenter extends Presenter<MSimAnswerPresenter.AnswerUi> + implements CallList.CallUpdateListener, CallList.Listener, + CallList.ActiveSubChangeListener { + + private static final String TAG = MSimAnswerPresenter.class.getSimpleName(); + + private int mCallId[] = {Call.INVALID_CALL_ID, Call.INVALID_CALL_ID}; + private Call mCall[] = {null, null}; + + @Override + public void onUiReady(AnswerUi ui) { + super.onUiReady(ui); + + final CallList calls = CallList.getInstance(); + final Call call = calls.getIncomingCall(); + // TODO: change so that answer presenter never starts up if it's not incoming. + if (call != null) { + processIncomingCall(call); + } + + // Listen for incoming calls. + calls.addListener(this); + CallList.getInstance().addActiveSubChangeListener(this); + } + + @Override + public void onUiUnready(AnswerUi ui) { + super.onUiUnready(ui); + + int subscription = CallList.getInstance().getActiveSubscription(); + CallList.getInstance().removeListener(this); + + // This is necessary because the activity can be destroyed while an incoming call exists. + // This happens when back button is pressed while incoming call is still being shown. + if (mCallId[subscription] != Call.INVALID_CALL_ID) { + CallList.getInstance().removeCallUpdateListener(mCallId[subscription], this); + } + CallList.getInstance().removeActiveSubChangeListener(this); + } + + @Override + public void onCallListChange(CallList callList) { + // no-op + } + + @Override + public void onDisconnect(Call call) { + // no-op + } + + @Override + public void onIncomingCall(Call call) { + int subscription = call.getSubscription(); + // TODO: Ui is being destroyed when the fragment detaches. Need clean up step to stop + // getting updates here. + Log.d(this, "onIncomingCall: " + this); + if (getUi() != null) { + if (call.getCallId() != mCallId[subscription]) { + // A new call is coming in. + processIncomingCall(call); + } + } + } + + private void processIncomingCall(Call call) { + int subscription = call.getSubscription(); + mCallId[subscription] = call.getCallId(); + mCall[subscription] = call; + + // Listen for call updates for the current call. + CallList.getInstance().addCallUpdateListener(mCallId[subscription], this); + + Log.d(TAG, "Showing incoming for call id: " + mCallId[subscription] + " " + this); + final ArrayList<String> textMsgs = CallList.getInstance().getTextResponses( + call.getCallId()); + getUi().showAnswerUi(true); + + if (call.can(Call.Capabilities.RESPOND_VIA_TEXT) && textMsgs != null) { + getUi().showTextButton(true); + getUi().configureMessageDialog(textMsgs); + } else { + getUi().showTextButton(false); + } + } + + + @Override + public void onCallStateChanged(Call call) { + Log.d(this, "onCallStateChange() " + call + " " + this); + if (call.getState() != Call.State.INCOMING && call.getState() != Call.State.CALL_WAITING) { + int subscription = call.getSubscription(); + // Stop listening for updates. + CallList.getInstance().removeCallUpdateListener(mCallId[subscription], this); + + getUi().showAnswerUi(false); + + // mCallId will hold the state of the call. We don't clear the mCall variable here as + // it may be useful for sending text messages after phone disconnects. + mCallId[subscription] = Call.INVALID_CALL_ID; + } + } + + public void onAnswer(int callType) { + int subscription = CallList.getInstance().getActiveSubscription(); + if (mCallId[subscription] == Call.INVALID_CALL_ID) { + return; + } + + Log.d(this, "onAnswer " + mCallId[subscription]); + + CallCommandClient.getInstance().answerCall(mCallId[subscription]); + } + + public void onDecline() { + int subscription = CallList.getInstance().getActiveSubscription(); + Log.d(this, "onDecline " + mCallId[subscription]); + + CallCommandClient.getInstance().rejectCall(mCall[subscription], false, null); + } + + public void onText() { + if (getUi() != null) { + getUi().showMessageDialog(); + } + } + + public void rejectCallWithMessage(String message) { + int subscription = CallList.getInstance().getActiveSubscription(); + Log.d(this, "sendTextToDefaultActivity()..."); + + CallCommandClient.getInstance().rejectCall(mCall[subscription], true, message); + + onDismissDialog(); + } + + public void onDismissDialog() { + InCallPresenter.getInstance().onDismissDialog(); + } + + interface AnswerUi extends Ui { + public void showAnswerUi(boolean show); + public void showTextButton(boolean show); + public void showMessageDialog(); + public void configureMessageDialog(ArrayList<String> textResponses); + } + + @Override + public void onActiveSubChanged(int subscription) { + final CallList calls = CallList.getInstance(); + final Call call = calls.getIncomingCall(); + + if ((call != null) && (call.getCallId() == mCallId[subscription])) { + Log.i(TAG, "Show incoming for call id: " + mCallId[subscription] + " " + this); + final ArrayList<String> textMsgs = CallList.getInstance().getTextResponses( + call.getCallId()); + getUi().showAnswerUi(true); + + if (call.can(Call.Capabilities.RESPOND_VIA_TEXT) && textMsgs != null) { + getUi().showTextButton(true); + getUi().configureMessageDialog(textMsgs); + } else { + getUi().showTextButton(false); + } + } else if ((call == null) && (calls.existsLiveCall(subscription))) { + Log.i(TAG, "Hide incoming for call id: " + mCallId[subscription] + " " + this); + getUi().showAnswerUi(false); + } + } +} diff --git a/src/com/android/incallui/MSimInCallActivity.java b/src/com/android/incallui/MSimInCallActivity.java new file mode 100644 index 00000000..3f61a595 --- /dev/null +++ b/src/com/android/incallui/MSimInCallActivity.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2013 The Linux Foundation. All rights reserved. + * Not a Contribution. + * + * Copyright (C) 2006 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.ActionBar; +import android.app.FragmentTransaction; +import android.app.ActionBar.Tab; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.telephony.MSimTelephonyManager; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Phone app "multisim in call" screen. + */ +public class MSimInCallActivity extends InCallActivity { + + private MSimAnswerFragment mAnswerFragment; + + private final int TAB_COUNT_ONE = 1; + private final int TAB_COUNT_TWO = 2; + private final int TAB_POSITION_FIRST = 0; + + private Tab[] mDsdaTab = new Tab[TAB_COUNT_TWO]; + private boolean[] mDsdaTabAdd = {false, false}; + + @Override + protected void onCreate(Bundle icicle) { + Log.d(this, "onCreate()... this = " + this); + + super.onCreate(icicle); + + // set this flag so this activity will stay in front of the keyguard + // Have the WindowManager filter out touch events that are "too fat". + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); + + requestWindowFeature(Window.FEATURE_ACTION_BAR); + + getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + getActionBar().setDisplayShowTitleEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + + // Inflate everything in incall_screen.xml and add it to the screen. + setContentView(R.layout.incall_screen_msim); + + initializeInCall(); + + initializeDsdaSwitchTab(); + Log.d(this, "onCreate(): exit"); + } + + @Override + protected void onStart() { + Log.d(this, "onStart()..."); + super.onStart(); + + // setting activity should be last thing in setup process + InCallPresenter.getInstance().setActivity(this); + } + + @Override + public void finish() { + Log.i(this, "finish(). Dialog showing: " + (mDialog != null)); + + // skip finish if we are still showing a dialog. + if (!hasPendingErrorDialog() && !mAnswerFragment.hasPendingDialogs()) { + super.finish(); + } + } + + @Override + protected void initializeInCall() { + if (mCallButtonFragment == null) { + mCallButtonFragment = (CallButtonFragment) getFragmentManager() + .findFragmentById(R.id.callButtonFragment); + mCallButtonFragment.getView().setVisibility(View.INVISIBLE); + } + + if (mCallCardFragment == null) { + mCallCardFragment = (CallCardFragment) getFragmentManager() + .findFragmentById(R.id.callCardFragment); + } + + if (mAnswerFragment == null) { + mAnswerFragment = (MSimAnswerFragment) getFragmentManager() + .findFragmentById(R.id.answerFragment); + } + + if (mDialpadFragment == null) { + mDialpadFragment = (DialpadFragment) getFragmentManager() + .findFragmentById(R.id.dialpadFragment); + mDialpadFragment.getView().setVisibility(View.INVISIBLE); + } + + if (mConferenceManagerFragment == null) { + mConferenceManagerFragment = (ConferenceManagerFragment) getFragmentManager() + .findFragmentById(R.id.conferenceManagerFragment); + mConferenceManagerFragment.getView().setVisibility(View.INVISIBLE); + } + } + + @Override + public void dismissPendingDialogs() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + mAnswerFragment.dismissPendingDialogues(); + } + + private void initializeDsdaSwitchTab() { + int phoneCount = MSimTelephonyManager.getDefault().getPhoneCount(); + ActionBar bar = getActionBar(); + View[] mDsdaTabLayout = new View[phoneCount]; + TypedArray icons = getResources().obtainTypedArray(R.array.sim_icons); + int[] subString = {R.string.sub_1, R.string.sub_2}; + + for (int i = 0; i < phoneCount; i++) { + mDsdaTabLayout[i] = getLayoutInflater() + .inflate(R.layout.msim_tab_sub_info, null); + + ((ImageView)mDsdaTabLayout[i].findViewById(R.id.tabSubIcon)) + .setBackground(icons.getDrawable(i)); + + ((TextView)mDsdaTabLayout[i].findViewById(R.id.tabSubText)) + .setText(subString[i]); + + mDsdaTab[i] = bar.newTab().setCustomView(mDsdaTabLayout[i]) + .setTabListener(new TabListener(i)); + } + } + + @Override + public void updateDsdaTab() { + int phoneCount = MSimTelephonyManager.getDefault().getPhoneCount(); + ActionBar bar = getActionBar(); + + for (int i = 0; i < phoneCount; i++) { + if (CallList.getInstance().existsLiveCall(i)) { + if (!mDsdaTabAdd[i]) { + addDsdaTab(i); + } + } else { + removeDsdaTab(i); + } + } + + updateDsdaTabSelection(); + } + + private void addDsdaTab(int subscription) { + ActionBar bar = getActionBar(); + int tabCount = bar.getTabCount(); + + if (tabCount < subscription) { + bar.addTab(mDsdaTab[subscription], false); + } else { + bar.addTab(mDsdaTab[subscription], subscription, false); + } + mDsdaTabAdd[subscription] = true; + } + + private void removeDsdaTab(int subscription) { + ActionBar bar = getActionBar(); + int tabCount = bar.getTabCount(); + + for (int i = 0; i < tabCount; i++) { + if (bar.getTabAt(i).equals(mDsdaTab[subscription])) { + bar.removeTab(mDsdaTab[subscription]); + mDsdaTabAdd[subscription] = false; + return; + } + } + } + + private void updateDsdaTabSelection() { + ActionBar bar = getActionBar(); + int barCount = bar.getTabCount(); + + if (barCount == TAB_COUNT_ONE) { + bar.selectTab(bar.getTabAt(TAB_POSITION_FIRST)); + } else if (barCount == TAB_COUNT_TWO) { + bar.selectTab(bar.getTabAt(CallList.getInstance().getActiveSubscription())); + } + } + + private class TabListener implements ActionBar.TabListener { + int mSubscription; + + public TabListener(int subId) { + mSubscription = subId; + } + + public void onTabSelected(Tab tab, FragmentTransaction ft) { + ActionBar bar = getActionBar(); + + if (CallList.getInstance().existsLiveCall(mSubscription)) { + CallCommandClient.getInstance().setActiveSubscription(mSubscription); + } + } + + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } + } +} diff --git a/src/com/android/incallui/MediaHandler.java b/src/com/android/incallui/MediaHandler.java new file mode 100644 index 00000000..95acd7b9 --- /dev/null +++ b/src/com/android/incallui/MediaHandler.java @@ -0,0 +1,361 @@ +/* Copyright (c) 2012, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import android.content.pm.ActivityInfo; +import android.graphics.SurfaceTexture; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.util.Log; + +/** + * Provides an interface to handle the media part of the video telephony call + */ +public class MediaHandler extends Handler { + + //Use QVGA as default resolution + private static final int DEFAULT_WIDTH = 240; + private static final int DEFAULT_HEIGHT = 320; + public static final int DPL_INIT_SUCCESSFUL = 0; + public static final int DPL_INIT_FAILURE = -1; + public static final int DPL_INIT_MULTIPLE = -2; + + private static final String TAG = "VideoCall_MediaHandler"; + + private static SurfaceTexture mSurface; + + private static boolean mInitCalledFlag = false; + + private static native int nativeInit(); + private static native void nativeDeInit(); + private static native void nativeHandleRawFrame(byte[] frame); + private static native int nativeSetSurface(SurfaceTexture st); + private static native void nativeSetDeviceOrientation(int orientation); + private static native short nativeGetNegotiatedFPS(); + private static native int nativeGetNegotiatedHeight(); + private static native int nativeGetNegotiatedWidth(); + private static native int nativeGetUIOrientationMode(); + private static native int nativeGetPeerHeight(); + private static native int nativeGetPeerWidth(); + private static native void nativeRegisterForMediaEvents(MediaHandler instance); + + public static final int MEDIA_EVENT = 0; + + //Following values are from the IMS VT API documentation + public static final int PARAM_READY_EVT = 1; + public static final int START_READY_EVT = 2; + public static final int DISPLAY_MODE_EVT = 5; + public static final int PEER_RESOLUTION_CHANGE_EVT = 6; + + protected final RegistrantList mDisplayModeEventRegistrants + = new RegistrantList(); + + // UI Orientation Modes + private static final int LANDSCAPE_MODE = 1; + private static final int PORTRAIT_MODE = 2; + private static final int CVO_MODE = 3; + /* + * Initializing default negotiated parameters to a working set of valuesso + * that the application does not crash in case we do not get the Param ready + * event + */ + private static int mNegotiatedHeight = 240; + private static int mNegotiatedWidth = 320; + private static int mUIOrientationMode = PORTRAIT_MODE; + private static short mNegotiatedFps = 20; + + private int mPeerHeight = DEFAULT_HEIGHT; + private int mPeerWidth = DEFAULT_WIDTH; + private IMediaEventListener mMediaEventListener; + public RegistrantList mCvoModeOnRegistrant = new RegistrantList(); + + // Use a singleton + private static MediaHandler mInstance; + + /** + * This method returns the single instance of MediaHandler object * + */ + public static synchronized MediaHandler getInstance() { + if (mInstance == null) { + mInstance = new MediaHandler(); + } + return mInstance; + } + + /** + * Private constructor for MediaHandler + */ + private MediaHandler() { + } + + public interface IMediaEventListener { + void onParamReadyEvent(); + void onDisplayModeEvent(); + void onStartReadyEvent(); + void onPeerResolutionChangeEvent(); + } + + static { + System.loadLibrary("vt_jni"); + } + + /* + * Initialize Media + * @return + DPL_INIT_SUCCESSFUL 0 initialization is successful. + DPL_INIT_FAILURE -1 error in initialization of QMI or other components. + DPL_INIT_MULTIPLE -2 trying to initialize an already initialized library. + */ + public int init() { + if (!mInitCalledFlag) { + int error = nativeInit(); + Log.d(TAG, "init called error = " + error); + switch (error) { + case DPL_INIT_SUCCESSFUL: + mInitCalledFlag = true; + registerForMediaEvents(this); + break; + case DPL_INIT_FAILURE: + mInitCalledFlag = false; + break; + case DPL_INIT_MULTIPLE: + mInitCalledFlag = true; + Log.e(TAG, "Dpl init is called multiple times"); + error = DPL_INIT_SUCCESSFUL; + break; + } + return error; + } + + // Dpl is already initialized. So return success + return DPL_INIT_SUCCESSFUL; + } + + /* + * Deinitialize Media + */ + public static void deInit() { + Log.d(TAG, "deInit called"); + nativeDeInit(); + mInitCalledFlag = false; + } + + public void sendCvoInfo(int orientation) { + Log.d(TAG, "sendCvoInfo orientation=" + orientation); + nativeSetDeviceOrientation(orientation); + } + + /** + * Send the camera preview frames to the media module to be sent to the far + * end party + * @param frame raw frames from the camera + */ + public static void sendPreviewFrame(byte[] frame) { + nativeHandleRawFrame(frame); + } + + /** + * Send the SurfaceTexture to media module + * @param st + */ + public static void setSurface(SurfaceTexture st) { + Log.d(TAG, "setSurface(SurfaceTexture: " + st + ")"); + mSurface = st; + nativeSetSurface(st); + } + + /** + * Send the SurfaceTexture to media module. This should be called only for + * re-sending an already created surface + */ + public static void setSurface() { + Log.d(TAG, "setSurface()"); + if (mSurface == null) { + Log.e(TAG, "sSurface is null. So not passing it down"); + return; + } + nativeSetSurface(mSurface); + } + + /** + * Get Negotiated Height + */ + public synchronized static int getNegotiatedHeight() { + Log.d(TAG, "Negotiated Height = " + mNegotiatedHeight); + return mNegotiatedHeight; + } + + /** + * Get Negotiated Width + */ + public synchronized static int getNegotiatedWidth() { + Log.d(TAG, "Negotiated Width = " + mNegotiatedWidth); + return mNegotiatedWidth; + } + + /** + * Get Negotiated Width + */ + public int getUIOrientationMode() { + Log.d(TAG, "UI Orientation Mode = " + mUIOrientationMode); + return mUIOrientationMode; + } + + public synchronized static short getNegotiatedFps() { + return mNegotiatedFps; + } + + /** + * Get Peer Height + */ + public int getPeerHeight() { + Log.d(TAG, "Peer Height = " + mPeerHeight); + return mPeerHeight; + } + + /** + * Get Peer Width + */ + public int getPeerWidth() { + Log.d(TAG, "Peer Width = " + mPeerWidth); + return mPeerWidth; + } + + /** + * Register for event that will invoke + * {@link MediaHandler#onMediaEvent(int)} + */ + private static void registerForMediaEvents(MediaHandler instance) { + Log.d(TAG, "Registering for Media Callback Events"); + nativeRegisterForMediaEvents(instance); + } + + public void setMediaEventListener(IMediaEventListener listener) { + mMediaEventListener = listener; + } + + private void doOnMediaEvent(int eventId) { + switch (eventId) { + case PARAM_READY_EVT: + Log.d(TAG, "Received PARAM_READY_EVT. Updating negotiated values"); + if (updatePreviewParams() && mMediaEventListener != null) { + mMediaEventListener.onParamReadyEvent(); + } + break; + case PEER_RESOLUTION_CHANGE_EVT: + mPeerHeight = nativeGetPeerHeight(); + mPeerWidth = nativeGetPeerWidth(); + Log.d(TAG, "Received PEER_RESOLUTION_CHANGE_EVENT. Updating peer values" + + " mPeerHeight=" + mPeerHeight + " mPeerWidth=" + mPeerWidth); + if (mMediaEventListener != null) { + mMediaEventListener.onPeerResolutionChangeEvent(); + } + break; + case START_READY_EVT: + Log.d(TAG, "Received START_READY_EVT. Camera frames can be sent now"); + if (mMediaEventListener != null) { + mMediaEventListener.onStartReadyEvent(); + } + break; + case DISPLAY_MODE_EVT: + mUIOrientationMode = nativeGetUIOrientationMode(); + processUIOrientationMode(); + if (mMediaEventListener != null) { + mMediaEventListener.onDisplayModeEvent(); + } + break; + default: + Log.e(TAG, "Received unknown event id=" + eventId); + } + } + + /** + * Callback method that is invoked when Media events occur + */ + public void onMediaEvent(int eventId) { + Log.d(TAG, "onMediaEvent eventId = " + eventId); + final Message msg = obtainMessage(MEDIA_EVENT, eventId, 0); + sendMessage(msg); + } + + public void handleMessage(Message msg) { + switch (msg.what) { + case MEDIA_EVENT: + doOnMediaEvent(msg.arg1); + break; + default: + Log.e(TAG, "Received unknown msg id = " + msg.what); + } + } + + private synchronized boolean updatePreviewParams() { + int h = nativeGetNegotiatedHeight(); + int w = nativeGetNegotiatedWidth(); + short fps = nativeGetNegotiatedFPS(); + if (mNegotiatedHeight != h + || mNegotiatedWidth != w + || mNegotiatedFps != fps) { + mNegotiatedHeight = h; + mNegotiatedWidth = w; + mNegotiatedFps = fps; + return true; + } + return false; + } + + private void processUIOrientationMode() { + mCvoModeOnRegistrant.notifyRegistrants(new AsyncResult(null, + isCvoModeEnabled(), null)); + } + + /** + * Register for mode change notification from IMS media library to determine + * if CVO mode needs to be activated or deactivated + */ + public void registerForCvoModeRequestChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mCvoModeOnRegistrant.add(r); + } + + /** + * TODO Call all unregister methods Unregister for mode change notification + * from IMS media library to determine if CVO mode needs to be activated or + * deactivated + */ + public void unregisterForCvoModeRequestChanged(Handler h) { + mCvoModeOnRegistrant.remove(h); + } + + public boolean isCvoModeEnabled() { + return mUIOrientationMode == CVO_MODE; + } +} diff --git a/src/com/android/incallui/ProximityListener.java b/src/com/android/incallui/ProximityListener.java new file mode 100644 index 00000000..a5a79ff8 --- /dev/null +++ b/src/com/android/incallui/ProximityListener.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2013 dimfish + * + * 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.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +public final class ProximityListener { + private static final String TAG = "ProximityListener"; + private static final boolean DEBUG = true; + private static final boolean VDEBUG = false; + + private static final float PROXIMITY_THRESHOLD = 5.0f; + + private long mLastProximityEventTime; + private boolean mActive; + + private SensorManager mSensorManager; + private Sensor mSensor; + + + public ProximityListener(Context context) { + mActive = false; + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + } + + public void enable(boolean enable) { + if (DEBUG) Log.d(TAG, "enable(" + enable + ")"); + synchronized (this) { + mActive = false; + if (enable) { + mSensorManager.registerListener(mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL); + } else { + mSensorManager.unregisterListener(mSensorListener); + } + } + } + + public boolean isActive() { + return mActive; + } + + SensorEventListener mSensorListener = new SensorEventListener() { + public void onSensorChanged(SensorEvent event) { + synchronized (this) { + float distance = event.values[0]; + // compare against getMaximumRange to support sensors that only return 0 or 1 + mActive = (distance >= 0.0 && distance < PROXIMITY_THRESHOLD && + distance < mSensor.getMaximumRange()); + + if (VDEBUG) Log.d(TAG, "mProximityListener.onSensorChanged active: " + mActive); + } + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // ignore + } + }; +} diff --git a/src/com/android/incallui/ProximitySensor.java b/src/com/android/incallui/ProximitySensor.java index 607a54f8..f3a7a9c0 100644 --- a/src/com/android/incallui/ProximitySensor.java +++ b/src/com/android/incallui/ProximitySensor.java @@ -43,6 +43,7 @@ public class ProximitySensor implements AccelerometerListener.OrientationListene private final PowerManager.WakeLock mProximityWakeLock; private final AudioModeProvider mAudioModeProvider; private final AccelerometerListener mAccelerometerListener; + private final ProximityListener mProximityListener; private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; private boolean mUiShowing = false; private boolean mIsPhoneOffhook = false; @@ -64,6 +65,7 @@ public class ProximitySensor implements AccelerometerListener.OrientationListene Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock); mAccelerometerListener = new AccelerometerListener(context, this); + mProximityListener = new ProximityListener(context); mAudioModeProvider = audioModeProvider; mAudioModeProvider.addListener(this); } @@ -72,6 +74,7 @@ public class ProximitySensor implements AccelerometerListener.OrientationListene mAudioModeProvider.removeListener(this); mAccelerometerListener.enable(false); + mProximityListener.enable(false); if (mProximityWakeLock != null && mProximityWakeLock.isHeld()) { mProximityWakeLock.release(); @@ -102,6 +105,7 @@ public class ProximitySensor implements AccelerometerListener.OrientationListene mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; mAccelerometerListener.enable(mIsPhoneOffhook); + mProximityListener.enable(mIsPhoneOffhook); updateProximitySensorMode(); } @@ -162,6 +166,10 @@ public class ProximitySensor implements AccelerometerListener.OrientationListene return !mPowerManager.isScreenOn(); } + public boolean isScreenOffByProximity() { + return mProximityListener.isActive(); + } + /** * @return true if this device supports the "proximity sensor * auto-lock" feature while in-call (see updateProximitySensorMode()). diff --git a/src/com/android/incallui/StatusBarNotifier.java b/src/com/android/incallui/StatusBarNotifier.java index 2de1b2d0..98442671 100644 --- a/src/com/android/incallui/StatusBarNotifier.java +++ b/src/com/android/incallui/StatusBarNotifier.java @@ -539,12 +539,18 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener { // If a call is onhold during an incoming call, the call actually comes in as // INCOMING. For that case *and* traditional call-waiting, we want to // cancel the notification. + + // For DSDA, we want to cancel the notification if we get an incoming call on + // one sub and there is a live call on another sub. + CallList callList = CallList.getInstance(); boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || (call.getState() == Call.State.INCOMING && - CallList.getInstance().getBackgroundCall() != null)); + (callList.getBackgroundCall() != null || + callList.isAnyOtherSubActive(callList.getActiveSubscription())))); if (isCallWaiting) { - Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); + Log.i(this, "updateInCallNotification: call-waiting or dsda incoming call!" + + " force relaunch..."); // Cancel the IN_CALL_NOTIFICATION immediately before // (re)posting it; this seems to force the // NotificationManager to launch the fullScreenIntent. diff --git a/src/com/android/incallui/VideoCallManager.java b/src/com/android/incallui/VideoCallManager.java new file mode 100644 index 00000000..f9bd0461 --- /dev/null +++ b/src/com/android/incallui/VideoCallManager.java @@ -0,0 +1,346 @@ +/* Copyright (c) 2012, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import com.android.incallui.CameraHandler.CameraState; +import com.android.incallui.CvoHandler.CvoEventListener; +import com.android.incallui.MediaHandler.IMediaEventListener; + +import java.io.IOException; + +/** + * Provides an interface for the applications to interact with Camera for the + * near end preview and sending the frames to the far end and also with Media + * engine to render the far end video during a Video Call Session. + */ +public class VideoCallManager { + private static final String TAG = "VideoCallManager"; + private static VideoCallManager mInstance; // Use a singleton + private static final int INVALID_SIZE = -1; + private CameraHandler mCameraHandler; + private MediaHandler mMediaHandler; + private CvoHandler mCvoHandler; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + log("handleMessage id=" + msg.what); + + switch (msg.what) { + case CVO_MODE_REQUEST_CHANGED: + ar = (AsyncResult) msg.obj; + if (ar != null && ar.result != null && ar.exception == null) { + boolean start = (Boolean) ar.result; + mCvoHandler.startOrientationListener(start); + } + break; + case CVO_INFO_CHANGED: + ar = (AsyncResult) msg.obj; + if (ar != null && ar.result != null && ar.exception == null) { + int orientation = (Integer) ar.result; + mMediaHandler.sendCvoInfo(orientation); + notifyCvoClient(orientation); + } + break; + } + } + }; + private CvoEventListener mCvoEventListener; + + private static final int CVO_MODE_REQUEST_CHANGED = 0; + private static final int CVO_INFO_CHANGED = 2; + + /** @hide */ + private VideoCallManager(Context context) { + log("Instantiating VideoCallManager"); + mCameraHandler = CameraHandler.getInstance(context); + mMediaHandler = MediaHandler.getInstance(); + mCvoHandler = new CvoHandler(context); + mMediaHandler.registerForCvoModeRequestChanged(mHandler, CVO_MODE_REQUEST_CHANGED, null); + mCvoHandler.registerForCvoInfoChange(mHandler, CVO_INFO_CHANGED, null); + } + + private void notifyCvoClient(int orientation) { + int angle = mCvoHandler + .convertMediaOrientationToActualAngle(orientation); + log("handleMessage Device orientation angle=" + angle); + if (mCvoEventListener != null) { + mCvoEventListener.onDeviceOrientationChanged(angle); + } + } + + /** + * This method returns the single instance of VideoCallManager object + * + * @param mContext + */ + public static synchronized VideoCallManager getInstance(Context context) { + if (mInstance == null) { + mInstance = new VideoCallManager(context); + } + return mInstance; + } + + /** + * Called to notify if media initialization/deinitialization is necessary. + * + * @param init true if the media should be initialized, false if it should be deinitialized. + */ + public void onMediaRequest(boolean init) { + if (!init) { + MediaHandler.deInit(); + } else if (mMediaHandler.init() == MediaHandler.DPL_INIT_SUCCESSFUL) { + MediaHandler.setSurface(); // TODO: Pass the surface if the surface is created. + } + } + + /** + * Initialize the Media + * @deprecated + */ + public int mediaInit() { + return mMediaHandler.init(); + } + + /** + * Deinitialize the Media + * @deprecated + */ + public void mediaDeInit() { + MediaHandler.deInit(); + } + + /** + * Send the SurfaceTexture to Media module + * @param st SurfaceTexture to be passed + */ + public void setFarEndSurface(SurfaceTexture st) { + MediaHandler.setSurface(st); + } + + /** + * Send the SurfaceTexture to Media module + */ + public void setFarEndSurface() { + MediaHandler.setSurface(); + } + + /** + * Get negotiated height + */ + public int getNegotiatedHeight() { + return MediaHandler.getNegotiatedHeight(); + } + + /** + * Get negotiated width + */ + public int getNegotiatedWidth() { + return MediaHandler.getNegotiatedWidth(); + } + + /** + * Get UI Orientation mode + */ + public int getUIOrientationMode() { + return mMediaHandler.getUIOrientationMode(); + } + + /** + * Get negotiated FPS + */ + public short getNegotiatedFps() { + return MediaHandler.getNegotiatedFps(); + } + + public boolean isCvoModeEnabled() { + return mMediaHandler.isCvoModeEnabled(); + } + + /** + * Return the number of cameras supported by the device + * + * @return number of cameras + */ + public int getNumberOfCameras() { + return mCameraHandler.getNumberOfCameras(); + } + + /** + * Open the camera hardware + * + * @param cameraId front or the back camera to open + * @return true if the camera was opened successfully + * @throws Exception + */ + public synchronized boolean openCamera(int cameraId) throws Exception { + return mCameraHandler.open(cameraId); + } + + /** + * Start the camera preview if camera was opened previously + * + * @param mSurfaceTexture Surface on which to draw the camera preview + * @throws IOException + */ + public void startCameraPreview(SurfaceTexture surfaceTexture) throws IOException { + mCameraHandler.startPreview(surfaceTexture); + } + + /** + * Close the camera hardware if the camera was opened previously + */ + public void closeCamera() { + mCameraHandler.close(); + } + + /** + * Stop the camera preview if the camera is open and the preview is not + * already started + */ + public void stopCameraPreview() { + mCameraHandler.stopPreview(); + } + + /** + * Get the camera ID for the back camera + * + * @return camera ID + */ + public int getBackCameraId() { + return mCameraHandler.getBackCameraId(); + } + + /** + * Get the camera ID for the front camera + * + * @return camera ID + */ + public int getFrontCameraId() { + return mCameraHandler.getFrontCameraId(); + } + + /** + * Return the current camera state + * + * @return current state of the camera state machine + */ + public CameraState getCameraState() { + return mCameraHandler.getCameraState(); + } + + /** + * Set the display texture for the camera + * + * @param surfaceTexture + */ + public void setDisplay(SurfaceTexture surfaceTexture) { + mCameraHandler.setDisplay(surfaceTexture); + } + + /** + * Returns the direction of the currently open camera + * + * @return one of the following possible values + * - CameraInfo.CAMERA_FACING_FRONT + * - CameraInfo.CAMERA_FACING_BACK + * - -1 - No Camera active + */ + public int getCameraDirection() { + return mCameraHandler.getCameraDirection(); + } + + /** + * Set the camera display orientation based on the screen rotation and the + * camera direction + */ + void setCameraDisplayOrientation() { + mCameraHandler.setDisplayOrientation(); + } + + public ImsCamera getImsCameraInstance() { + return mCameraHandler.getImsCameraInstance(); + } + + public void startCameraRecording() { + mCameraHandler.startCameraRecording(); + } + + public void stopCameraRecording() { + mCameraHandler.stopCameraRecording(); + } + + public void setMediaEventListener(VideoCallPanel.MediaEventListener listener) { + mMediaHandler.setMediaEventListener(listener); + } + + /* + * Setup a CVO Event listener for triggering UI callbacks like + * onDeviceOrientationChanged to be invoked directly + */ + public void setCvoEventListener(CvoEventListener listener) { + log("setCvoEventListener"); + // TODO: Create a list of listeners or do not allow over-write + mCvoEventListener = listener; + } + + public void startOrientationListener(boolean start) { + if (isCvoModeEnabled()) { + mCvoHandler.startOrientationListener(start); + } + } + + public float getPeerAspectRatio() { + int peerHeight = mMediaHandler.getPeerHeight(); + int peerWidth = mMediaHandler.getPeerWidth(); + //Check for invalid size and divide by zero + if (peerHeight == INVALID_SIZE || peerWidth == INVALID_SIZE || peerHeight == 0) { + loge("getPeerAspectRatio ERROR peerHeight=" + peerHeight + " peerWidth=" + peerWidth); + return INVALID_SIZE; + } + float aspectRatio = (float) peerWidth / peerHeight; + log("aspectRatio= " + aspectRatio + " peerHeight=" + peerHeight + " peerWidth=" + + peerWidth); + return aspectRatio; + } + + private void log(String msg) { + Log.d(TAG, msg); + } + + private void loge(String msg) { + Log.e(TAG, msg); + } +} diff --git a/src/com/android/incallui/VideoCallPanel.java b/src/com/android/incallui/VideoCallPanel.java new file mode 100644 index 00000000..1f71c0f3 --- /dev/null +++ b/src/com/android/incallui/VideoCallPanel.java @@ -0,0 +1,651 @@ +/* Copyright (c) 2012-2013, The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.incallui; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.os.SystemProperties; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.incallui.CameraHandler.CameraState; +import com.android.services.telephony.common.CallDetails; + +import java.io.IOException; + +/** + * Helper class to initialize and run the InCallScreen's "Video Call" UI. + */ +public class VideoCallPanel extends RelativeLayout implements TextureView.SurfaceTextureListener, View.OnClickListener { + private static final int LOOPBACK_MODE_HEIGHT = 144; + private static final int LOOPBACK_MODE_WIDTH = 176; + private static final int CAMERA_UNKNOWN = -1; + private static final String LOG_TAG = "VideoCallPanel"; + private static final boolean DBG = true; + + private static final int MEDIA_TO_CAMERA_CONV_UNIT = 1000; + private static final int DEFAULT_CAMERA_ZOOM_VALUE = 0; + private static final int INVALID_SIZE = -1; + + private Context mContext; + private VideoCallManager mVideoCallManager; + + // "Video Call" UI elements and state + private ViewGroup mVideoCallPanel; + private ZoomControlBar mZoomControl; + private TextureView mFarEndView; + private TextureView mCameraPreview; + private SurfaceTexture mCameraSurface; + private SurfaceTexture mFarEndSurface; + private ImageView mCameraPicker; + private final Resize mResize = new Resize(); + + private int mZoomMax; + private int mZoomValue; // The current zoom value + + // Multiple cameras support + private int mNumberOfCameras; + private int mFrontCameraId; + private int mBackCameraId; + private int mCameraId; + + private int mHeight = INVALID_SIZE; + private int mWidth = INVALID_SIZE; + + // Property used to indicate that the Media running in loopback mode + private boolean mIsMediaLoopback = false; + + // Flag to indicate if camera is needed for a certain call type. + // For eg. VT_RX call will not need camera + private boolean mCameraNeeded = false; + + /** + * This class implements the zoom listener for zoomControl + */ + private class ZoomChangeListener implements ZoomControl.OnZoomChangedListener { + @Override + public void onZoomValueChanged(int index) { + VideoCallPanel.this.onZoomValueChanged(index); + } + } + + + /** + * This class implements the listener for PARAM READY EVENT + */ + public class MediaEventListener implements MediaHandler.IMediaEventListener { + @Override + public void onParamReadyEvent() { + CameraState cameraState = mVideoCallManager.getCameraState(); + if (DBG) log("onParamReadyEvent cameraState= " + cameraState); + if (cameraState == CameraState.PREVIEW_STARTED) { + // If camera is already capturing stop preview, reset the + // parameters and then start preview again + try { + mVideoCallManager.stopCameraRecording(); + mVideoCallManager.stopCameraPreview(); + initializeCameraParams(); + mVideoCallManager.startCameraPreview(mCameraSurface); + mVideoCallManager.startCameraRecording(); + } catch (IOException ioe) { + loge("Exception onParamReadyEvent stopping and starting preview " + + ioe.toString()); + } + } + } + + @Override + public void onDisplayModeEvent() { + // NO-OP + } + + @Override + public void onStartReadyEvent() { + // NO-OP + } + + @Override + public void onPeerResolutionChangeEvent() { + if (DBG) log("onPeerResolutionChangeEvent"); + + if (mHeight != INVALID_SIZE && mWidth != INVALID_SIZE) { + resizeFarEndView(); + } + } + } + + public class CvoListener implements CvoHandler.CvoEventListener { + @Override + public void onDeviceOrientationChanged(int rotation) { + int requiredSurfaceRotation = 360 - rotation; + if (DBG) { + log("onDeviceOrientationChanged: Local sensor rotation =" + rotation + + " Rotate far end based on local sensor by " + requiredSurfaceRotation); + } + mFarEndView.setRotation(requiredSurfaceRotation); + } + } + + public VideoCallPanel(Context context) { + super(context); + mContext = context; + } + + public VideoCallPanel(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + } + + public VideoCallPanel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mContext = context; + } + + /** + * Finalize view from inflation. + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + if (DBG) log("onFinishInflate(this = " + this + ")..."); + + // Check the Media loopback property + int property = SystemProperties.getInt("net.lte.VT_LOOPBACK_ENABLE", 0); + mIsMediaLoopback = (property == 1) ? true : false; + if (DBG) log("Is Media running in loopback mode: " + mIsMediaLoopback); + + // Get UI widgets + mVideoCallPanel = (ViewGroup) findViewById(R.id.videoCallPanel); + mZoomControl = (ZoomControlBar) findViewById(R.id.zoom_control); + mFarEndView = (TextureView) findViewById(R.id.video_view); + mCameraPreview = (TextureView) findViewById(R.id.camera_view); + mCameraPicker = (ImageView) findViewById(R.id.camera_picker); + + // Set listeners + mCameraPreview.setSurfaceTextureListener(this); + mFarEndView.setSurfaceTextureListener(this); + mCameraPicker.setOnClickListener(this); + + // Get the camera IDs for front and back cameras + mVideoCallManager = VideoCallManager.getInstance(mContext); + mBackCameraId = mVideoCallManager.getBackCameraId(); + mFrontCameraId = mVideoCallManager.getFrontCameraId(); + chooseCamera(true); + + // Check if camera supports dual cameras + mNumberOfCameras = mVideoCallManager.getNumberOfCameras(); + if (mNumberOfCameras > 1) { + mCameraPicker.setVisibility(View.VISIBLE); + } else { + mCameraPicker.setVisibility(View.GONE); + } + + // Set media event listener + mVideoCallManager.setMediaEventListener(new MediaEventListener()); + mVideoCallManager.setCvoEventListener(new CvoListener()); + } + + public void setCameraNeeded(boolean mCameraNeeded) { + this.mCameraNeeded = mCameraNeeded; + } + + /** + * Call is either is either being originated or an MT call is received. + */ + public void onCallInitiating(int callType) { + if (DBG) log("onCallInitiating"); + + // Only for VT TX it is required to default to back camera + boolean chooseFrontCamera = true; + if (callType == CallDetails.CALL_TYPE_VT_TX) { + chooseFrontCamera = false; + } + + chooseCamera(chooseFrontCamera); + + if (callType == CallDetails.CALL_TYPE_VT + || callType == CallDetails.CALL_TYPE_VT_TX) { + mCameraNeeded = true; + } else { + mCameraNeeded = false; + } + } + + /** + * Called during layout when the size of the view has changed. This method + * store the VideoCallPanel size to be later used to resize the camera + * preview accordingly + */ + @Override + protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { + log("onSizeChanged"); + log("Video Panel xNew=" + xNew + ", yNew=" + yNew + " xOld=" + xOld + " yOld=" + yOld); + if (xNew != xOld || yNew != yOld) { + post(mResize); + } + } + + private class Resize implements Runnable { + @Override + public void run() { + doSizeChanged(); + } + } + + private void doSizeChanged() { + mWidth = getWidth(); + mHeight = getHeight(); + + if (DBG) log("doSizeChanged: VideoCallPanel width=" + mWidth + ", height=" + mHeight); + resizeCameraPreview(); + resizeFarEndView(); + } + + /** + * Called when the InCallScreen activity is being paused. This method hides + * the VideoCallPanel so that other activities can use the camera at this + * time. + */ + public void onPause() { + if (DBG) log("onPause"); + mVideoCallPanel.setVisibility(View.GONE); + } + + /** + * This method opens the camera and starts the camera preview + */ + private void initializeCamera() { + if (DBG) log("Initializing camera id=" + mCameraId); + + if (mCameraId == CAMERA_UNKNOWN) { + loge("initializeCamera: Not initializing camera as mCameraId is unknown"); + return; + } + + // Open camera if not already open + if (false == openCamera(mCameraId)) { + return; + } + initializeZoom(); + initializeCameraParams(); + startPreviewAndRecording(); + } + + public boolean isCameraInitNeeded() { + if (DBG) { + log("isCameraInitNeeded mCameraNeeded=" + mCameraNeeded + " mCameraSurface= " + + mCameraSurface + " camera state = " + + mVideoCallManager.getCameraState()); + } + return mCameraNeeded && mCameraSurface != null + && mVideoCallManager.getCameraState() == CameraState.CAMERA_CLOSED; + } + + /** + * This method crates the camera object if camera is not disabled + * + * @param cameraId ID of the front or the back camera + * @return Camera instance on success, null otherwise + */ + private boolean openCamera(int cameraId) { + boolean result = false; + + try { + return mVideoCallManager.openCamera(cameraId); + } catch (Exception e) { + loge("Failed to open camera device, error " + e.toString()); + return result; + } + } + + /** + * This method disconnect and releases the camera + */ + private void closeCamera() { + mVideoCallManager.closeCamera(); + } + + /** + * This method starts the camera preview and recording + */ + private void startPreviewAndRecording() { + try { + mVideoCallManager.startCameraPreview(mCameraSurface); + mVideoCallManager.startCameraRecording(); + } catch (IOException ioe) { + closeCamera(); + loge("Exception startPreviewAndRecording, " + ioe.toString()); + } + } + + /** + * This method stops the camera recording and preview + */ + private void stopRecordingAndPreview() { + mVideoCallManager.stopCameraRecording(); + mVideoCallManager.stopCameraPreview(); + } + + /* Implementation of listeners */ + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + if (surface.equals(mCameraPreview.getSurfaceTexture())) { + if (DBG) log("Camera surface texture created"); + mCameraSurface = surface; + if (isCameraInitNeeded()) { + initializeCamera(); + } + } else if (surface.equals(mFarEndView.getSurfaceTexture())) { + if (DBG) log("Video surface texture created"); + mFarEndSurface = surface; + mVideoCallManager.setFarEndSurface(mFarEndSurface); + } + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (surface.equals(mCameraPreview.getSurfaceTexture())) { + if (DBG) log("CameraPreview surface texture destroyed"); + stopRecordingAndPreview(); + closeCamera(); + mCameraSurface = null; + } else if (surface.equals(mFarEndView.getSurfaceTexture())) { + if (DBG) log("FarEndView surface texture destroyed"); + mFarEndSurface = null; + mVideoCallManager.setFarEndSurface(null); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // Invoked every time there's a new Camera preview frame + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + // Ignored camera does all the work for us + } + + /** + * This method is called when the visibility of the VideoCallPanel is changed + */ + @Override + protected void onVisibilityChanged (View changedView, int visibility) { + if (changedView != this || mVideoCallManager == null) { + return; + } + + switch(visibility) + { + case View.INVISIBLE: + case View.GONE: + if (DBG) log("VideoCallPanel View is GONE or INVISIBLE"); + // Stop the preview and close the camera now because other + // activities may need to use it + if (mVideoCallManager.getCameraState() != CameraState.CAMERA_CLOSED) { + stopRecordingAndPreview(); + closeCamera(); + } + break; + case View.VISIBLE: + if (DBG) log("VideoCallPanel View is VISIBLE"); + if (isCameraInitNeeded()) { + initializeCamera(); + } + break; + } + } + + @Override + public void onClick(View v) { + int direction = mVideoCallManager.getCameraDirection(); + + // Switch the camera front/back/off + // The state machine is as follows + // front --> back --> stop preview --> front... + switch(direction) { + case CAMERA_UNKNOWN: + switchCamera(mFrontCameraId); + break; + case Camera.CameraInfo.CAMERA_FACING_FRONT: + switchCamera(mBackCameraId); + break; + case Camera.CameraInfo.CAMERA_FACING_BACK: + switchCamera(CAMERA_UNKNOWN); + break; + } + } + + /** + * This method get the zoom related parameters from the camera and + * initialized the zoom control + */ + private void initializeZoom() { + ImsCamera imsCamera = mVideoCallManager.getImsCameraInstance(); + if (imsCamera == null) { + return; + } + if (!imsCamera.isZoomSupported()) { + mZoomControl.setVisibility(View.GONE); // Disable ZoomControl + return; + } + + mZoomControl.setVisibility(View.VISIBLE); // Enable ZoomControl + mZoomMax = imsCamera.getMaxZoom(); + // Currently we use immediate zoom for fast zooming to get better UX and + // there is no plan to take advantage of the smooth zoom. + mZoomControl.setZoomMax(mZoomMax); + mZoomControl.setZoomIndex(DEFAULT_CAMERA_ZOOM_VALUE); + mZoomControl.setOnZoomChangeListener(new ZoomChangeListener()); + } + + /** + * This method gets called when the zoom control reports that the zoom value + * has changed. This method sets the camera zoom value accordingly. + * @param index + */ + private void onZoomValueChanged(int index) { + mZoomValue = index; + ImsCamera imsCamera = mVideoCallManager.getImsCameraInstance(); + // Set zoom + if (imsCamera.isZoomSupported()) { + imsCamera.setZoom(mZoomValue); + } + } + + /** + * Initialize camera parameters based on negotiated height, width + */ + private void initializeCameraParams() { + try { + // Get the parameter to make sure we have the up-to-date value. + ImsCamera imsCamera = mVideoCallManager.getImsCameraInstance(); + // Set the camera preview size + if (mIsMediaLoopback) { + // In loopback mode the IMS is hard coded to render the + // camera frames of only the size 176x144 on the far end surface + imsCamera.setPreviewSize(LOOPBACK_MODE_WIDTH, LOOPBACK_MODE_HEIGHT); + } else { + log("Set Preview Size directly with negotiated Height = " + + mVideoCallManager.getNegotiatedHeight() + + " negotiated width= " + mVideoCallManager.getNegotiatedWidth()); + imsCamera.setPreviewSize(mVideoCallManager.getNegotiatedWidth(), + mVideoCallManager.getNegotiatedHeight()); + imsCamera.setPreviewFpsRange(mVideoCallManager.getNegotiatedFps()); + } + } catch (RuntimeException e) { + loge("Error setting Camera preview size/fps exception=" + e); + } + } + + public void setPanelElementsVisibility(int callType) { + log("setPanelElementsVisibility: callType= " + callType); + switch (callType) { + case CallDetails.CALL_TYPE_VT: + mCameraPreview.setVisibility(VISIBLE); + mFarEndView.setVisibility(VISIBLE); + if (isCameraInitNeeded()) { + initializeCamera(); + } + log("setPanelElementsVisibility: VT: mCameraPreview:VISIBLE, mFarEndView:VISIBLE"); + break; + case CallDetails.CALL_TYPE_VT_TX: + mCameraPreview.setVisibility(View.VISIBLE); + if (isCameraInitNeeded()) { + initializeCamera(); + } + // Not setting mFarEndView to GONE as receiver side did not get the frames + log("setPanelElementsVisibility VT_TX: mCameraPreview:VISIBLE"); + break; + case CallDetails.CALL_TYPE_VT_RX: + mFarEndView.setVisibility(View.VISIBLE); + // Stop the preview and close the camera now because other + // activities may need to use it + if (mVideoCallManager.getCameraState() != CameraState.CAMERA_CLOSED) { + stopRecordingAndPreview(); + closeCamera(); + } + mCameraPreview.setVisibility(View.GONE); + log("setPanelElementsVisibility VT_RX: mCameraPreview:GONE mFarEndView:VISIBLE"); + break; + default: + log("setPanelElementsVisibility: Default: " + + "VideoCallPanel is " + mVideoCallPanel.getVisibility() + + "mCameraPreview is " + mCameraPreview.getVisibility() + + "mFarEndView is " + mFarEndView.getVisibility()); + break; + } + } + + /** + * This method resizes the camera preview based on the size of the + * VideoCallPanel + */ + private void resizeCameraPreview() { + if (DBG) log("resizeCameraPreview: mHeight=" + mHeight); + // For now, set the preview size to be 1/4th of the VideoCallPanel + ViewGroup.LayoutParams cameraPreivewLp = mCameraPreview.getLayoutParams(); + cameraPreivewLp.height = mHeight / 4; + cameraPreivewLp.width = mHeight / 4; // use mHeight to create small + // square box for camera preview + mCameraPreview.setLayoutParams(cameraPreivewLp); + } + + /** + * This method resizes the far end view based on the size of VideoCallPanel + * Presently supports only full size far end video + */ + private void resizeFarEndView() { + int minDimension = Math.min(mWidth, mHeight); + int farEndWidth = mWidth; + int farEndHeight = mHeight; + float aspectRatio = mVideoCallManager.getPeerAspectRatio(); + if (aspectRatio > 1) { + // Width > Height, so fix the width + farEndWidth = minDimension; + farEndHeight = Math.round(minDimension / aspectRatio); + } else if (aspectRatio > 0 && aspectRatio <= 1) { + farEndHeight = minDimension; + farEndWidth = Math.round(aspectRatio * minDimension); + } // In other cases continue with target height and width + if (DBG) { + log("resizeFarEndView FarEnd to width:" + farEndWidth + ", height:" + farEndHeight); + } + + ViewGroup.LayoutParams farEndViewLp = mFarEndView.getLayoutParams(); + farEndViewLp.height = farEndHeight; + farEndViewLp.width = farEndWidth; + + mFarEndView.setLayoutParams(farEndViewLp); + } + + /** + * This method switches the camera to front/back or off + * @param cameraId + */ + private void switchCamera(int cameraId) { + // Change the camera Id + mCameraId = cameraId; + + // Stop camera preview if already running + if (mVideoCallManager.getCameraState() != CameraState.CAMERA_CLOSED) { + stopRecordingAndPreview(); + closeCamera(); + } + + log("VideoCall: switchCamera: IsCameraNeeded=" + mCameraNeeded + " cameraId=" + cameraId); + final boolean showCameraPreview = mCameraNeeded && cameraId != CAMERA_UNKNOWN; + mCameraPreview.setVisibility(showCameraPreview ? TextureView.VISIBLE : TextureView.GONE); + + // Restart camera if camera doesn't need to stay off + if (isCameraInitNeeded()) { + initializeCamera(); + } + } + + /** + * Choose the camera direction to the front camera if it is available. Else + * set the camera direction to the rear facing + */ + private void chooseCamera(boolean chooseFrontCamera) { + // Assuming that both cameras are available. + mCameraId = chooseFrontCamera ? mFrontCameraId : mBackCameraId; + + // The requested camera is not available. + if (mCameraId == CAMERA_UNKNOWN) { + // Reverse the above logic, this will choose the other camera. + mCameraId = !chooseFrontCamera ? mFrontCameraId : mBackCameraId; + } + + if (mCameraId == CAMERA_UNKNOWN) { + loge("chooseCamera " + chooseFrontCamera + " Both camera ids unknown"); + } + } + + public void startOrientationListener(boolean start) { + mVideoCallManager.startOrientationListener(start); + } + + private void log(String msg) { + Log.d(LOG_TAG, msg); + } + + private void loge(String msg) { + Log.e(LOG_TAG, msg); + } +} diff --git a/src/com/android/incallui/ZoomControl.java b/src/com/android/incallui/ZoomControl.java new file mode 100644 index 00000000..ce98279a --- /dev/null +++ b/src/com/android/incallui/ZoomControl.java @@ -0,0 +1,124 @@ +/* Copyright (c) 2012, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * + * Copyright (C) 2011 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.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +/** + * A view that contains camera zoom control which could adjust the zoom in/out + * if the camera supports zooming. + */ +public abstract class ZoomControl extends RelativeLayout{ + protected ImageView mZoomIn; + protected ImageView mZoomOut; + protected ImageView mZoomSlider; + protected int mOrientation; + + public interface OnZoomChangedListener { + void onZoomValueChanged(int index); // only for immediate zoom + } + + // The interface OnZoomIndexChangedListener is used to inform the + // ZoomIndexBar about the zoom index change. The index position is between + // 0 (the index is zero) and 1.0 (the index is mZoomMax). + public interface OnZoomIndexChangedListener { + void onZoomIndexChanged(double indexPosition); + } + + protected int mZoomMax, mZoomIndex; + private OnZoomChangedListener mListener; + + private int mStep; + + public ZoomControl(Context context, AttributeSet attrs) { + super(context, attrs); + mZoomIn = addImageView(context, R.drawable.ic_zoom_in); + mZoomSlider = addImageView(context, R.drawable.ic_zoom_slider); + mZoomOut = addImageView(context, R.drawable.ic_zoom_out); + } + + public void startZoomControl() { + mZoomSlider.setPressed(true); + setZoomIndex(mZoomIndex); // Update the zoom index bar. + } + + protected ImageView addImageView(Context context, int iconResourceId) { + ImageView image = new ImageView(context); + image.setImageResource(iconResourceId); + addView(image); + return image; + } + + public void closeZoomControl() { + mZoomSlider.setPressed(false); + } + + public void setZoomMax(int zoomMax) { + mZoomMax = zoomMax; + + // Layout should be requested as the maximum zoom level is the key to + // show the correct zoom slider position. + requestLayout(); + } + + public void setOnZoomChangeListener(OnZoomChangedListener listener) { + mListener = listener; + } + + public void setZoomIndex(int index) { + if (index < 0 || index > mZoomMax) { + throw new IllegalArgumentException("Invalid zoom value:" + index); + } + mZoomIndex = index; + invalidate(); + } + + protected void setZoomStep(int step) { + mStep = step; + } + + // Called from ZoomControlBar to change the zoom level. + protected void performZoom(double zoomPercentage) { + int index = (int) (mZoomMax * zoomPercentage); + if (mZoomIndex == index) return; + changeZoomIndex(index); + } + + private boolean changeZoomIndex(int index) { + if (mListener != null) { + if (index > mZoomMax) index = mZoomMax; + if (index < 0) index = 0; + mListener.onZoomValueChanged(index); + mZoomIndex = index; + } + return true; + } + + @Override + public void setActivated(boolean activated) { + super.setActivated(activated); + mZoomIn.setActivated(activated); + mZoomOut.setActivated(activated); + } +} diff --git a/src/com/android/incallui/ZoomControlBar.java b/src/com/android/incallui/ZoomControlBar.java new file mode 100644 index 00000000..7d7597eb --- /dev/null +++ b/src/com/android/incallui/ZoomControlBar.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2012, The Linux Foundation. All rights reserved. + * Not a Contribution, Apache license notifications and license are retained + * for attribution purposes only. + * + * Copyright (C) 2011 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.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * A view that contains camera zoom control and its layout. + */ +public class ZoomControlBar extends ZoomControl { + private static final int THRESHOLD_FIRST_MOVE = 10; // pixels + // Space between indicator icon and the zoom-in/out icon. + private static final int ICON_SPACING = 12; + + private View mBar; + private boolean mStartChanging; + private int mSliderPosition = 0; + private int mSliderLength; + private int mWidth; + private int mIconWidth; + private int mTotalIconWidth; + + public ZoomControlBar(Context context, AttributeSet attrs) { + super(context, attrs); + mBar = new View(context); + mBar.setBackgroundResource(R.drawable.zoom_slider_bar); + addView(mBar); + } + + @Override + public void setActivated(boolean activated) { + super.setActivated(activated); + mBar.setActivated(activated); + } + + private int getSliderPosition(int x) { + // Calculate the absolute offset of the slider in the zoom control bar. + // For left-hand users, as the device is rotated for 180 degree for + // landscape mode, the zoom-in bottom should be on the top, so the + // position should be reversed. + int pos; // the relative position in the zoom slider bar + if (mOrientation == 90) { + pos = mWidth - mTotalIconWidth - x; + } else { + pos = x - mTotalIconWidth; + } + if (pos < 0) pos = 0; + if (pos > mSliderLength) pos = mSliderLength; + return pos; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + mIconWidth = mZoomIn.getMeasuredWidth(); + mTotalIconWidth = mIconWidth + ICON_SPACING; + mSliderLength = mWidth - (2 * mTotalIconWidth); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!isEnabled() || (mWidth == 0)) return false; + int action = event.getAction(); + + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + setActivated(false); + closeZoomControl(); + break; + + case MotionEvent.ACTION_DOWN: + setActivated(true); + mStartChanging = false; + case MotionEvent.ACTION_MOVE: + int pos = getSliderPosition((int) event.getX()); + if (!mStartChanging) { + // Make sure the movement is large enough before we start + // changing the zoom. + int delta = mSliderPosition - pos; + if ((delta > THRESHOLD_FIRST_MOVE) || + (delta < -THRESHOLD_FIRST_MOVE)) { + mStartChanging = true; + } + } + if (mStartChanging) { + performZoom(1.0d * pos / mSliderLength); + mSliderPosition = pos; + } + requestLayout(); + } + return true; + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (mZoomMax == 0) return; + int height = bottom - top; + mBar.layout(mTotalIconWidth, 0, mWidth - mTotalIconWidth, height); + // For left-hand users, as the device is rotated for 180 degree, + // the zoom-in button should be on the top. + int pos; // slider position + int sliderPosition; + if (mSliderPosition != -1) { // -1 means invalid + sliderPosition = mSliderPosition; + } else { + sliderPosition = (int) ((double) mSliderLength * mZoomIndex / mZoomMax); + } + if (mOrientation == 90) { + mZoomIn.layout(0, 0, mIconWidth, height); + mZoomOut.layout(mWidth - mIconWidth, 0, mWidth, height); + pos = mBar.getRight() - sliderPosition; + } else { + mZoomOut.layout(0, 0, mIconWidth, height); + mZoomIn.layout(mWidth - mIconWidth, 0, mWidth, height); + pos = mBar.getLeft() + sliderPosition; + } + int sliderWidth = mZoomSlider.getMeasuredWidth(); + mZoomSlider.layout((pos - sliderWidth / 2), 0, + (pos + sliderWidth / 2), height); + } + + @Override + public void setZoomIndex(int index) { + super.setZoomIndex(index); + mSliderPosition = -1; // -1 means invalid + requestLayout(); + } +} |