blob: 986765e4172a2b52750be92a2c19d636dfb22522 [file] [log] [blame]
/*
* Copyright (C) 2009 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.phone;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.ViewStub;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.CallManager;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.widget.multiwaveview.GlowPadView;
import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
import com.android.phone.InCallUiState.InCallScreenMode;
/**
* In-call onscreen touch UI elements, used on some platforms.
*
* This widget is a fullscreen overlay, drawn on top of the
* non-touch-sensitive parts of the in-call UI (i.e. the call card).
*/
public class InCallTouchUi extends FrameLayout
implements View.OnClickListener, View.OnLongClickListener, OnTriggerListener,
PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
private static final String LOG_TAG = "InCallTouchUi";
private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
// Incoming call widget targets
private static final int ANSWER_CALL_ID = 0; // drag right
private static final int SEND_SMS_ID = 1; // drag up
private static final int DECLINE_CALL_ID = 2; // drag left
/**
* Reference to the InCallScreen activity that owns us. This may be
* null if we haven't been initialized yet *or* after the InCallScreen
* activity has been destroyed.
*/
private InCallScreen mInCallScreen;
// Phone app instance
private PhoneGlobals mApp;
// UI containers / elements
private GlowPadView mIncomingCallWidget; // UI used for an incoming call
private boolean mIncomingCallWidgetIsFadingOut;
private boolean mIncomingCallWidgetShouldBeReset = true;
/** UI elements while on a regular call (bottom buttons, DTMF dialpad) */
private View mInCallControls;
private boolean mShowInCallControlsDuringHidingAnimation;
//
private ImageButton mAddButton;
private ImageButton mMergeButton;
private ImageButton mEndButton;
private CompoundButton mDialpadButton;
private CompoundButton mMuteButton;
private CompoundButton mAudioButton;
private CompoundButton mHoldButton;
private ImageButton mSwapButton;
private View mHoldSwapSpacer;
// "Extra button row"
private ViewStub mExtraButtonRow;
private ViewGroup mCdmaMergeButton;
private ViewGroup mManageConferenceButton;
private ImageButton mManageConferenceButtonImage;
// "Audio mode" PopupMenu
private PopupMenu mAudioModePopup;
private boolean mAudioModePopupVisible = false;
// Time of the most recent "answer" or "reject" action (see updateState())
private long mLastIncomingCallActionTime; // in SystemClock.uptimeMillis() time base
// Parameters for the GlowPadView "ping" animation; see triggerPing().
private static final boolean ENABLE_PING_ON_RING_EVENTS = false;
private static final boolean ENABLE_PING_AUTO_REPEAT = true;
private static final long PING_AUTO_REPEAT_DELAY_MSEC = 1200;
private static final int INCOMING_CALL_WIDGET_PING = 101;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// If the InCallScreen activity isn't around any more,
// there's no point doing anything here.
if (mInCallScreen == null) return;
switch (msg.what) {
case INCOMING_CALL_WIDGET_PING:
if (DBG) log("INCOMING_CALL_WIDGET_PING...");
triggerPing();
break;
default:
Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
break;
}
}
};
public InCallTouchUi(Context context, AttributeSet attrs) {
super(context, attrs);
if (DBG) log("InCallTouchUi constructor...");
if (DBG) log("- this = " + this);
if (DBG) log("- context " + context + ", attrs " + attrs);
mApp = PhoneGlobals.getInstance();
}
void setInCallScreenInstance(InCallScreen inCallScreen) {
mInCallScreen = inCallScreen;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
// Look up the various UI elements.
// "Drag-to-answer" widget for incoming calls.
mIncomingCallWidget = (GlowPadView) findViewById(R.id.incomingCallWidget);
mIncomingCallWidget.setOnTriggerListener(this);
// Container for the UI elements shown while on a regular call.
mInCallControls = findViewById(R.id.inCallControls);
// Regular (single-tap) buttons, where we listen for click events:
// Main cluster of buttons:
mAddButton = (ImageButton) mInCallControls.findViewById(R.id.addButton);
mAddButton.setOnClickListener(this);
mAddButton.setOnLongClickListener(this);
mMergeButton = (ImageButton) mInCallControls.findViewById(R.id.mergeButton);
mMergeButton.setOnClickListener(this);
mMergeButton.setOnLongClickListener(this);
mEndButton = (ImageButton) mInCallControls.findViewById(R.id.endButton);
mEndButton.setOnClickListener(this);
mDialpadButton = (CompoundButton) mInCallControls.findViewById(R.id.dialpadButton);
mDialpadButton.setOnClickListener(this);
mDialpadButton.setOnLongClickListener(this);
mMuteButton = (CompoundButton) mInCallControls.findViewById(R.id.muteButton);
mMuteButton.setOnClickListener(this);
mMuteButton.setOnLongClickListener(this);
mAudioButton = (CompoundButton) mInCallControls.findViewById(R.id.audioButton);
mAudioButton.setOnClickListener(this);
mAudioButton.setOnLongClickListener(this);
mHoldButton = (CompoundButton) mInCallControls.findViewById(R.id.holdButton);
mHoldButton.setOnClickListener(this);
mHoldButton.setOnLongClickListener(this);
mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
mSwapButton.setOnClickListener(this);
mSwapButton.setOnLongClickListener(this);
mHoldSwapSpacer = mInCallControls.findViewById(R.id.holdSwapSpacer);
// TODO: Back when these buttons had text labels, we changed
// the label of mSwapButton for CDMA as follows:
//
// if (PhoneApp.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
// // In CDMA we use a generalized text - "Manage call", as behavior on selecting
// // this option depends entirely on what the current call state is.
// mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
// } else {
// mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
// }
//
// If this is still needed, consider having a special icon for this
// button in CDMA.
// Buttons shown on the "extra button row", only visible in certain (rare) states.
mExtraButtonRow = (ViewStub) mInCallControls.findViewById(R.id.extraButtonRow);
// If in PORTRAIT, add a custom OnTouchListener to shrink the "hit target".
if (!PhoneUtils.isLandscape(this.getContext())) {
mEndButton.setOnTouchListener(new SmallerHitTargetTouchListener());
}
}
/**
* Updates the visibility and/or state of our UI elements, based on
* the current state of the phone.
*
* TODO: This function should be relying on a state defined by InCallScreen,
* and not generic call states. The incoming call screen handles more states
* than Call.State or PhoneConstant.State know about.
*/
/* package */ void updateState(CallManager cm) {
if (mInCallScreen == null) {
log("- updateState: mInCallScreen has been destroyed; bailing out...");
return;
}
PhoneConstants.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK
if (DBG) log("updateState: current state = " + state);
boolean showIncomingCallControls = false;
boolean showInCallControls = false;
final Call ringingCall = cm.getFirstActiveRingingCall();
final Call.State fgCallState = cm.getActiveFgCallState();
// If the FG call is dialing/alerting, we should display for that call
// and ignore the ringing call. This case happens when the telephony
// layer rejects the ringing call while the FG call is dialing/alerting,
// but the incoming call *does* briefly exist in the DISCONNECTING or
// DISCONNECTED state.
if ((ringingCall.getState() != Call.State.IDLE) && !fgCallState.isDialing()) {
// A phone call is ringing *or* call waiting.
// Watch out: even if the phone state is RINGING, it's
// possible for the ringing call to be in the DISCONNECTING
// state. (This typically happens immediately after the user
// rejects an incoming call, and in that case we *don't* show
// the incoming call controls.)
if (ringingCall.getState().isAlive()) {
if (DBG) log("- updateState: RINGING! Showing incoming call controls...");
showIncomingCallControls = true;
}
// Ugly hack to cover up slow response from the radio:
// if we get an updateState() call immediately after answering/rejecting a call
// (via onTrigger()), *don't* show the incoming call
// UI even if the phone is still in the RINGING state.
// This covers up a slow response from the radio for some actions.
// To detect that situation, we are using "500 msec" heuristics.
//
// Watch out: we should *not* rely on this behavior when "instant text response" action
// has been chosen. See also onTrigger() for why.
long now = SystemClock.uptimeMillis();
if (now < mLastIncomingCallActionTime + 500) {
log("updateState: Too soon after last action; not drawing!");
showIncomingCallControls = false;
}
// b/6765896
// If the glowview triggers two hits of the respond-via-sms gadget in
// quick succession, it can cause the incoming call widget to show and hide
// twice in a row. However, the second hide doesn't get triggered because
// we are already attemping to hide. This causes an additional glowview to
// stay up above all other screens.
// In reality, we shouldn't even be showing incoming-call UI while we are
// showing the respond-via-sms popup, so we check for that here.
//
// TODO: In the future, this entire state machine
// should be reworked. Respond-via-sms was stapled onto the current
// design (and so were other states) and should be made a first-class
// citizen in a new state machine.
if (mInCallScreen.isQuickResponseDialogShowing()) {
log("updateState: quickResponse visible. Cancel showing incoming call controls.");
showIncomingCallControls = false;
}
} else {
// Ok, show the regular in-call touch UI (with some exceptions):
if (okToShowInCallControls()) {
showInCallControls = true;
} else {
if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
}
}
// In usual cases we don't allow showing both incoming call controls and in-call controls.
//
// There's one exception: if this call is during fading-out animation for the incoming
// call controls, we need to show both for smoother transition.
if (showIncomingCallControls && showInCallControls) {
throw new IllegalStateException(
"'Incoming' and 'in-call' touch controls visible at the same time!");
}
if (mShowInCallControlsDuringHidingAnimation) {
if (DBG) {
log("- updateState: FORCE showing in-call controls during incoming call widget"
+ " being hidden with animation");
}
showInCallControls = true;
}
// Update visibility and state of the incoming call controls or
// the normal in-call controls.
if (showInCallControls) {
if (DBG) log("- updateState: showing in-call controls...");
updateInCallControls(cm);
mInCallControls.setVisibility(View.VISIBLE);
} else {
if (DBG) log("- updateState: HIDING in-call controls...");
mInCallControls.setVisibility(View.GONE);
}
if (showIncomingCallControls) {
if (DBG) log("- updateState: showing incoming call widget...");
showIncomingCallWidget(ringingCall);
// On devices with a system bar (soft buttons at the bottom of
// the screen), disable navigation while the incoming-call UI
// is up.
// This prevents false touches (e.g. on the "Recents" button)
// from interfering with the incoming call UI, like if you
// accidentally touch the system bar while pulling the phone
// out of your pocket.
mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(false);
} else {
if (DBG) log("- updateState: HIDING incoming call widget...");
hideIncomingCallWidget();
// The system bar is allowed to work normally in regular
// in-call states.
mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
}
// Dismiss the "Audio mode" PopupMenu if necessary.
//
// The "Audio mode" popup is only relevant in call states that support
// in-call audio, namely when the phone is OFFHOOK (not RINGING), *and*
// the foreground call is either ALERTING (where you can hear the other
// end ringing) or ACTIVE (when the call is actually connected.) In any
// state *other* than these, the popup should not be visible.
if ((state == PhoneConstants.State.OFFHOOK)
&& (fgCallState == Call.State.ALERTING || fgCallState == Call.State.ACTIVE)) {
// The audio mode popup is allowed to be visible in this state.
// So if it's up, leave it alone.
} else {
// The Audio mode popup isn't relevant in this state, so make sure
// it's not visible.
dismissAudioModePopup(); // safe even if not active
}
}
private boolean okToShowInCallControls() {
// Note that this method is concerned only with the internal state
// of the InCallScreen. (The InCallTouchUi widget has separate
// logic to make sure it's OK to display the touch UI given the
// current telephony state, and that it's allowed on the current
// device in the first place.)
// The touch UI is available in the following InCallScreenModes:
// - NORMAL (obviously)
// - CALL_ENDED (which is intended to look mostly the same as
// a normal in-call state, even though the in-call
// buttons are mostly disabled)
// and is hidden in any of the other modes, like MANAGE_CONFERENCE
// or one of the OTA modes (which use totally different UIs.)
return ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
|| (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED));
}
@Override
public void onClick(View view) {
int id = view.getId();
if (DBG) log("onClick(View " + view + ", id " + id + ")...");
switch (id) {
case R.id.addButton:
case R.id.mergeButton:
case R.id.endButton:
case R.id.dialpadButton:
case R.id.muteButton:
case R.id.holdButton:
case R.id.swapButton:
case R.id.cdmaMergeButton:
case R.id.manageConferenceButton:
// Clicks on the regular onscreen buttons get forwarded
// straight to the InCallScreen.
mInCallScreen.handleOnscreenButtonClick(id);
break;
case R.id.audioButton:
handleAudioButtonClick();
break;
default:
Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
break;
}
}
@Override
public boolean onLongClick(View view) {
final int id = view.getId();
if (DBG) log("onLongClick(View " + view + ", id " + id + ")...");
switch (id) {
case R.id.addButton:
case R.id.mergeButton:
case R.id.dialpadButton:
case R.id.muteButton:
case R.id.holdButton:
case R.id.swapButton:
case R.id.audioButton: {
final CharSequence description = view.getContentDescription();
if (!TextUtils.isEmpty(description)) {
// Show description as ActionBar's menu buttons do.
// See also ActionMenuItemView#onLongClick() for the original implementation.
final Toast cheatSheet =
Toast.makeText(view.getContext(), description, Toast.LENGTH_SHORT);
cheatSheet.setGravity(
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, view.getHeight());
cheatSheet.show();
}
return true;
}
default:
Log.w(LOG_TAG, "onLongClick() with unexpected View " + view + ". Ignoring it.");
break;
}
return false;
}
/**
* Updates the enabledness and "checked" state of the buttons on the
* "inCallControls" panel, based on the current telephony state.
*/
private void updateInCallControls(CallManager cm) {
int phoneType = cm.getActiveFgCall().getPhone().getPhoneType();
// Note we do NOT need to worry here about cases where the entire
// in-call touch UI is disabled, like during an OTA call or if the
// dtmf dialpad is up. (That's handled by updateState(), which
// calls okToShowInCallControls().)
//
// If we get here, it *is* OK to show the in-call touch UI, so we
// now need to update the enabledness and/or "checked" state of
// each individual button.
//
// The InCallControlState object tells us the enabledness and/or
// state of the various onscreen buttons:
InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
if (DBG) {
log("updateInCallControls()...");
inCallControlState.dumpState();
}
// "Add" / "Merge":
// These two buttons occupy the same space onscreen, so at any
// given point exactly one of them must be VISIBLE and the other
// must be GONE.
if (inCallControlState.canAddCall) {
mAddButton.setVisibility(View.VISIBLE);
mAddButton.setEnabled(true);
mMergeButton.setVisibility(View.GONE);
} else if (inCallControlState.canMerge) {
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
// In CDMA "Add" option is always given to the user and the
// "Merge" option is provided as a button on the top left corner of the screen,
// we always set the mMergeButton to GONE
mMergeButton.setVisibility(View.GONE);
} else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
mMergeButton.setVisibility(View.VISIBLE);
mMergeButton.setEnabled(true);
mAddButton.setVisibility(View.GONE);
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
} else {
// Neither "Add" nor "Merge" is available. (This happens in
// some transient states, like while dialing an outgoing call,
// and in other rare cases like if you have both lines in use
// *and* there are already 5 people on the conference call.)
// Since the common case here is "while dialing", we show the
// "Add" button in a disabled state so that there won't be any
// jarring change in the UI when the call finally connects.
mAddButton.setVisibility(View.VISIBLE);
mAddButton.setEnabled(false);
mMergeButton.setVisibility(View.GONE);
}
if (inCallControlState.canAddCall && inCallControlState.canMerge) {
if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
|| (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
// Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
// should both be available right now. This *should* never
// happen with GSM, but if it's possible on any
// future devices we may need to re-layout Add and Merge so
// they can both be visible at the same time...
Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
" but can't show both!");
} else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
// In CDMA "Add" option is always given to the user and the hence
// in this case both "Add" and "Merge" options would be available to user
if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
} else {
throw new IllegalStateException("Unexpected phone type: " + phoneType);
}
}
// "End call"
mEndButton.setEnabled(inCallControlState.canEndCall);
// "Dialpad": Enabled only when it's OK to use the dialpad in the
// first place.
mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
mDialpadButton.setChecked(inCallControlState.dialpadVisible);
// "Mute"
mMuteButton.setEnabled(inCallControlState.canMute);
mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
// "Audio"
updateAudioButton(inCallControlState);
// "Hold" / "Swap":
// These two buttons occupy the same space onscreen, so at any
// given point exactly one of them must be VISIBLE and the other
// must be GONE.
if (inCallControlState.canHold) {
mHoldButton.setVisibility(View.VISIBLE);
mHoldButton.setEnabled(true);
mHoldButton.setChecked(inCallControlState.onHold);
mSwapButton.setVisibility(View.GONE);
mHoldSwapSpacer.setVisibility(View.VISIBLE);
} else if (inCallControlState.canSwap) {
mSwapButton.setVisibility(View.VISIBLE);
mSwapButton.setEnabled(true);
mHoldButton.setVisibility(View.GONE);
mHoldSwapSpacer.setVisibility(View.VISIBLE);
} else {
// Neither "Hold" nor "Swap" is available. This can happen for two
// reasons:
// (1) this is a transient state on a device that *can*
// normally hold or swap, or
// (2) this device just doesn't have the concept of hold/swap.
//
// In case (1), show the "Hold" button in a disabled state. In case
// (2), remove the button entirely. (This means that the button row
// will only have 4 buttons on some devices.)
if (inCallControlState.supportsHold) {
mHoldButton.setVisibility(View.VISIBLE);
mHoldButton.setEnabled(false);
mHoldButton.setChecked(false);
mSwapButton.setVisibility(View.GONE);
mHoldSwapSpacer.setVisibility(View.VISIBLE);
} else {
mHoldButton.setVisibility(View.GONE);
mSwapButton.setVisibility(View.GONE);
mHoldSwapSpacer.setVisibility(View.GONE);
}
}
mInCallScreen.updateButtonStateOutsideInCallTouchUi();
if (inCallControlState.canSwap && inCallControlState.canHold) {
// Uh oh, the InCallControlState thinks that Swap *and* Hold
// should both be available. This *should* never happen with
// either GSM or CDMA, but if it's possible on any future
// devices we may need to re-layout Hold and Swap so they can
// both be visible at the same time...
Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
}
if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
if (inCallControlState.canSwap && inCallControlState.canMerge) {
// Uh oh, the InCallControlState thinks that Swap *and* Merge
// should both be available. This *should* never happen with
// CDMA, but if it's possible on any future
// devices we may need to re-layout Merge and Swap so they can
// both be visible at the same time...
Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
"enabled, but can't show both!");
}
}
// 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.)
//
// The row contains two buttons:
//
// - "Manage conference" (used only on GSM devices)
// - "Merge" button (used only on CDMA devices)
//
// Note that mExtraButtonRow is ViewStub, which will be inflated for the first time when
// any of its buttons becomes visible.
final boolean showCdmaMerge =
(phoneType == PhoneConstants.PHONE_TYPE_CDMA) && inCallControlState.canMerge;
final boolean showExtraButtonRow =
showCdmaMerge || inCallControlState.manageConferenceVisible;
if (showExtraButtonRow && !inCallControlState.dialpadVisible) {
// This will require the ViewStub inflate itself.
mExtraButtonRow.setVisibility(View.VISIBLE);
// Need to set up mCdmaMergeButton and mManageConferenceButton if this is the first
// time they're visible.
if (mCdmaMergeButton == null) {
setupExtraButtons();
}
mCdmaMergeButton.setVisibility(showCdmaMerge ? View.VISIBLE : View.GONE);
if (inCallControlState.manageConferenceVisible) {
mManageConferenceButton.setVisibility(View.VISIBLE);
mManageConferenceButtonImage.setEnabled(inCallControlState.manageConferenceEnabled);
} else {
mManageConferenceButton.setVisibility(View.GONE);
}
} else {
mExtraButtonRow.setVisibility(View.GONE);
}
if (DBG) {
log("At the end of updateInCallControls().");
dumpBottomButtonState();
}
}
/**
* Set up the buttons that are part of the "extra button row"
*/
private void setupExtraButtons() {
// The two "buttons" here (mCdmaMergeButton and mManageConferenceButton)
// are actually layouts containing an icon and a text label side-by-side.
mCdmaMergeButton = (ViewGroup) mInCallControls.findViewById(R.id.cdmaMergeButton);
if (mCdmaMergeButton == null) {
Log.wtf(LOG_TAG, "CDMA Merge button is null even after ViewStub being inflated.");
return;
}
mCdmaMergeButton.setOnClickListener(this);
mManageConferenceButton =
(ViewGroup) mInCallControls.findViewById(R.id.manageConferenceButton);
mManageConferenceButton.setOnClickListener(this);
mManageConferenceButtonImage =
(ImageButton) mInCallControls.findViewById(R.id.manageConferenceButtonImage);
}
private void dumpBottomButtonState() {
log(" - dialpad: " + getButtonState(mDialpadButton));
log(" - speaker: " + getButtonState(mAudioButton));
log(" - mute: " + getButtonState(mMuteButton));
log(" - hold: " + getButtonState(mHoldButton));
log(" - swap: " + getButtonState(mSwapButton));
log(" - add: " + getButtonState(mAddButton));
log(" - merge: " + getButtonState(mMergeButton));
log(" - cdmaMerge: " + getButtonState(mCdmaMergeButton));
log(" - swap: " + getButtonState(mSwapButton));
log(" - manageConferenceButton: " + getButtonState(mManageConferenceButton));
}
private static String getButtonState(View view) {
if (view == null) {
return "(null)";
}
StringBuilder builder = new StringBuilder();
builder.append("visibility: " + (view.getVisibility() == View.VISIBLE ? "VISIBLE"
: view.getVisibility() == View.INVISIBLE ? "INVISIBLE" : "GONE"));
if (view instanceof ImageButton) {
builder.append(", enabled: " + ((ImageButton) view).isEnabled());
} else if (view instanceof CompoundButton) {
builder.append(", enabled: " + ((CompoundButton) view).isEnabled());
builder.append(", checked: " + ((CompoundButton) view).isChecked());
}
return builder.toString();
}
/**
* Updates the onscreen "Audio mode" button based on the current state.
*
* - If bluetooth is available, this button's function is to bring up the
* "Audio mode" popup (which provides a 3-way choice between earpiece /
* speaker / bluetooth). So it should look like a regular action button,
* but should also have the small "more_indicator" triangle that indicates
* that a menu will pop up.
*
* - If speaker (but not bluetooth) is available, this button should look like
* a regular toggle button (and indicate the current speaker state.)
*
* - If even speaker isn't available, disable the button entirely.
*/
private void updateAudioButton(InCallControlState inCallControlState) {
if (DBG) log("updateAudioButton()...");
// The various layers of artwork for this button come from
// btn_compound_audio.xml. Keep track of which layers we want to be
// visible:
//
// - This selector shows the blue bar below the button icon when
// this button is a toggle *and* it's currently "checked".
boolean showToggleStateIndication = false;
//
// - This is visible if the popup menu is enabled:
boolean showMoreIndicator = false;
//
// - Foreground icons for the button. Exactly one of these is enabled:
boolean showSpeakerOnIcon = false;
boolean showSpeakerOffIcon = false;
boolean showHandsetIcon = false;
boolean showBluetoothIcon = false;
if (inCallControlState.bluetoothEnabled) {
if (DBG) log("- updateAudioButton: 'popup menu action button' mode...");
mAudioButton.setEnabled(true);
// The audio button is NOT a toggle in this state. (And its
// setChecked() state is irrelevant since we completely hide the
// btn_compound_background layer anyway.)
// Update desired layers:
showMoreIndicator = true;
if (inCallControlState.bluetoothIndicatorOn) {
showBluetoothIcon = true;
} else if (inCallControlState.speakerOn) {
showSpeakerOnIcon = true;
} else {
showHandsetIcon = true;
// TODO: if a wired headset is plugged in, that takes precedence
// over the handset earpiece. If so, maybe we should show some
// sort of "wired headset" icon here instead of the "handset
// earpiece" icon. (Still need an asset for that, though.)
}
} else if (inCallControlState.speakerEnabled) {
if (DBG) log("- updateAudioButton: 'speaker toggle' mode...");
mAudioButton.setEnabled(true);
// The audio button *is* a toggle in this state, and indicates the
// current state of the speakerphone.
mAudioButton.setChecked(inCallControlState.speakerOn);
// Update desired layers:
showToggleStateIndication = true;
showSpeakerOnIcon = inCallControlState.speakerOn;
showSpeakerOffIcon = !inCallControlState.speakerOn;
} else {
if (DBG) log("- updateAudioButton: disabled...");
// The audio button is a toggle in this state, but that's mostly
// irrelevant since it's always disabled and unchecked.
mAudioButton.setEnabled(false);
mAudioButton.setChecked(false);
// Update desired layers:
showToggleStateIndication = true;
showSpeakerOffIcon = true;
}
// Finally, update the drawable layers (see btn_compound_audio.xml).
// Constants used below with Drawable.setAlpha():
final int HIDDEN = 0;
final int VISIBLE = 255;
LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
if (DBG) log("- 'layers' drawable: " + layers);
layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
.setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.moreIndicatorItem)
.setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.bluetoothItem)
.setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.handsetItem)
.setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
.setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
.setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
}
/**
* Handles a click on the "Audio mode" button.
* - If bluetooth is available, bring up the "Audio mode" popup
* (which provides a 3-way choice between earpiece / speaker / bluetooth).
* - If bluetooth is *not* available, just toggle between earpiece and
* speaker, with no popup at all.
*/
private void handleAudioButtonClick() {
InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
if (inCallControlState.bluetoothEnabled) {
if (DBG) log("- handleAudioButtonClick: 'popup menu' mode...");
showAudioModePopup();
} else {
if (DBG) log("- handleAudioButtonClick: 'speaker toggle' mode...");
mInCallScreen.toggleSpeaker();
}
}
/**
* Brings up the "Audio mode" popup.
*/
private void showAudioModePopup() {
if (DBG) log("showAudioModePopup()...");
mAudioModePopup = new PopupMenu(mInCallScreen /* context */,
mAudioButton /* anchorView */);
mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
mAudioModePopup.getMenu());
mAudioModePopup.setOnMenuItemClickListener(this);
mAudioModePopup.setOnDismissListener(this);
// Update the enabled/disabledness of menu items based on the
// current call state.
InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
Menu menu = mAudioModePopup.getMenu();
// TODO: Still need to have the "currently active" audio mode come
// up pre-selected (or focused?) with a blue highlight. Still
// need exact visual design, and possibly framework support for this.
// See comments below for the exact logic.
MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
speakerItem.setEnabled(inCallControlState.speakerEnabled);
// TODO: Show speakerItem as initially "selected" if
// inCallControlState.speakerOn is true.
// We display *either* "earpiece" or "wired headset", never both,
// depending on whether a wired headset is physically plugged in.
MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
final boolean usingHeadset = mApp.isHeadsetPlugged();
earpieceItem.setVisible(!usingHeadset);
earpieceItem.setEnabled(!usingHeadset);
wiredHeadsetItem.setVisible(usingHeadset);
wiredHeadsetItem.setEnabled(usingHeadset);
// TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
// as initially "selected" if inCallControlState.speakerOn and
// inCallControlState.bluetoothIndicatorOn are both false.
MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
bluetoothItem.setEnabled(inCallControlState.bluetoothEnabled);
// TODO: Show bluetoothItem as initially "selected" if
// inCallControlState.bluetoothIndicatorOn is true.
mAudioModePopup.show();
// Unfortunately we need to manually keep track of the popup menu's
// visiblity, since PopupMenu doesn't have an isShowing() method like
// Dialogs do.
mAudioModePopupVisible = true;
}
/**
* Dismisses the "Audio mode" popup if it's visible.
*
* This is safe to call even if the popup is already dismissed, or even if
* you never called showAudioModePopup() in the first place.
*/
public void dismissAudioModePopup() {
if (mAudioModePopup != null) {
mAudioModePopup.dismiss(); // safe even if already dismissed
mAudioModePopup = null;
mAudioModePopupVisible = false;
}
}
/**
* Refreshes the "Audio mode" popup if it's visible. This is useful
* (for example) when a wired headset is plugged or unplugged,
* since we need to switch back and forth between the "earpiece"
* and "wired headset" items.
*
* This is safe to call even if the popup is already dismissed, or even if
* you never called showAudioModePopup() in the first place.
*/
public void refreshAudioModePopup() {
if (mAudioModePopup != null && mAudioModePopupVisible) {
// Dismiss the previous one
mAudioModePopup.dismiss(); // safe even if already dismissed
// And bring up a fresh PopupMenu
showAudioModePopup();
}
}
// PopupMenu.OnMenuItemClickListener implementation; see showAudioModePopup()
@Override
public boolean onMenuItemClick(MenuItem item) {
if (DBG) log("- onMenuItemClick: " + item);
if (DBG) log(" id: " + item.getItemId());
if (DBG) log(" title: '" + item.getTitle() + "'");
if (mInCallScreen == null) {
Log.w(LOG_TAG, "onMenuItemClick(" + item + "), but null mInCallScreen!");
return true;
}
switch (item.getItemId()) {
case R.id.audio_mode_speaker:
mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.SPEAKER);
break;
case R.id.audio_mode_earpiece:
case R.id.audio_mode_wired_headset:
// InCallAudioMode.EARPIECE means either the handset earpiece,
// or the wired headset (if connected.)
mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.EARPIECE);
break;
case R.id.audio_mode_bluetooth:
mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.BLUETOOTH);
break;
default:
Log.wtf(LOG_TAG,
"onMenuItemClick: unexpected View ID " + item.getItemId()
+ " (MenuItem = '" + item + "')");
break;
}
return true;
}
// 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
// of the menu items.
@Override
public void onDismiss(PopupMenu menu) {
if (DBG) log("- onDismiss: " + menu);
mAudioModePopupVisible = false;
}
/**
* @return the amount of vertical space (in pixels) that needs to be
* reserved for the button cluster at the bottom of the screen.
* (The CallCard uses this measurement to determine how big
* the main "contact photo" area can be.)
*
* NOTE that this returns the "canonical height" of the main in-call
* button cluster, which may not match the amount of vertical space
* actually used. Specifically:
*
* - If an incoming call is ringing, the button cluster isn't
* visible at all. (And the GlowPadView widget is actually
* much taller than the button cluster.)
*
* - If the InCallTouchUi widget's "extra button row" is visible
* (in some rare phone states) the button cluster will actually
* be slightly taller than the "canonical height".
*
* In either of these cases, we allow the bottom edge of the contact
* photo to be covered up by whatever UI is actually onscreen.
*/
public int getTouchUiHeight() {
// Add up the vertical space consumed by the various rows of buttons.
int height = 0;
// - The main row of buttons:
height += (int) getResources().getDimension(R.dimen.in_call_button_height);
// - The End button:
height += (int) getResources().getDimension(R.dimen.in_call_end_button_height);
// - Note we *don't* consider the InCallTouchUi widget's "extra
// button row" here.
//- And an extra bit of margin:
height += (int) getResources().getDimension(R.dimen.in_call_touch_ui_upper_margin);
return height;
}
//
// GlowPadView.OnTriggerListener implementation
//
@Override
public void onGrabbed(View v, int handle) {
}
@Override
public void onReleased(View v, int handle) {
}
/**
* Handles "Answer" and "Reject" actions for an incoming call.
* We get this callback from the incoming call widget
* when the user triggers an action.
*/
@Override
public void onTrigger(View view, int whichHandle) {
if (DBG) log("onTrigger(whichHandle = " + whichHandle + ")...");
if (mInCallScreen == null) {
Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
+ ") from incoming-call widget, but null mInCallScreen!");
return;
}
// The InCallScreen actually implements all of these actions.
// Each possible action from the incoming call widget corresponds
// to an R.id value; we pass those to the InCallScreen's "button
// click" handler (even though the UI elements aren't actually
// buttons; see InCallScreen.handleOnscreenButtonClick().)
mShowInCallControlsDuringHidingAnimation = false;
switch (whichHandle) {
case ANSWER_CALL_ID:
if (DBG) log("ANSWER_CALL_ID: answer!");
mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
mShowInCallControlsDuringHidingAnimation = true;
// ...and also prevent it from reappearing right away.
// (This covers up a slow response from the radio for some
// actions; see updateState().)
mLastIncomingCallActionTime = SystemClock.uptimeMillis();
break;
case SEND_SMS_ID:
if (DBG) log("SEND_SMS_ID!");
mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
// Watch out: mLastIncomingCallActionTime should not be updated for this case.
//
// The variable is originally for avoiding a problem caused by delayed phone state
// update; RINGING state may remain just after answering/declining an incoming
// call, so we need to wait a bit (500ms) until we get the effective phone state.
// For this case, we shouldn't rely on that hack.
//
// When the user selects this case, there are two possibilities, neither of which
// should rely on the hack.
//
// 1. The first possibility is that, the device eventually sends one of canned
// responses per the user's "send" request, and reject the call after sending it.
// At that moment the code introducing the canned responses should handle the
// case separately.
//
// 2. The second possibility is that, the device will show incoming call widget
// again per the user's "cancel" request, where the incoming call will still
// remain. At that moment the incoming call will keep its RINGING state.
// The remaining phone state should never be ignored by the hack for
// answering/declining calls because the RINGING state is legitimate. If we
// use the hack for answer/decline cases, the user loses the incoming call
// widget, until further screen update occurs afterward, which often results in
// missed calls.
break;
case DECLINE_CALL_ID:
if (DBG) log("DECLINE_CALL_ID: reject!");
mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
// Same as "answer" case.
mLastIncomingCallActionTime = SystemClock.uptimeMillis();
break;
default:
Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
break;
}
// On any action by the user, hide the widget.
//
// If requested above (i.e. if mShowInCallControlsDuringHidingAnimation is set to true),
// in-call controls will start being shown too.
//
// TODO: The decision to hide this should be made by the controller
// (InCallScreen), and not this view.
hideIncomingCallWidget();
// Regardless of what action the user did, be sure to clear out
// the hint text we were displaying while the user was dragging.
mInCallScreen.updateIncomingCallWidgetHint(0, 0);
}
public void onFinishFinalAnimation() {
// Not used
}
/**
* Apply an animation to hide the incoming call widget.
*/
private void hideIncomingCallWidget() {
if (DBG) log("hideIncomingCallWidget()...");
if (mIncomingCallWidget.getVisibility() != View.VISIBLE
|| mIncomingCallWidgetIsFadingOut) {
if (DBG) log("Skipping hideIncomingCallWidget action");
// Widget is already hidden or in the process of being hidden
return;
}
// Hide the incoming call screen with a transition
mIncomingCallWidgetIsFadingOut = true;
ViewPropertyAnimator animator = mIncomingCallWidget.animate();
animator.cancel();
animator.setDuration(AnimationUtils.ANIMATION_DURATION);
animator.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (mShowInCallControlsDuringHidingAnimation) {
if (DBG) log("IncomingCallWidget's hiding animation started");
updateInCallControls(mApp.mCM);
mInCallControls.setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (DBG) log("IncomingCallWidget's hiding animation ended");
mIncomingCallWidget.setAlpha(1);
mIncomingCallWidget.setVisibility(View.GONE);
mIncomingCallWidget.animate().setListener(null);
mShowInCallControlsDuringHidingAnimation = false;
mIncomingCallWidgetIsFadingOut = false;
mIncomingCallWidgetShouldBeReset = true;
}
@Override
public void onAnimationCancel(Animator animation) {
mIncomingCallWidget.animate().setListener(null);
mShowInCallControlsDuringHidingAnimation = false;
mIncomingCallWidgetIsFadingOut = false;
mIncomingCallWidgetShouldBeReset = true;
// Note: the code which reset this animation should be responsible for
// alpha and visibility.
}
});
animator.alpha(0f);
}
/**
* Shows the incoming call widget and cancels any animation that may be fading it out.
*/
private void showIncomingCallWidget(Call ringingCall) {
if (DBG) log("showIncomingCallWidget()...");
// TODO: wouldn't be ok to suppress this whole request if the widget is already VISIBLE
// and we don't need to reset it?
// log("showIncomingCallWidget(). widget visibility: " + mIncomingCallWidget.getVisibility());
ViewPropertyAnimator animator = mIncomingCallWidget.animate();
if (animator != null) {
animator.cancel();
}
mIncomingCallWidget.setAlpha(1.0f);
// Update the GlowPadView widget's targets based on the state of
// the ringing call. (Specifically, we need to disable the
// "respond via SMS" option for certain types of calls, like SIP
// addresses or numbers with blocked caller-id.)
final boolean allowRespondViaSms =
RespondViaSmsManager.allowRespondViaSmsForCall(mInCallScreen, ringingCall);
final int targetResourceId = allowRespondViaSms
? R.array.incoming_call_widget_3way_targets
: R.array.incoming_call_widget_2way_targets;
// The widget should be updated only when appropriate; if the previous choice can be reused
// for this incoming call, we'll just keep using it. Otherwise we'll see UI glitch
// everytime when this method is called during a single incoming call.
if (targetResourceId != mIncomingCallWidget.getTargetResourceId()) {
if (allowRespondViaSms) {
// The GlowPadView widget is allowed to have all 3 choices:
// Answer, Decline, and Respond via SMS.
mIncomingCallWidget.setTargetResources(targetResourceId);
mIncomingCallWidget.setTargetDescriptionsResourceId(
R.array.incoming_call_widget_3way_target_descriptions);
mIncomingCallWidget.setDirectionDescriptionsResourceId(
R.array.incoming_call_widget_3way_direction_descriptions);
} else {
// You only get two choices: Answer or Decline.
mIncomingCallWidget.setTargetResources(targetResourceId);
mIncomingCallWidget.setTargetDescriptionsResourceId(
R.array.incoming_call_widget_2way_target_descriptions);
mIncomingCallWidget.setDirectionDescriptionsResourceId(
R.array.incoming_call_widget_2way_direction_descriptions);
}
// This will be used right after this block.
mIncomingCallWidgetShouldBeReset = true;
}
if (mIncomingCallWidgetShouldBeReset) {
// Watch out: be sure to call reset() and setVisibility() *after*
// updating the target resources, since otherwise the GlowPadView
// widget will make the targets visible initially (even before you
// touch the widget.)
mIncomingCallWidget.reset(false);
mIncomingCallWidgetShouldBeReset = false;
}
// On an incoming call, if the layout is landscape, then align the "incoming call" text
// to the left, because the incomingCallWidget (black background with glowing ring)
// is aligned to the right and would cover the "incoming call" text.
// Note that callStateLabel is within CallCard, outside of the context of InCallTouchUi
if (PhoneUtils.isLandscape(this.getContext())) {
TextView callStateLabel = (TextView) mIncomingCallWidget
.getRootView().findViewById(R.id.callStateLabel);
if (callStateLabel != null) callStateLabel.setGravity(Gravity.LEFT);
}
mIncomingCallWidget.setVisibility(View.VISIBLE);
// Finally, manually trigger a "ping" animation.
//
// Normally, the ping animation is triggered by RING events from
// the telephony layer (see onIncomingRing().) But that *doesn't*
// happen for the very first RING event of an incoming call, since
// the incoming-call UI hasn't been set up yet at that point!
//
// So trigger an explicit ping() here, to force the animation to
// run when the widget first appears.
//
mHandler.removeMessages(INCOMING_CALL_WIDGET_PING);
mHandler.sendEmptyMessageDelayed(
INCOMING_CALL_WIDGET_PING,
// Visual polish: add a small delay here, to make the
// GlowPadView widget visible for a brief moment
// *before* starting the ping animation.
// This value doesn't need to be very precise.
250 /* msec */);
}
/**
* Handles state changes of the incoming-call widget.
*
* In previous releases (where we used a SlidingTab widget) we would
* display an onscreen hint depending on which "handle" the user was
* dragging. But we now use a GlowPadView widget, which has only
* one handle, so for now we don't display a hint at all (see the TODO
* comment below.)
*/
@Override
public void onGrabbedStateChange(View v, int grabbedState) {
if (mInCallScreen != null) {
// Look up the hint based on which handle is currently grabbed.
// (Note we don't simply pass grabbedState thru to the InCallScreen,
// since *this* class is the only place that knows that the left
// handle means "Answer" and the right handle means "Decline".)
int hintTextResId, hintColorResId;
switch (grabbedState) {
case GlowPadView.OnTriggerListener.NO_HANDLE:
case GlowPadView.OnTriggerListener.CENTER_HANDLE:
hintTextResId = 0;
hintColorResId = 0;
break;
default:
Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
+ grabbedState);
hintTextResId = 0;
hintColorResId = 0;
break;
}
// Tell the InCallScreen to update the CallCard and force the
// screen to redraw.
mInCallScreen.updateIncomingCallWidgetHint(hintTextResId, hintColorResId);
}
}
/**
* Handles an incoming RING event from the telephony layer.
*/
public void onIncomingRing() {
if (ENABLE_PING_ON_RING_EVENTS) {
// Each RING from the telephony layer triggers a "ping" animation
// of the GlowPadView widget. (The intent here is to make the
// pinging appear to be synchronized with the ringtone, although
// that only works for non-looping ringtones.)
triggerPing();
}
}
/**
* Runs a single "ping" animation of the GlowPadView widget,
* or do nothing if the GlowPadView widget is no longer visible.
*
* Also, if ENABLE_PING_AUTO_REPEAT is true, schedule the next ping as
* well (but again, only if the GlowPadView widget is still visible.)
*/
public void triggerPing() {
if (DBG) log("triggerPing: mIncomingCallWidget = " + mIncomingCallWidget);
if (!mInCallScreen.isForegroundActivity()) {
// InCallScreen has been dismissed; no need to run a ping *or*
// schedule another one.
log("- triggerPing: InCallScreen no longer in foreground; ignoring...");
return;
}
if (mIncomingCallWidget == null) {
// This shouldn't happen; the GlowPadView widget should
// always be present in our layout file.
Log.w(LOG_TAG, "- triggerPing: null mIncomingCallWidget!");
return;
}
if (DBG) log("- triggerPing: mIncomingCallWidget visibility = "
+ mIncomingCallWidget.getVisibility());
if (mIncomingCallWidget.getVisibility() != View.VISIBLE) {
if (DBG) log("- triggerPing: mIncomingCallWidget no longer visible; ignoring...");
return;
}
// Ok, run a ping (and schedule the next one too, if desired...)
mIncomingCallWidget.ping();
if (ENABLE_PING_AUTO_REPEAT) {
// Schedule the next ping. (ENABLE_PING_AUTO_REPEAT mode
// allows the ping animation to repeat much faster than in
// the ENABLE_PING_ON_RING_EVENTS case, since telephony RING
// events come fairly slowly (about 3 seconds apart.))
// No need to check here if the call is still ringing, by
// the way, since we hide mIncomingCallWidget as soon as the
// ringing stops, or if the user answers. (And at that
// point, any future triggerPing() call will be a no-op.)
// TODO: Rather than having a separate timer here, maybe try
// having these pings synchronized with the vibrator (see
// VibratorThread in Ringer.java; we'd just need to get
// events routed from there to here, probably via the
// PhoneApp instance.) (But watch out: make sure pings
// still work even if the Vibrate setting is turned off!)
mHandler.sendEmptyMessageDelayed(INCOMING_CALL_WIDGET_PING,
PING_AUTO_REPEAT_DELAY_MSEC);
}
}
// Debugging / testing code
private void log(String msg) {
Log.d(LOG_TAG, msg);
}
}