package org.sipdroid.sipua.phone;
/*
* Copyright (C) 2009 The Sipdroid Open Source Project
* Copyright (C) 2006 The Android Open Source Project
*
* This file is part of Sipdroid (http://www.sipdroid.org)
*
* Sipdroid is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import org.sipdroid.sipua.ui.InCallScreen;
import org.sipdroid.sipua.ui.Receiver;
import com.neugent.aethervoice.R;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.view.ViewTreeObserver;
/**
* Helper class to manage the sliding "call card" on the InCallScreen.
*/
public class SlidingCardManager implements ViewTreeObserver.OnGlobalLayoutListener {
private static final String LOG_TAG = "PHONE/SlidingCardManager";
private static final boolean DBG = false;
//
// In the new "simplified" sliding card UI, the card is always in
// one of the following states:
//
// CARD AT TOP OF SCREEN:
// - "In call" states
// (including "Dialing", "On hold", "Conference call", "Busy")
//
// CARD AT BOTTOM OF SCREEN:
// - Incoming call (either normal *or* "Call waiting")
// - "Call ended" state:
// - This is a non-interactive state. All touch events are ignored,
// and the card never moves.)
// - This state is only ever used while the Phone is totally idle;
// it's visible for a couple of seconds after a call ends
// (see InCallScreen.delayedCleanupAfterDisconnect().)
//
// Sliding the card UP *always* answers the incoming call.
// (We put the ongoing call on hold if there's already one line in
// use, or hang up the ongoing call if both lines are in use.)
//
// Sliding the card DOWN *always* ends all current calls.
//
// There's no longer any concept of "locking" or "unlocking" the
// touchscreen by sliding the card.
//
/**
* Reference to the InCallScreen activity that owns us. This will be
* null if we haven't been initialized yet *or* after the InCallScreen
* activity has been destroyed.
*/
private InCallScreen mInCallScreen;
private Phone mPhone;
// Touch mode states, and state of the sliding card
private boolean mSlideInProgress = false;
private int mTouchDownY; // Y-coordinate of the DOWN event that started the slide
// mCardAtTop is true if the card should currently be at the top
// of the screen, or false if it should be at the bottom.
private boolean mCardAtTop;
private boolean mCallEndedState;
private int mCardPreferredX, mCardPreferredY;
// UI elements:
private CallCard mCallCard;
// Slide hints
private ViewGroup mSlideUp;
private TextView mSlideUpHint;
private ViewGroup mSlideDown;
private TextView mSlideDownHint;
// Some UI elements from the main InCallScreen that we use.
private ViewGroup mMainFrame;
// Temporary int array used with various getLocationInWindow() calls.
private int[] mTempLocation = new int[2]; // Use this only from the main thread!
//
// Slide hints (portrait mode values come directly from incall_screen.xml):
static final int SLIDE_UP_HINT_TOP_LANDSCAPE = 88;
static final int SLIDE_DOWN_HINT_TOP_LANDSCAPE = 160;
public SlidingCardManager() {
}
/**
* Initializes the internal state of the SlidingCardManager.
*
* @param phone the Phone app's Phone instance
* @param inCallScreen the InCallScreen activity
* @param mainFrame the InCallScreen's main frame containing the in-call UI elements
*/
/* package */ public void init(Phone phone,
InCallScreen inCallScreen,
ViewGroup mainFrame) {
if (DBG) log("init()...");
mPhone = phone;
mInCallScreen = inCallScreen;
mMainFrame = mainFrame;
// Slide hints
mSlideUp = (ViewGroup) mInCallScreen.findViewById(R.id.slideUp);
mSlideUpHint = (TextView) mInCallScreen.findViewById(R.id.slideUpHint);
mSlideDown = (ViewGroup) mInCallScreen.findViewById(R.id.slideDown);
mSlideDownHint = (TextView) mInCallScreen.findViewById(R.id.slideDownHint);
mCallCard = (CallCard) mMainFrame.findViewById(R.id.callCard);
mCallCard.setSlidingCardManager(this);
}
/* package */ void setPhone(Phone phone) {
mPhone = phone;
}
/**
* Null out our reference to the InCallScreen activity.
* This indicates that the InCallScreen activity has been destroyed.
*/
void clearInCallScreenReference() {
mInCallScreen = null;
}
/**
* Updates the PopupWindow's size and makes it visible onscreen.
*
* This needs to be done *after* our main UI gets laid out and
* attached to its Window, because (1) we base the popup's size on the
* post-layout size of elements in our main View hierarchy, and (2) we
* need to have a valid window token for our call to
* mPopup.showAtLocation().
*/
public /* package */ void showPopup() {
if (DBG) log("showPopup()...");
updateCardPreferredPosition(); // Sets mCardAtTop, mCallEndedState,
// mCardPreferredX, and mCardPreferredY
updateCardSlideHints();
}
int height;
/**
* Update the "permanent" position (ie. the actual layout params)
* of the sliding card based on the current call state.
*
* This method sets mCardAtTop, mCallEndedState, mCardPreferredX, and mCardPreferredY.
* It also repositions the PopupWindow if it's showing.
*
* Note that *while sliding* we manually reposition the card
* on every motion event that comes in. The x/y position we set here
* determines where the card should be while *not* sliding.
*
* TODO: If the card position changes for some reason *other*
* than user action (i.e. as a result of an onPhoneStateChanged()
* callback), we should smoothly animate the position change!
* (For example, if you're in a call and the other end hangs up, the
* card should switch to "Call ended" mode and smoothly animate to the
* bottom position.)
*/
public /* package */ void updateCardPreferredPosition() {
if (DBG) log("updateCardPreferredPosition()...");
//if (DBG) log("- card's LayoutParams: " + mCallCard.getLayoutParams());
// Bail out if our View hierarchy isn't attached to a Window yet
// (since the mMainFrame.getLocationOnScreen() call below
// will fail.)
if (mMainFrame.getWindowToken() == null) {
if (DBG) log("updateCardPreferredPosition: View hierarchy unattached; bailing...");
return;
}
if (mMainFrame.getHeight() == 0) {
// The code here needs to know the sizes and positions of some
// views in the mMainFrame view hierarchy, so you're only
// allowed to call this method *after* the whole in-call UI
// has been measured and laid out.
// (This is why we defer calling showPopup() until an
// onGlobalLayout() call comes in.)
throw new IllegalStateException(
"updateCardPreferredPosition: main frame not measured yet");
}
// Given the current state of the Phone and the UI, decide whether
// the card should be at the TOP or BOTTOM of the screen right now.
// Compute the possible coordinates onscreen for the popup.
// TODO: this block is duplicated below; use a single helper method instead.
mMainFrame.getLocationInWindow(mTempLocation);
final int mainFrameX = mTempLocation[0];
final int mainFrameY = 0; //mTempLocation[1];
if (DBG) log("- mMainFrame loc in window: " + mainFrameX + ", " + mainFrameY);
// In the "top" position the CallCard is aligned exactly with the
// top edge of the main frame.
final int popupTopPosY = mainFrameY;
// And in the "bottom" position, the bottom of the CallCard is
// aligned exactly with the bottom of the main frame.
if (height == 0) {
height = mCallCard.getHeight();
// Reposition the "slide hints".
RelativeLayout.LayoutParams lp =
(RelativeLayout.LayoutParams) mSlideUp.getLayoutParams();
// Equivalent to setting android:layout_marginTop in XML
lp.bottomMargin = height;
mSlideUp.setLayoutParams(lp);
lp = (RelativeLayout.LayoutParams) mSlideDown.getLayoutParams();
// Equivalent to setting android:layout_marginTop in XML
lp.topMargin = height;
mSlideDown.setLayoutParams(lp);
height += 10;
}
final int popupBottomPosY = mainFrameY + mMainFrame.getHeight() - height;
if (Receiver.ccCall != null && Receiver.ccCall.getState() != Call.State.DISCONNECTED) {
// When the phone is in use, the position of the card is
// determined solely by whether an incoming call is ringing or
// not.
final boolean hasRingingCall = Receiver.ccCall.getState() == Call.State.INCOMING;
mCardAtTop = !hasRingingCall;
mCallEndedState = false;
} else {
// Phone is completely idle! Display the CALL ENDED state
// with the card at the bottom of the screen.
mCardAtTop = false;
mCallEndedState = true;
}
mCardPreferredX = mainFrameX;
mCardPreferredY = mCardAtTop ? popupTopPosY : popupBottomPosY;
if (DBG) log("==> Setting card preferred position (mCardAtTop = "
+ mCardAtTop + ") to: "
+ mCardPreferredX + ", " + mCardPreferredY);
// This is a no-op if the PopupWindow isn't showing.
mCallCard.update(mCardPreferredX, mCardPreferredY, -1, -1);
}
//
// "Slide hints" management
//
/**
* Update the "slide hints" (displayed onscreen either above or below
* the slidable CallCard) based on the current state.
*/
public /* package */ void updateCardSlideHints() {
if (DBG) log("updateCardSlideHints()...");
if (mSlideInProgress) {
// If currently sliding, do nothing. (Leave the hints in whatever
// state they were before we started the slide.)
if (DBG) log("--> SLIDING: do nothing...");
// Or, to have slide hints always hidden while sliding:
//setSlideHints(0, 0);
return;
}
// Update slide hints based on the current Phone state.
final boolean hasRingingCall = Receiver.ccCall != null && Receiver.ccCall.getState() == Call.State.INCOMING;
int slideUpHint = 0;
int slideDownHint = 0;
if (hasRingingCall) {
slideUpHint = R.string.slide_hint_up_to_answer;
} else {
slideDownHint = R.string.slide_hint_down_to_end_call;
}
setSlideHints(slideUpHint, slideDownHint);
}
/**
* Sets the text of the "slide hints" based on the specified resource
* IDs. (A resource ID of zero means "hide the hint and arrow".)
*/
private void setSlideHints(int upHintResId, int downHintResId) {
// TODO: It would probably look really cool to do a "fade in" animation
// when a hint becomes visible after previously being hidden, rather
// than having it just pop on.
// TODO: Also consider having both slide hints *always* visible,
// so that as you slide you first cover up one and later reveal
// the other. But this is tricky: the text of the hint that
// "starts off hidden" will need to be pre-set to the value it
// should have *after* the slide is complete.
if (DBG) {
String upHint =
(upHintResId != 0) ? mInCallScreen.getString(upHintResId) : "<empty>";
String downHint =
(downHintResId != 0) ? mInCallScreen.getString(downHintResId) : "<empty>";
log("setSlideHints: UP '" + upHint + "', DOWN '" + downHint + "'");
}
mSlideUp.setVisibility((upHintResId != 0) ? View.VISIBLE : View.GONE);
if (upHintResId != 0) mSlideUpHint.setText(upHintResId);
mSlideDown.setVisibility((downHintResId != 0) ? View.VISIBLE : View.GONE);
if (downHintResId != 0) mSlideDownHint.setText(downHintResId);
}
//
// Sliding state management
//
/**
* Handles a touch event on the CallCard.
* @see CallCard.dispatchTouchEvent
*/
/* package */ void handleCallCardTouchEvent(MotionEvent ev) {
// if (DBG) log("handleCallCardTouchEvent(" + ev + ")...");
if (mInCallScreen == null || mInCallScreen.isFinishing()) {
Log.i(LOG_TAG, "handleCallCardTouchEvent: InCallScreen gone; ignoring touch...");
return;
}
final int action = ev.getAction();
// All the sliding code depends on deltas, so it
// doesn't really matter in what coordinate space
// we are, as long as it's independant of our position
final int xAbsolute = (int) ev.getRawX();
final int yAbsolute = (int) ev.getRawY();
if (isSlideInProgress()) {
if (SystemClock.elapsedRealtime()-mTouchDownTime > 1000)
abortSlide();
else
switch (action) {
case MotionEvent.ACTION_DOWN:
// Shouldn't happen in this state.
break;
case MotionEvent.ACTION_MOVE:
// Move the CallCard!
updateWhileSliding(yAbsolute);
break;
case MotionEvent.ACTION_UP:
// See if we've slid far enough to do some action
// (ie. hang up, or answer an incoming call,
// depending on our current state.)
stopSliding(yAbsolute);
break;
case MotionEvent.ACTION_CANCEL:
// Because we set the FLAG_IGNORE_CHEEK_PRESSES
// WindowManager flag (see init()), we'll get an
// ACTION_CANCEL event if a valid ACTION_DOWN is later
// followed by an ACTION_MOVE that's a "fat touch".
// In this case, abort the slide.
if (DBG) log("handleCallCardTouchEvent: ACTION_CANCEL: " + ev);
abortSlide();
break;
}
} else {
switch (action) {
case MotionEvent.ACTION_DOWN:
// This event is a touch DOWN on the card: start sliding!
startSliding(xAbsolute, yAbsolute);
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// If we're not sliding (yet), ignore anything other than
// ACTION_DOWN.
break;
}
}
}
long mTouchDownTime;
/**
* Start sliding the card.
* Called when we get a DOWN touch event on the slideable CallCard.
* x and y are the location of the DOWN event in absolute screen coordinates.
*/
/* package */ void startSliding(int x, int y) {
if (DBG) log("startSliding(" + x + ", " + y + ")...");
if (mCallEndedState) {
if (DBG) log("startSliding: CALL ENDED state; ignoring...");
return;
}
mSlideInProgress = true;
mTouchDownY = y;
mTouchDownTime = SystemClock.elapsedRealtime();
}
/**
* Handle a MOVE event while sliding.
* @param y the y-position of the MOVE event in absolute screen coordinates.
*/
/* package */ void updateWhileSliding(int y) {
int totalSlideAmount = y - mTouchDownY;
// if (DBG) log("--------------> MOTION! y = " + y
// + " (total slide = " + totalSlideAmount + ")");
// TODO: consider caching popupTopPosY and popupBottomPosY in
// startSliding (to save a couple of method calls each time here.)
// TODO: this block is duplicated above; use a single helper method instead.
mMainFrame.getLocationInWindow(mTempLocation);
final int mainFrameX = mTempLocation[0];
final int mainFrameY = 0; //mTempLocation[1];
// In the "top" position the CallCard is aligned exactly with the
// top edge of the main frame.
final int popupTopPosY = mainFrameY;
// And in the "bottom" position, the bottom of the CallCard is
// aligned exactly with the bottom of the main frame.
final int popupBottomPosY = mainFrameY + mMainFrame.getHeight() - height;
// Forcibly reposition the CallCard
int newCardTop = mCardPreferredY + totalSlideAmount;
// if (DBG) log(" ==> New card top: " + newCardTop);
// But never slide *beyond* the topmost or bottom-most position:
if (newCardTop < popupTopPosY) newCardTop = popupTopPosY;
else if (newCardTop > popupBottomPosY) newCardTop = popupBottomPosY;
// Forcibly reposition the PopupWindow.
mCallCard.update(mCardPreferredX, newCardTop, -1, -1);
}
/**
* Stop sliding the card.
* Called when we get an UP touch event while sliding.
*/
/* package */ void stopSliding(int y) {
int totalSlideAmount = y - mTouchDownY;
if (DBG) log("stopSliding: Total slide delta: " + totalSlideAmount);
// When not sliding, the card is pegged to either the very top
// or very bottom of the in-call main frame.
// So the precise slide amount that *should* be required to "complete
// the slide" is the amount of vertical space in the in-call frame NOT
// taken up by the CallCard:
int slideDistanceRequired = mMainFrame.getHeight() - height;
// if (DBG) log(" -> main frame height: " + mMainFrame.getHeight());
// if (DBG) log(" -> PopupWindow height: " + mPopup.getHeight());
// if (DBG) log(" -> DIFFERENCE = " + slideDistanceRequired);
// But fudge slideDistanceRequired a little, in case you slid
// "just far enough" but the card jittered a bit when you let go.
slideDistanceRequired -= 30;
// Modify totalSlideAmount so that a positive value means "a slide
// in the correct direction". (Thus if the card started at the
// bottom of the screen, meaning that the user needs to slide
// *up*, we need to flip the sign of totalSlideAmount.)
if (!mCardAtTop) totalSlideAmount = -totalSlideAmount;
if (totalSlideAmount >= slideDistanceRequired) {
if (DBG) log("==> Slide was far enough! slid "
+ totalSlideAmount + ", needed >= " + slideDistanceRequired);
finishSuccessfulSlide();
} else {
if (DBG) log("==> Didn't slide enough to take action: slid "
+ totalSlideAmount + ", needed >= " + slideDistanceRequired);
abortSlide();
}
}
/**
* The user successfully completed a "slide" operation.
* Activate whatever action the slide was supposed to trigger.
*
* (That could either be (1) hang up the ongoing call(s), or (2)
* answer an incoming call.)
*
* This method is responsible for triggering any screen updates that need
* to happen, based on any internal state changes due to the slide.
*/
private void finishSuccessfulSlide() {
if (DBG) log("finishSuccessfulSlide()...");
mSlideInProgress = false;
// TODO: Need to test lots of possible edge cases here, like if the
// state of the Phone changes while the slide was happening.
// (For example, say the user slid the card UP to answer an incoming
// call, but the phone's no longer ringing by the time we got here...)
// TODO: The state-checking logic here is very similar to the logic in
// updateCardSlideHints(). Rather than duplicating this logic in both
// places, maybe use a single helper function that generates a
// complete "slidability matrix" (ie. all slide hints / states /
// actions) based on the current state of the Phone.
boolean phoneStateAboutToChange = false;
// Perform the "successful slide" action.
if (mCardAtTop) {
// The downward slide action is to hang up any ongoing
// call(s).
if (DBG) log(" =========> Slide complete: HANGING UP...");
mInCallScreen.reject();
// Any "hangup" action is going to cause
// the Phone state to change imminently.
phoneStateAboutToChange = true;
} else {
// The upward slide action is to answer the incoming call.
// (We put the ongoing call on hold if there's already one line in
// use, or hang up the ongoing call if both lines are in use.)
if (DBG) log(" =========> Slide complete: ANSWERING...");
mInCallScreen.answer();
// Either of the "answer call" functions is going to cause
// the Phone state to change imminently.
phoneStateAboutToChange = true;
}
// Finally, update the state of the UI depending on what just happened.
// Update the "permanent" position of the sliding card, and the slide
// hints.
//
// But *don't* do this if we know the Phone state's about to change,
// like if the user just did a "slide up to answer". In that case
// we know we're going to get a onPhoneStateChanged() call in a few
// milliseconds, and *that's* going to result in an updateScreen() call.
// (And if we were to do that update now, we'd just get a brief flash
// of the card at the bottom or the screen. So don't do anything
// here.)
if (!phoneStateAboutToChange) {
updateCardPreferredPosition();
updateCardSlideHints();
// And force an immmediate re-layout. (No need to do any
// animation here, since the card's new "preferred position" is
// exactly where the user just slid it.)
mMainFrame.requestLayout();
}
}
/**
* Bail out of an in-progress slide *without* activating whatever
* action the slide was supposed to trigger.
*/
private void abortSlide() {
if (DBG) log("abortSlide()...");
mSlideInProgress = false;
// This slide had no effect. Nothing about the state of the
// UI has changed, so no need to updateCardPreferredPosition() or
// updateCardSlideHints(). But we *do* need to reposition the
// PopupWindow back in its correct position.
// TODO: smoothly animate back to the preferred position.
mCallCard.update(mCardPreferredX, mCardPreferredY, -1, -1);
}
/* package */ public boolean isSlideInProgress() {
return mSlideInProgress;
}
private void log(String msg) {
Log.d(LOG_TAG, "[" + this + "] " + msg);
}
boolean first = true;
public void onGlobalLayout() {
if (DBG) log("onGlobalLayout()...");
if (first) {
first = false;
showPopup();
}
}
/* package */ public static class WindowAttachNotifierView extends View {
private SlidingCardManager mSlidingCardManager;
public WindowAttachNotifierView(Context c) {
super(c);
}
public void setSlidingCardManager(SlidingCardManager slidingCardManager) {
mSlidingCardManager = slidingCardManager;
}
protected void onAttachedToWindow() {
// This is called when the view is attached to a window.
// At this point it has a Surface and will start drawing.
if (DBG) mSlidingCardManager.log("WindowAttachNotifierView: onAttachedToWindow!");
super.onAttachedToWindow();
// The code in showPopup() needs to know the sizes and
// positions of some views in the mMainFrame view hierarchy,
// in order to set the popup window's size and position. That
// means that showPopup() needs to be called *after* the whole
// in-call UI has been measured and laid out. At this point
// that hasn't happened yet, so we can't directly call
// mSlidingCardManager.showPopup() from here.
//
// Also, to reduce flicker onscreen, we'd like the PopupWindow
// to appear *before* any of the main view hierarchy becomes
// visible. So we use the main view hierarchy's
// ViewTreeObserver to get notified *after* the layout
// happens, but before anything gets drawn.
// Get the ViewTreeObserver for the main InCallScreen view
// hierarchy. (You can only call getViewTreeObserver() after
// the view tree gets attached to a Window, which is why we do
// this here rather than in InCallScreen.onCreate().)
final ViewTreeObserver viewTreeObserver = getViewTreeObserver();
// Arrange for the SlidingCardManager to get called after
// the main view tree has been laid out.
// (addOnPreDrawListener() would also be basically equivalent here.)
viewTreeObserver.addOnGlobalLayoutListener(mSlidingCardManager);
// See SlidingCardManager.onGlobalLayout() for the next step.
}
protected void onDetachedFromWindow() {
// This is called when the view is detached from a window.
// At this point it no longer has a surface for drawing.
if (DBG) mSlidingCardManager.log("WindowAttachNotifierView: onDetachedFromWindow!");
super.onDetachedFromWindow();
// Nothing necessary here (yet) since we already
// dismiss the popup from onDestroy().
}
}
}
|