diff options
-rw-r--r-- | InCallUI/res/drawable-hdpi/stat_sys_phone_call.png | bin | 0 -> 1095 bytes | |||
-rw-r--r-- | InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png | bin | 0 -> 4993 bytes | |||
-rw-r--r-- | InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png | bin | 0 -> 4578 bytes | |||
-rw-r--r-- | InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png | bin | 0 -> 5428 bytes | |||
-rw-r--r-- | InCallUI/res/drawable-mdpi/stat_sys_phone_call.png | bin | 0 -> 796 bytes | |||
-rw-r--r-- | InCallUI/res/drawable-xhdpi/stat_sys_phone_call.png | bin | 0 -> 1430 bytes | |||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallButtonPresenter.java | 8 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallCardPresenter.java | 7 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallHandlerService.java | 4 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallList.java | 36 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/InCallActivity.java | 10 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/InCallPresenter.java | 108 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/StatusBarNotifier.java | 267 |
13 files changed, 393 insertions, 47 deletions
diff --git a/InCallUI/res/drawable-hdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-hdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..7eda84ca5 --- /dev/null +++ b/InCallUI/res/drawable-hdpi/stat_sys_phone_call.png diff --git a/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..e0f33f88c --- /dev/null +++ b/InCallUI/res/drawable-ldrtl-hdpi/stat_sys_phone_call.png diff --git a/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..d771d87bf --- /dev/null +++ b/InCallUI/res/drawable-ldrtl-mdpi/stat_sys_phone_call.png diff --git a/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..86af9c202 --- /dev/null +++ b/InCallUI/res/drawable-ldrtl-xhdpi/stat_sys_phone_call.png diff --git a/InCallUI/res/drawable-mdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-mdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..70a4bbe71 --- /dev/null +++ b/InCallUI/res/drawable-mdpi/stat_sys_phone_call.png diff --git a/InCallUI/res/drawable-xhdpi/stat_sys_phone_call.png b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call.png Binary files differnew file mode 100644 index 000000000..1bb434076 --- /dev/null +++ b/InCallUI/res/drawable-xhdpi/stat_sys_phone_call.png diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java index 014f8f8ae..60ab7bbf4 100644 --- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java +++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java @@ -49,7 +49,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto getUi().setVisible(state == InCallState.INCALL); if (state == InCallState.INCALL) { - mCall = callList.getActiveCall(); + mCall = callList.getActiveOrBackgroundCall(); } else { mCall = null; } @@ -75,11 +75,15 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto } public void muteClicked(boolean checked) { + Logger.d(this, "turning on mute: " + checked); + CallCommandClient.getInstance().mute(checked); getUi().setMute(checked); } public void speakerClicked(boolean checked) { + Logger.d(this, "turning on speaker: " + checked); + CallCommandClient.getInstance().turnSpeakerOn(checked); getUi().setSpeaker(checked); } @@ -87,6 +91,8 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto public void holdClicked(boolean checked) { Preconditions.checkNotNull(mCall); + Logger.d(this, "holding: " + mCall.getCallId()); + // TODO(klp): use appropriate hold callId. CallCommandClient.getInstance().hold(mCall.getCallId(), checked); getUi().setHold(checked); diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java index 5f1f3b747..04a43eae1 100644 --- a/InCallUI/src/com/android/incallui/CallCardPresenter.java +++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java @@ -43,7 +43,12 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> primary = callList.getIncomingCall(); } else if (state == InCallState.INCALL) { primary = callList.getActiveCall(); - secondary = callList.getBackgroundCall(); + if (primary != null) { + secondary = callList.getBackgroundCall(); + } else { + primary = callList.getBackgroundCall(); + secondary = callList.getSecondBackgroundCall(); + } } Logger.d(this, "Primary call: " + primary); diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java index ad4772976..dcba7d6f9 100644 --- a/InCallUI/src/com/android/incallui/CallHandlerService.java +++ b/InCallUI/src/com/android/incallui/CallHandlerService.java @@ -46,9 +46,7 @@ public class CallHandlerService extends Service { mCallList = CallList.getInstance(); mMainHandler = new MainHandler(); - mInCallPresenter = InCallPresenter.getInstance(); - - mInCallPresenter.init(this); + mInCallPresenter = InCallPresenter.init(this); } @Override diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java index 17e719e7c..c16c53df0 100644 --- a/InCallUI/src/com/android/incallui/CallList.java +++ b/InCallUI/src/com/android/incallui/CallList.java @@ -128,8 +128,25 @@ public class CallList { return getFirstCallWithState(Call.State.ONHOLD); } + public Call getSecondBackgroundCall() { + return getCallWithState(Call.State.ONHOLD, 1); + } + + public Call getActiveOrBackgroundCall() { + Call call = getActiveCall(); + if (call == null) { + call = getBackgroundCall(); + } + return call; + } + public Call getIncomingCall() { - return getFirstCallWithState(Call.State.INCOMING); + Call call = getFirstCallWithState(Call.State.INCOMING); + if (call == null) { + call = getFirstCallWithState(Call.State.CALL_WAITING); + } + + return call; } public boolean existsLiveCall() { @@ -145,11 +162,24 @@ public class CallList { * Returns first call found in the call map with the specified state. */ public Call getFirstCallWithState(int state) { + return getCallWithState(state, 0); + } + + /** + * Returns the [position]th call found in the call map with the specified state. + * TODO(klp): Improve this logic to sort by call time. + */ + public Call getCallWithState(int state, int positionToFind) { Call retval = null; + int position = 0; for (Call call : mCallMap.values()) { if (call.getState() == state) { - retval = call; - break; + if (position >= positionToFind) { + retval = call; + break; + } else { + position++; + } } } diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java index 89450c534..9eb54ee5e 100644 --- a/InCallUI/src/com/android/incallui/InCallActivity.java +++ b/InCallUI/src/com/android/incallui/InCallActivity.java @@ -101,6 +101,8 @@ public class InCallActivity extends Activity { @Override public void finish() { Logger.d(this, "finish()..."); + tearDownPresenters(); + super.finish(); // TODO(klp): Actually finish the activity for now. Revisit performance implications of @@ -220,6 +222,14 @@ public class InCallActivity extends Activity { mainPresenter.setActivity(this); } + private void tearDownPresenters() { + InCallPresenter mainPresenter = InCallPresenter.getInstance(); + + mainPresenter.removeListener(mCallButtonFragment.getPresenter()); + mainPresenter.removeListener(mCallCardFragment.getPresenter()); + mainPresenter.removeListener(mAnswerFragment.getPresenter()); + } + private void toast(String text) { final Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java index 118c030c2..47d2d69c8 100644 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ b/InCallUI/src/com/android/incallui/InCallPresenter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import com.google.common.base.Preconditions; import android.content.Context; import android.content.Intent; +import com.android.services.telephony.common.Call; + import java.util.Set; /** @@ -31,35 +33,35 @@ import java.util.Set; * are disconnected. * Creates and manages the in-call state and provides a listener pattern for the presenters * that want to listen in on the in-call state changes. + * TODO(klp): This class has become more of a state machine at this point. Consider renaming. */ public class InCallPresenter implements CallList.Listener { private static InCallPresenter sInCallPresenter; - private Context mContext; + private final StatusBarNotifier mStatusBarNotifier; + private final Set<InCallStateListener> mListeners = Sets.newHashSet(); + private InCallState mInCallState = InCallState.HIDDEN; private InCallActivity mInCallActivity; - private final Set<InCallStateListener> mListeners = Sets.newHashSet(); - public static synchronized InCallPresenter getInstance() { - if (sInCallPresenter == null) { - sInCallPresenter = new InCallPresenter(); - } + public static InCallPresenter getInstance() { + Preconditions.checkNotNull(sInCallPresenter); return sInCallPresenter; } - public void init(Context context) { - Logger.i(this, "InCallPresenter initialized with context " + context); - Preconditions.checkState(mContext == null); - - mContext = context; - CallList.getInstance().addListener(this); + public static synchronized InCallPresenter init(Context context) { + Preconditions.checkState(sInCallPresenter == null); + sInCallPresenter = new InCallPresenter(context); + return sInCallPresenter; } public void setActivity(InCallActivity inCallActivity) { mInCallActivity = inCallActivity; mInCallState = InCallState.STARTED; + Logger.d(this, "UI Initialized"); + // Since the UI just came up, imitate an update from the call list // to set the proper UI state. onCallListChange(CallList.getInstance()); @@ -75,21 +77,14 @@ public class InCallPresenter implements CallList.Listener { public void onCallListChange(CallList callList) { // fast fail if we are still starting up if (mInCallState == InCallState.STARTING_UP) { + Logger.d(this, "Already on STARTING_UP, ignoring until ready"); return; } - InCallState newState = mInCallState; - if (callList.getIncomingCall() != null) { - newState = InCallState.INCOMING; - } else if (callList.getActiveCall() != null) { - newState = InCallState.INCALL; - } else { - newState = InCallState.HIDDEN; - } - + InCallState newState = getPotentialStateFromCallList(callList); newState = startOrFinishUi(newState); - // finally set the new state before announcing it to the world + // Set the new state before announcing it to the world mInCallState = newState; // notify listeners of new state @@ -99,6 +94,22 @@ public class InCallPresenter implements CallList.Listener { } } + /** + * Given the call list, return the state in which the in-call screen should be. + */ + public InCallState getPotentialStateFromCallList(CallList callList) { + InCallState newState = InCallState.HIDDEN; + + if (callList.getIncomingCall() != null) { + newState = InCallState.INCOMING; + } else if (callList.getActiveCall() != null || + callList.getBackgroundCall() != null) { + newState = InCallState.INCALL; + } + + return newState; + } + public void addListener(InCallStateListener listener) { Preconditions.checkNotNull(listener); mListeners.add(listener); @@ -115,26 +126,23 @@ public class InCallPresenter implements CallList.Listener { * It returns a potential new middle state (STARTING_UP) if appropriate. */ private InCallState startOrFinishUi(InCallState newState) { + Logger.d(this, "startOrFInishUi: " + newState.toString()); + // TODO(klp): Consider a proper state machine implementation // if we need to show something, we need to start the Ui... - if (newState != InCallState.HIDDEN) { - - // ...only if the UI is currently hidden - if (mInCallState == InCallState.HIDDEN) { - // TODO(klp): Update the flags to match the PhoneApp activity - final Intent intent = new Intent(mContext, InCallActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); - + if (!newState.isHidden()) { + + // When we attempt to go to any state from HIDDEN, it means that we need to create the + // entire UI. However, the StatusBarNotifier is in charge of starting up the Ui because + // it has special behavior in case we have to deal with an immersive foreground app. + // We set the STARTING_UP state to let StatusBarNotifier know it needs to start the + // the Ui. + if (mInCallState.isHidden()) { return InCallState.STARTING_UP; } - // (newState == InCallState.HIDDEN) - // Else, we need to hide the UI...if it exists } else if (mInCallActivity != null) { - mListeners.clear(); - // Null out reference before we start end sequence InCallActivity temp = mInCallActivity; mInCallActivity = null; @@ -148,7 +156,12 @@ public class InCallPresenter implements CallList.Listener { /** * Private constructor. Must use getInstance() to get this singleton. */ - private InCallPresenter() { + private InCallPresenter(Context context) { + Preconditions.checkNotNull(context); + + mStatusBarNotifier = new StatusBarNotifier(context); + addListener(mStatusBarNotifier); + CallList.getInstance().addListener(this); } @@ -156,12 +169,29 @@ public class InCallPresenter implements CallList.Listener { * All the main states of InCallActivity. */ public enum InCallState { + // InCall Screen is off and there are no calls HIDDEN, + + // In call is in the process of starting up STARTING_UP, + + // In call has started but is not displaying any information STARTED, + + // Incoming-call screen is up INCOMING, - INCALL - }; + + // In-call experience is showing + INCALL; + + public boolean isIncoming() { + return (this == INCOMING); + } + + public boolean isHidden() { + return (this == HIDDEN); + } + } /** * Interface implemented by classes that need to know about the InCall State. diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java new file mode 100644 index 000000000..617be2fac --- /dev/null +++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.incallui; + +import com.google.common.base.Preconditions; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import com.android.incallui.InCallPresenter.InCallState; +import com.android.services.telephony.common.Call; + +/** + * This class adds Notifications to the status bar for the in-call experience. + */ +public class StatusBarNotifier implements InCallPresenter.InCallStateListener { + // notification types + private static final int IN_CALL_NOTIFICATION = 1; + + private final Context mContext; + private final NotificationManager mNotificationManager; + + public StatusBarNotifier(Context context) { + Preconditions.checkNotNull(context); + + mContext = context; + mNotificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Creates notifications according to the state we receive from {@link InCallPresenter}. + */ + @Override + public void onStateChange(InCallState state, CallList callList) { + + if (InCallState.STARTING_UP == state) { + // TODO: This method needs to move somewhere more appropriate for sharing + InCallState newState = InCallPresenter.getInstance() + .getPotentialStateFromCallList(callList); + + updateNotificationAndLaunchIncomingCallUi(newState); + } else { + updateInCallNotification(state); + } + } + + /** + * Updates the phone app's status bar notification based on the + * current telephony state, or cancels the notification if the phone + * is totally idle. + * + * This method will never actually launch the incoming-call UI. + * (Use updateNotificationAndLaunchIncomingCallUi() for that.) + */ + private void updateInCallNotification(InCallState state) { + // allowFullScreenIntent=false means *don't* allow the incoming + // call UI to be launched. + updateInCallNotification(false, state); + } + + /** + * Updates the phone app's status bar notification *and* launches the + * incoming call UI in response to a new incoming call. + * + * This is just like updateInCallNotification(), with one exception: + * If an incoming call is ringing (or call-waiting), the notification + * will also include a "fullScreenIntent" that will cause the + * InCallScreen to be launched immediately, unless the current + * foreground activity is marked as "immersive". + * + * (This is the mechanism that actually brings up the incoming call UI + * when we receive a "new ringing connection" event from the telephony + * layer.) + * + * Watch out: this method should ONLY be called directly from the code + * path in CallNotifier that handles the "new ringing connection" + * event from the telephony layer. All other places that update the + * in-call notification (like for phone state changes) should call + * updateInCallNotification() instead. (This ensures that we don't + * end up launching the InCallScreen multiple times for a single + * incoming call, which could cause slow responsiveness and/or visible + * glitches.) + * + * Also note that this method is safe to call even if the phone isn't + * actually ringing (or, more likely, if an incoming call *was* + * ringing briefly but then disconnected). In that case, we'll simply + * update or cancel the in-call notification based on the current + * phone state. + * + * @see #updateInCallNotification(boolean) + */ + private void updateNotificationAndLaunchIncomingCallUi(InCallState state) { + // Set allowFullScreenIntent=true to indicate that we *should* + // launch the incoming call UI if necessary. + updateInCallNotification(true, state); + } + + + /** + * Take down the in-call notification. + * @see updateInCallNotification() + */ + private void cancelInCall() { + Logger.d(this, "cancelInCall()..."); + mNotificationManager.cancel(IN_CALL_NOTIFICATION); + } + + /** + * Helper method for updateInCallNotification() and + * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's + * status bar notification based on the current telephony state, or + * cancels the notification if the phone is totally idle. + * + * @param allowFullScreenIntent If true, *and* an incoming call is + * ringing, the notification will include a "fullScreenIntent" + * pointing at the InCallActivity (which will cause the InCallActivity + * to be launched.) + * Watch out: This should be set to true *only* when directly + * handling a new incoming call for the first time. + */ + private void updateInCallNotification(boolean allowFullScreenIntent, InCallState state) { + int resId; + Logger.d(this, "updateInCallNotification(allowFullScreenIntent = " + + allowFullScreenIntent + ")..."); + + if (InCallState.INCALL != state && InCallState.INCOMING != state) { + cancelInCall(); + return; + } + + final PendingIntent inCallPendingIntent = getLaunchPendingIntent(); + final Notification.Builder builder = getNotificationBuilder(); + + /* + * Set up the Intents that will get fires when the user interacts with the notificaiton. + */ + builder.setContentIntent(inCallPendingIntent); + if (InCallState.INCOMING == state) { + if (allowFullScreenIntent) { + configureFullScreenIntent(builder, inCallPendingIntent); + } + } else if (InCallState.INCALL == state) { + addActiveCallIntents(builder); + } + + + /* + * Set up notification Ui. + */ + setUpNotificationUi(builder); + + + /* + * Fire off the notification + */ + Notification notification = builder.build(); + Logger.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification); + mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); + } + + /** + * Sets up the main Ui for the notification + */ + private void setUpNotificationUi(Notification.Builder builder) { + // set the content + builder.setContentText(mContext.getString(R.string.notification_ongoing_call)); + builder.setSmallIcon(R.drawable.stat_sys_phone_call); + } + + private void addActiveCallIntents(Notification.Builder builder) { + Logger.i(this, "Will show \"hang-up\" action in the ongoing active call Notification"); + // TODO: use better asset. + // TODO(klp): uncomment this for "hang-up" capability + //builder.addAction(R.drawable.stat_sys_phone_call_end, + // mContext.getText(R.string.notification_action_end_call), + // PhoneGlobals.createHangUpOngoingCallPendingIntent(mContext)); + } + + /** + * Adds fullscreen intent to the builder. + */ + private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) { + // Ok, we actually want to launch the incoming call + // UI at this point (in addition to simply posting a notification + // to the status bar). Setting fullScreenIntent will cause + // the InCallScreen to be launched immediately *unless* the + // current foreground activity is marked as "immersive". + Logger.d(this, "- Setting fullScreenIntent: " + intent); + builder.setFullScreenIntent(intent, true); + + // Ugly hack alert: + // + // The NotificationManager has the (undocumented) behavior + // that it will *ignore* the fullScreenIntent field if you + // post a new Notification that matches the ID of one that's + // already active. Unfortunately this is exactly what happens + // when you get an incoming call-waiting call: the + // "ongoing call" notification is already visible, so the + // InCallScreen won't get launched in this case! + // (The result: if you bail out of the in-call UI while on a + // call and then get a call-waiting call, the incoming call UI + // won't come up automatically.) + // + // The workaround is to just notice this exact case (this is a + // call-waiting call *and* the InCallScreen is not in the + // foreground) and manually cancel the in-call notification + // before (re)posting it. + // + // TODO: there should be a cleaner way of avoiding this + // problem (see discussion in bug 3184149.) + + // TODO(klp): reenable this for klp + /*if (incomingCall.getState() == Call.State.CALL_WAITING) { + Logger.i(this, "updateInCallNotification: call-waiting! force relaunch..."); + // Cancel the IN_CALL_NOTIFICATION immediately before + // (re)posting it; this seems to force the + // NotificationManager to launch the fullScreenIntent. + mNotificationManager.cancel(IN_CALL_NOTIFICATION); + }*/ + } + + private Notification.Builder getNotificationBuilder() { + final Notification.Builder builder = new Notification.Builder(mContext); + builder.setOngoing(true); + + // Make the notification prioritized over the other normal notifications. + builder.setPriority(Notification.PRIORITY_HIGH); + + return builder; + } + + private PendingIntent getLaunchPendingIntent() { + + final Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_NO_USER_ACTION); + intent.setClass(mContext, InCallActivity.class); + + // PendingIntent that can be used to launch the InCallActivity. The + // system fires off this intent if the user pulls down the windowshade + // and clicks the notification's expanded view. It's also used to + // launch the InCallActivity immediately when when there's an incoming + // call (see the "fullScreenIntent" field below). + PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); + + return inCallPendingIntent; + } +} |