Skip to content

Commit

Permalink
First pass at "respond via SMS" UI for incoming calls
Browse files Browse the repository at this point in the history
Here's an initial ultra-simple implementation of the "respond via SMS"
feature.  The exact UX design and product requirements haven't been
thought through fully yet, so this change implements just the most basic
functionality in order for droidfooders to start trying it (and
hopefully help shake out the rest of the requirements for this feature.)

In this change:

- Handle the SEND_SMS_ID trigger from the MultiWaveView widget by
  bringing up a popup menu of "respond via SMS" actions.

- The actions contain a few canned responses, along with a "Custom
  message..." item.  The canned responses are non-customizable for now
  (see the <menu> in respond_via_sms.xml).  The "Custom" item brings up
  the regular SMS compose UI.

- Almost all of this implementation is found in the new file
  RespondViaSms.java.  That contains a PopupMenu subclass to display the
  choices, a class called RespondViaSmsClickListener that handles the
  various actions, and a static method called showRespondViaSmsPopup()
  that starts the whole sequence.

- Code cleanup in a few places

Still-open design issues:
  - Allow user to manage or customize the canned messages?
  - For the "Custom..." choice, is it OK to make the user unlock their
    device right at that point?
  - When you first select the "SMS" choice from the MultiWaveView
    widget, exactly what do we do with the incoming call?  reject it?
    just silence the ringer?  Do nothing till you've actually picked one
    of the choices from the popup menu?
  - What happens if the user dismisses the popup by tapping outside the
    menu or pressing Back?  Bring back the MultiWaveView widget? Restart
    the ringer too? Or just dismiss the whole incoming call screen?
    (Also, should the popup menu have an explicit "Cancel" choice?)
  - All visual design and exact layout in this change is provisional

Known missing pieces / bugs with this change (besides the above):

  - We're sending texts using the old SmsManager.sendTextMessage() API,
    but we should switch to the new SENDTO_NO_CONFIRMATION intent once
    change https://android-git.corp.google.com/g/114664 gets checked in.

  - Need to disable the "respond via SMS" option entirely in a few cases
    (see TODO in showIncomingCallWidget())

  - Some bad interaction with the non-secure keyguard when using "Custom
    message..." (see TODO in launchSmsCompose())

Bug: 4598484
Change-Id: Id7ebd25dfc7d62cfac16ecc077d7e9f059b807db
  • Loading branch information
David Brown committed Jun 21, 2011
1 parent a232063 commit 9e25b78
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 59 deletions.
1 change: 1 addition & 0 deletions AndroidManifest.xml
Expand Up @@ -55,6 +55,7 @@
<uses-permission android:name="android.permission.STATUS_BAR" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.SET_TIME_ZONE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Expand Down
26 changes: 26 additions & 0 deletions res/layout/incall_touch_ui.xml
Expand Up @@ -50,6 +50,32 @@
android:horizontalOffset="0dip"
android:verticalOffset="60dip"
/>

<!-- Placeholder view used to anchor the "Respond via SMS" popup menu. -->
<!-- TODO: Ideally this would be positioned as
android:layout_above="@id/incomingCallWidget"
along with a layout_marginBottom value equal to the height of the popup
menu. But the incomingCallWidget can potentially disappear while the
menu is up, which would cause the menu to jump.
So for now, just used a fixed distance from the top of the screen.
(The visual design hasn't been fully specced yet anyway.) -->
<View
android:id="@+id/popupMenuAnchor"
android:layout_width="match_parent"
android:layout_height="20dip"
android:layout_alignParentTop="true"
android:gravity="center"
android:layout_marginTop="40dip"
android:layout_marginLeft="30dip"
android:layout_marginRight="30dip"
/>
<!-- Note: to make layout debugging easier, temporarily make this into
a TextView with
android:text="@string/menu_answer"
android:textColor="#FD3D"
android:background="#F444"
to make it easy to see how this view affects the placement of the popup. -->

<!--
(2) inCallControls: the widgets visible while a regular call
(or calls) is in progress
Expand Down
36 changes: 36 additions & 0 deletions res/menu/respond_via_sms.xml
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 Google Inc.
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.
-->

<!-- Menu of choices for the "respond via SMS" feature for incoming calls. -->

<!-- TODO: still need product / UX decision about whether the user will be
allowed to to manage or customize the canned messages. For now, keep
these messages here in a static <menu>. (But I'll probably need to
build the menu dynamically at some point, which means that these
canned messages will probably move over to res/values/array.xml.) -->

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/canned_message_1"
android:title="Can't talk now. What's up?" />
<item android:id="@+id/canned_message_2"
android:title="I'll call you right back." />
<item android:id="@+id/canned_message_3"
android:title="I'll call you later." />
<item android:id="@+id/canned_message_4"
android:title="Can't talk now. Call me later?" />
<item android:id="@+id/custom_message"
android:title="Custom message..." />
</menu>
15 changes: 8 additions & 7 deletions res/values/ids.xml
Expand Up @@ -4,9 +4,9 @@
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.
Expand All @@ -31,9 +31,10 @@
<item type="id" name="menuAnswer" />
<item type="id" name="menuIgnore" />

<!-- IDs used with events from some in-call touch UI elements;
we treat these as "button clicks" even though the UI elements
themselves may not actually be buttons. -->
<item type="id" name="answerButton" />
<item type="id" name="rejectButton" />
<!-- IDs used with events from in-call touch UI elements.
(Note we handle these in the InCallScreen's "button click"
handler even though the UI elements may not actually be buttons.) -->
<item type="id" name="incomingCallAnswer" />
<item type="id" name="incomingCallReject" />
<item type="id" name="incomingCallRespondViaSms" />
</resources>
85 changes: 71 additions & 14 deletions src/com/android/phone/InCallScreen.java
Expand Up @@ -1521,13 +1521,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ " (PhoneWindowManager should have handled this key.)");
// But go ahead and handle the key as normal, since the
// PhoneWindowManager presumably did NOT handle it:

final CallNotifier notifier = mApp.notifier;
if (notifier.isRinging()) {
// ringer is actually playing, so silence it.
if (DBG) log("VOLUME key: silence ringer");
notifier.silenceRinger();
}
internalSilenceRinger();

// As long as an incoming call is ringing, we always
// consume the VOLUME keys.
Expand Down Expand Up @@ -2901,12 +2895,15 @@ private void onShowHideDialpad() {
// item *or* touch button) and does the appropriate user action.

// Actions while an incoming call is ringing:
case R.id.answerButton:
case R.id.incomingCallAnswer:
internalAnswerCall();
break;
case R.id.rejectButton:
case R.id.incomingCallReject:
internalHangupRingingCall();
break;
case R.id.incomingCallRespondViaSms:
internalRespondViaSms();
break;

// The other regular (single-tap) buttons used while in-call:
case R.id.holdButton:
Expand Down Expand Up @@ -3410,7 +3407,7 @@ private void dismissAllDialogs() {
* Answer a ringing call. This method does nothing if there's no
* ringing or waiting call.
*/
/* package */ void internalAnswerCall() {
private void internalAnswerCall() {
// if (DBG) log("internalAnswerCall()...");
// if (DBG) PhoneUtils.dumpCallState(mPhone);

Expand Down Expand Up @@ -3458,7 +3455,7 @@ private void dismissAllDialogs() {
/**
* Answer the ringing call *and* hang up the ongoing call.
*/
/* package */ void internalAnswerAndEnd() {
private void internalAnswerAndEnd() {
if (DBG) log("internalAnswerAndEnd()...");
if (VDBG) PhoneUtils.dumpCallManager();
// In the rare case when multiple calls are ringing, the UI policy
Expand All @@ -3469,18 +3466,78 @@ private void dismissAllDialogs() {
/**
* Hang up the ringing call (aka "Don't answer").
*/
/* package */ void internalHangupRingingCall() {
private void internalHangupRingingCall() {
if (DBG) log("internalHangupRingingCall()...");
if (VDBG) PhoneUtils.dumpCallManager();
// In the rare case when multiple calls are ringing, the UI policy
// it to always act on the first ringing call.v
// it to always act on the first ringing call.
PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
}

/**
* Silence the ringer (if an incoming call is ringing.)
*/
private void internalSilenceRinger() {
if (DBG) log("internalSilenceRinger()...");
final CallNotifier notifier = mApp.notifier;
if (notifier.isRinging()) {
// ringer is actually playing, so silence it.
notifier.silenceRinger();
}
}

/**
* Respond via SMS to the ringing call.
* @see RespondViaSms
*/
private void internalRespondViaSms() {
if (DBG) log("internalRespondViaSms()...");
if (VDBG) PhoneUtils.dumpCallManager();

// In the rare case when multiple calls are ringing, the UI policy
// it to always act on the first ringing call.
Call ringingCall = mCM.getFirstActiveRingingCall();

View anchorView = mInCallTouchUi.findViewById(R.id.popupMenuAnchor);

RespondViaSms.showRespondViaSmsPopup(this, ringingCall, anchorView);

// TODO: still need to decide exactly what to do with the incoming call.
//
// (1) definitely silence the ringer
// (2) but should we immediately reject the incoming call?
// or just ignore it and let it go to voicemail eventually?
// (3) also, should the code in InCallTouchUi dismiss the
// MultiWaveView widget right now, or leave it up with the
// popup menu on top, perhaps to show some visual context for
// how we got here?
//
// Or, maybe we should do nothing here, and instead wait
// till you've actually picked one of the choices from the
// popup menu (before we silence the ringer). That would
// allow you to change your mind (i.e. dismiss the popup
// menu, and then select either "answer" or "reject" from
// the incoming-call widget.)
//
// Also, issues if the incoming call disconnects on its own:
// - We should probably make sure to *not* exit the
// incoming-call screen immediately if the incoming call
// disconnects but the SMS popup is still visible (to give the
// user a little extra time to decide which response to use.)
// - But OTOH we *should* eventually time out if you bring up
// the popup but don't do anything.
// - We should also take down the popup (if it was up) if you
// press Power to turn the screen off, or if the screen times
// out on its own.
//
// For now:
internalSilenceRinger();
}

/**
* Hang up the current active call.
*/
/* package */ void internalHangup() {
private void internalHangup() {
if (DBG) log("internalHangup()...");
PhoneUtils.hangup(mCM);
}
Expand Down
85 changes: 47 additions & 38 deletions src/com/android/phone/InCallTouchUi.java
Expand Up @@ -531,61 +531,51 @@ 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.
*
* To answer or reject the incoming call, we call
* InCallScreen.handleOnscreenButtonClick() and pass one of the
* special "virtual button" IDs:
* - R.id.answerButton to answer the call
* or
* - R.id.rejectButton to reject the call.
*/
public void onTrigger(View v, int whichHandle) {
log("onDialTrigger(whichHandle = " + whichHandle + ")...");
if (DBG) log("onDialTrigger(whichHandle = " + whichHandle + ")...");

switch (whichHandle) {
case ANSWER_CALL_ID:
if (DBG) log("ANSWER_CALL_ID: answer!");
// On any action by the user, hide the widget:
hideIncomingCallWidget();

hideIncomingCallWidget();
// ...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();

// ...and also prevent it from reappearing right away.
// (This covers up a slow response from the radio; see updateState().)
mLastIncomingCallActionTime = SystemClock.uptimeMillis();
// 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().)

// Do the appropriate action.
if (mInCallScreen != null) {
// Send this to the InCallScreen as a virtual "button click" event:
mInCallScreen.handleOnscreenButtonClick(R.id.answerButton);
} else {
Log.e(LOG_TAG, "answer trigger: mInCallScreen is null");
}
if (mInCallScreen == null) {
Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
+ ") from incoming-call widget, but null mInCallScreen!");
return;
}
switch (whichHandle) {
case ANSWER_CALL_ID:
if (DBG) log("ANSWER_CALL_ID: answer!");
mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
break;

case SEND_SMS_ID:
// TODO
if (DBG) log("SEND_SMS_ID!");

// TODO: maybe *don't* hideIncomingCallWidget() in this
// case? (Exact UI spec is still TBD.)

mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
break;

case DECLINE_CALL_ID:
if (DBG) log("DECLINE_CALL_ID: reject!");

hideIncomingCallWidget();

// ...and also prevent it from reappearing right away.
// (This covers up a slow response from the radio; see updateState().)
mLastIncomingCallActionTime = SystemClock.uptimeMillis();

// Do the appropriate action.
if (mInCallScreen != null) {
// Send this to the InCallScreen as a virtual "button click" event:
mInCallScreen.handleOnscreenButtonClick(R.id.rejectButton);
} else {
Log.e(LOG_TAG, "reject trigger: mInCallScreen is null");
}
mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
break;

default:
Log.e(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
break;
}

Expand Down Expand Up @@ -636,6 +626,25 @@ private void showIncomingCallWidget() {
}
mIncomingCallWidget.reset(false);
mIncomingCallWidget.setVisibility(View.VISIBLE);

// TODO: may need update or reconfigure the MultiWaveView widget
// at this point based on the state of the ringing call.
//
// Specifically, we probably need to disable the "respond via SMS"
// option in a few cases:
//
// - if the ringing call's getAddress() is blank, or not a valid
// phone number
//
// - if the ringing call's getAddress() is a SIP address rather
// than a PSTN number. (Or do we really need to disable this
// feature for SIP addresses? Could there be some SIP-specific
// equivalent to sending a text?)
//
// - if the ringing call's getNumberPresentation() is anything
// other than PRESENTATION_ALLOWED
//
// (any other cases?)
}

/**
Expand Down

0 comments on commit 9e25b78

Please sign in to comment.