Temporary "double tap to answer call" UI for Sholes.
David Brown [Wed, 27 May 2009 20:57:36 +0000 (13:57 -0700)]
Since we're about to start dogfooding Sholes devices (which have no hard
send/end buttons), here's an ultra-simple onscreen UI that lets you
double-tap to answer an incoming call.

This is a TEMPORARY HACK until we have the real UI (which will go into
master, and may come from the moto team.)

Implementation notes and TODOs:

- The widget looks pretty ugly; I don't have any assets from Jeff (or
  moto) yet so I'm just using the default button background.
  There's also no visual feedback when you tap the button; that needs to
  be designed too.

- Right now the double-tap-to-answer widget is only used on sholes.
  At some point (once I get a nicer looking asset for the button
  background) I *do* plan to enable it for ALL devices, so more people get
  a chance to try it out.  But in the long term this UI will only be used
  on devices with no hard send/end buttons.

- I check for sholes devices by looking at the ro.product.device system
  property.  This is a temporary hack; in master these onscreen buttons
  will be enabled via a special resource from the vendor/moto overlay,
  but that overlay hierarchy doesn't exist yet.

- The double-tap detection code is a little ugly; I added a TODO about
  extracting it out to a helper class that can listen for double-taps on
  an arbitrary View, or maybe even a whole new "DoubleTapButton" widget.

TESTED:
 - Built dream-eng, incoming call experience is unchanged.
 - Built sholes-eng, the button appears for incoming calls, and
   double-tapping answers the call.  ("Answering" works in the emulator,
   at least.)
 - Made sure I didn't break the "touch lock" overlay for the dialpad.

res/layout/incall_screen.xml
res/layout/onscreen_answer_ui.xml [new file with mode: 0644]
res/values/strings.xml
src/com/android/phone/InCallScreen.java

index 81f5cc1..0ef1bed 100644 (file)
@@ -28,7 +28,6 @@
     <FrameLayout android:id="@+id/mainFrame"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"
-        android:layout_weight="1"
         android:paddingTop="10dip"
         android:paddingLeft="6dip"
         android:paddingRight="6dip"
             />
     </RelativeLayout>
 
+    <!-- Onscreen "answer" UI, used on devices with no hardware CALL
+         button while an incoming call is ringing. -->
+    <ViewStub android:id="@+id/onscreenAnswerUiStub"
+        android:inflatedId="@+id/onscreenAnswerUiContainer"
+        android:layout="@layout/onscreen_answer_ui"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        />
+
 </FrameLayout>
diff --git a/res/layout/onscreen_answer_ui.xml b/res/layout/onscreen_answer_ui.xml
new file mode 100644 (file)
index 0000000..25578b2
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Onscreen "answer" UI: Used while an incoming call is ringing, on devices
+     with no hardware CALL button.
+
+     This view hiererarchy appears as a ViewStub in the InCallScreen's layout,
+     and is inflated only on devices where it's needed.
+
+     TODO: This UI is a TEMPORARY HACK until we have a final
+     design and assets from the UI team. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical"
+    android:visibility="gone"
+    >
+
+    <!-- Blank space on the top 2/3 of the screen -->
+    <View
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:layout_weight="2"
+        android:visibility="invisible"
+        />
+
+    <!-- The button, on the lower 1/3 of the screen -->
+    <!-- TODO: Need a new asset for the background; this
+         one is just a temporary placeholder. -->
+    <TextView android:id="@+id/onscreenAnswerButton"
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:layout_weight="1"
+        android:gravity="center"
+        android:layout_centerHorizontal="true"
+        android:layout_marginLeft="50dip"
+        android:layout_marginRight="50dip"
+        android:layout_marginBottom="20dip"
+        android:text="@string/onscreenAnswerText"
+        android:textAppearance="?android:attr/textAppearanceLargeInverse"
+        android:background="@*android:drawable/btn_default_normal"
+        />
+
+</LinearLayout>
index 2353778..615236d 100644 (file)
          screen. -->
     <string name="touchLockText">Tap twice\nto unlock</string>
 
+    <!-- Text for the onscreen "Answer" button, instructing the user that
+         they need to double-tap to answer the incoming call. -->
+    <string name="onscreenAnswerText">Tap twice\nto answer</string>
+
     <!-- Menu item label in SIM Contacts: Import a single contact entry from the SIM card -->
     <string name="importSimEntry">Import</string>
     <!-- Menu item label in SIM Contacts: Import all contact entries from the SIM card -->
index 2e30ac8..401ef97 100644 (file)
@@ -56,6 +56,7 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.animation.Animation;
@@ -222,6 +223,11 @@ public class InCallScreen extends Activity
     private Animation mTouchLockFadeIn;
     private long mTouchLockLastTouchTime;  // in SystemClock.uptimeMillis() time base
 
+    // Onscreen "answer" UI, for devices with no hardware CALL button.
+    private View mOnscreenAnswerUiContainer;  // The container for the whole UI, or null if unused
+    private View mOnscreenAnswerButton;  // The "answer" button itself
+    private long mOnscreenAnswerButtonLastTouchTime;  // in SystemClock.uptimeMillis() time base
+
     // Various dialogs we bring up (see dismissAllDialogs())
     // The MMI started dialog can actually be one of 2 items:
     //   1. An alert dialog if the MMI code is a normal MMI
@@ -956,6 +962,9 @@ public class InCallScreen extends Activity
         // Menu Button hint
         mMenuButtonHint = (TextView) findViewById(R.id.menuButtonHint);
 
+        // Other platform-specific UI initialization.
+        initOnscreenAnswerUi();
+
         // Make any final updates to our View hierarchy that depend on the
         // current configuration.
         ConfigurationHelper.applyConfigurationToLayout(this);
@@ -1867,6 +1876,7 @@ public class InCallScreen extends Activity
         if (VDBG) log("- updateScreen: updating the in-call UI...");
         mCallCard.updateState(mPhone);
         updateDialpadVisibility();
+        updateOnscreenAnswerUi();
         updateMenuButtonHint();
     }
 
@@ -3270,6 +3280,58 @@ public class InCallScreen extends Activity
         return !ConfigurationHelper.isLandscape() && okToDialDTMFTones();
     }
 
+
+    /**
+     * Initializes the onscreen "answer" UI on devices that need it.
+     */
+    private void initOnscreenAnswerUi() {
+        // This UI is only used on devices with no hard CALL or SEND button.
+
+        // TODO: For now, explicitly enable this for sholes devices.
+        // (Note PRODUCT_DEVICE is "sholes" for both sholes and voles builds.)
+        boolean allowOnscreenAnswerUi =
+                "sholes".equals(SystemProperties.get("ro.product.device"));
+        //
+        // TODO: But ultimately we should either (a) use some framework API
+        // to detect whether the current device has a hard SEND button, or
+        // (b) have this depend on a product-specific resource flag in
+        // config.xml, like the forthcoming "is_full_touch_ui" boolean.
+
+        if (DBG) log("initOnscreenAnswerUi: device '" + SystemProperties.get("ro.product.device")
+                     + "', allowOnscreenAnswerUi = " + allowOnscreenAnswerUi);
+
+        if (allowOnscreenAnswerUi) {
+            ViewStub stub = (ViewStub) findViewById(R.id.onscreenAnswerUiStub);
+            mOnscreenAnswerUiContainer = stub.inflate();
+
+            mOnscreenAnswerButton = findViewById(R.id.onscreenAnswerButton);
+            mOnscreenAnswerButton.setOnTouchListener(this);
+        }
+    }
+
+    /**
+     * Updates the visibility of the onscreen "answer" UI.  On devices
+     * with no hardware CALL button, this UI becomes visible while an
+     * incoming call is ringing.
+     *
+     * TODO: This method should eventually be rolled into a more general
+     * method to update *all* onscreen UI elements that need to be
+     * different on different devices (depending on which hard buttons are
+     * present and/or if we don't have to worry about false touches while
+     * in-call.)
+     */
+    private void updateOnscreenAnswerUi() {
+        if (mOnscreenAnswerUiContainer != null) {
+            if (mPhone.getState() == Phone.State.RINGING) {
+                // A phone call is ringing *or* call waiting.
+                mOnscreenAnswerUiContainer.setVisibility(View.VISIBLE);
+            } else {
+                mOnscreenAnswerUiContainer.setVisibility(View.GONE);
+            }
+        }
+    }
+
+
     /**
      * Helper class to manage the (small number of) manual layout and UI
      * changes needed by the in-call UI when switching between landscape
@@ -3768,77 +3830,137 @@ public class InCallScreen extends Activity
     public boolean onTouch(View v, MotionEvent event) {
         if (VDBG) log ("onTouch(View " + v + ")...");
 
-        //
         // Handle touch events on the "touch lock" overlay.
-        // (v == mTouchLockIcon) means the user hit the lock icon in the
-        // middle of the screen, and (v == mTouchLockOverlay) is a touch
-        // anywhere else on the overlay.
-        //
+        if ((v == mTouchLockIcon) || (v == mTouchLockOverlay)) {
+
+            // TODO: move this big hunk of code to a helper function, or
+            // even better out to a separate helper class containing all
+            // the touch lock overlay code.
+
+            // We only care about these touches while the touch lock UI is
+            // visible (including the time during the fade-in animation.)
+            if (!isTouchLocked()) {
+                // Got an event from the touch lock UI, but we're not locked!
+                // (This was probably a touch-UP right after we unlocked.
+                // Ignore it.)
+                return false;
+            }
 
-        // We only care about touch events while the touch lock UI is
-        // visible (including the time during the fade-in animation.)
-        if (((v == mTouchLockIcon) || (v == mTouchLockOverlay)) && !isTouchLocked()) {
-            // Got an event from the touch lock UI, but we're not locked!
-            // (This was probably a touch-UP right after we unlocked.
-            // Ignore it.)
-            return false;
-        }
+            // (v == mTouchLockIcon) means the user hit the lock icon in the
+            // middle of the screen, and (v == mTouchLockOverlay) is a touch
+            // anywhere else on the overlay.
+
+            if (v == mTouchLockIcon) {
+                // Direct hit on the "lock" icon.  Handle the double-tap gesture.
+                if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                    long now = SystemClock.uptimeMillis();
+                    if (VDBG) log("- touch lock icon: handling a DOWN event, t = " + now);
+
+                    // Look for the double-tap gesture:
+                    if (now < mTouchLockLastTouchTime + ViewConfiguration.getDoubleTapTimeout()) {
+                        if (VDBG) log("==> touch lock icon: DOUBLE-TAP!");
+                        // This was the 2nd tap of a double-tap gesture.
+                        // Take down the touch lock overlay, but post a
+                        // message in the future to bring it back later.
+                        enableTouchLock(false);
+                        resetTouchLockTimer();
+                        // This counts as explicit "user activity".
+                        PhoneApp.getInstance().pokeUserActivity();
+                    }
+                } else if (event.getAction() == MotionEvent.ACTION_UP) {
+                    // Stash away the current time in case this is the first
+                    // tap of a double-tap gesture.  (We measure the time from
+                    // the first tap's UP to the second tap's DOWN.)
+                    mTouchLockLastTouchTime = SystemClock.uptimeMillis();
+                }
+
+                // And regardless of what just happened, we *always* consume
+                // touch events while the touch lock UI is (or was) visible.
+                return true;
+
+            } else {  // (v == mTouchLockOverlay)
+                // User touched the "background" area of the touch lock overlay.
+
+                // TODO: If we're in the middle of the fade-in animation,
+                // consider making a touch *anywhere* immediately unlock the
+                // UI.  This could be risky, though, if the user tries to
+                // *double-tap* during the fade-in (in which case the 2nd tap
+                // might 't become a false touch on the dialpad!)
+                //
+                //if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                //    if (DBG) log("- touch lock overlay background: handling a DOWN event.");
+                //
+                //    if (mTouchLockFadeIn.hasStarted() && !mTouchLockFadeIn.hasEnded()) {
+                //        // If we're still fading-in, a touch *anywhere* onscreen
+                //        // immediately unlocks.
+                //        if (DBG) log("==> touch lock: tap during fade-in!");
+                //
+                //        mTouchLockOverlay.clearAnimation();
+                //        enableTouchLock(false);
+                //        // ...but post a message in the future to bring it
+                //        // back later.
+                //        resetTouchLockTimer();
+                //    }
+                //}
+
+                // And regardless of what just happened, we *always* consume
+                // touch events while the touch lock UI is (or was) visible.
+                return true;
+            }
+
+        // Handle touch events on the onscreen "answer" button.
+        } else if (v == mOnscreenAnswerButton) {
+
+            // TODO: this "double-tap detection" code is also duplicated
+            // above (for mTouchLockIcon).  Instead, extract it out to a
+            // helper class that can listen for double-taps on an
+            // arbitrary View, or maybe even a whole new "DoubleTapButton"
+            // widget.
 
-        if (v == mTouchLockIcon) {
-            // Direct hit on the "lock" icon.  Handle the double-tap gesture.
+            // Look for the double-tap gesture.
             if (event.getAction() == MotionEvent.ACTION_DOWN) {
                 long now = SystemClock.uptimeMillis();
-                if (VDBG) log("- touch lock icon: handling a DOWN event, t = " + now);
+                if (DBG) log("- onscreen answer button: handling a DOWN event, t = " + now);  // foo -- VDBG
 
                 // Look for the double-tap gesture:
-                if (now < mTouchLockLastTouchTime + ViewConfiguration.getDoubleTapTimeout()) {
-                    if (VDBG) log("==> touch lock icon: DOUBLE-TAP!");
-                    // This was the 2nd tap of a double-tap gesture.
-                    // Take down the touch lock overlay, but post a
-                    // message in the future to bring it back later.
-                    enableTouchLock(false);
-                    resetTouchLockTimer();
-                    // This counts as explicit "user activity".
-                    PhoneApp.getInstance().pokeUserActivity();
+                if (now < mOnscreenAnswerButtonLastTouchTime + ViewConfiguration.getDoubleTapTimeout()) {
+                    if (DBG) log("==> onscreen answer button: DOUBLE-TAP!");
+                    // This was the 2nd tap of the double-tap gesture: answer the call!
+
+                    final boolean hasRingingCall = !mRingingCall.isIdle();
+                    if (hasRingingCall) {
+                        final boolean hasActiveCall = !mForegroundCall.isIdle();
+                        final boolean hasHoldingCall = !mBackgroundCall.isIdle();
+                        if (hasActiveCall && hasHoldingCall) {
+                            if (DBG) log("onscreen answer button: ringing (both lines in use) ==> answer!");
+                            internalAnswerCallBothLinesInUse();
+                        } else {
+                            if (DBG) log("onscreen answer button: ringing ==> answer!");
+                            internalAnswerCall();  // Automatically holds the current active call,
+                                                   // if there is one
+                        }
+                    } else {
+                        // The ringing call presumably stopped just when
+                        // the user was double-tapping.
+                        if (DBG) log("onscreen answer button: no ringing call (any more); ignoring...");
+                    }
                 }
+                // The onscreen "answer" button will go away as soon as
+                // the phone goes from ringing to offhook, since that
+                // state change will trigger an updateScreen() call.
+                // TODO: consider explicitly starting some fancier
+                // animation here, like fading out the "answer" button, or
+                // sliding it offscreen...
+
             } else if (event.getAction() == MotionEvent.ACTION_UP) {
                 // Stash away the current time in case this is the first
                 // tap of a double-tap gesture.  (We measure the time from
                 // the first tap's UP to the second tap's DOWN.)
-                mTouchLockLastTouchTime = SystemClock.uptimeMillis();
+                mOnscreenAnswerButtonLastTouchTime = SystemClock.uptimeMillis();
             }
 
-            // And regardless of what just happened, we *always* consume
-            // touch events while the touch lock UI is (or was) visible.
-            return true;
-
-        } else if (v == mTouchLockOverlay) {
-            // User touched the "background" area of the touch lock overlay.
-
-            // TODO: If we're in the middle of the fade-in animation,
-            // consider making a touch *anywhere* immediately unlock the
-            // UI.  This could be risky, though, if the user tries to
-            // *double-tap* during the fade-in (in which case the 2nd tap
-            // might 't become a false touch on the dialpad!)
-            //
-            //if (event.getAction() == MotionEvent.ACTION_DOWN) {
-            //    if (DBG) log("- touch lock overlay background: handling a DOWN event.");
-            //
-            //    if (mTouchLockFadeIn.hasStarted() && !mTouchLockFadeIn.hasEnded()) {
-            //        // If we're still fading-in, a touch *anywhere* onscreen
-            //        // immediately unlocks.
-            //        if (DBG) log("==> touch lock: tap during fade-in!");
-            //
-            //        mTouchLockOverlay.clearAnimation();
-            //        enableTouchLock(false);
-            //        // ...but post a message in the future to bring it
-            //        // back later.
-            //        resetTouchLockTimer();
-            //    }
-            //}
-
-            // And regardless of what just happened, we *always* consume
-            // touch events while the touch lock UI is (or was) visible.
+            // And regardless of what just happened, we *always*
+            // consume touch events to this button.
             return true;
 
         } else {