summaryrefslogtreecommitdiff
path: root/InCallUI/src/com/android/incallui/InCallPresenter.java
diff options
context:
space:
mode:
authorSantos Cordon <santoscordon@google.com>2013-09-05 13:22:07 -0700
committerSantos Cordon <santoscordon@google.com>2013-09-09 12:18:12 -0700
commit1f63d2d8e8db3616c16886952813b3d0473216e7 (patch)
tree9ffb5c9a744cc5679c59091e617c00acc4209193 /InCallUI/src/com/android/incallui/InCallPresenter.java
parent24deb0f78f1854bfc273bb888e7fd4ac123bcc22 (diff)
Improve lifecycle ordering for InCallUI
This CL rearranges startup and teardown of InCallUI to reduce the number of race conditions resulting in runtime exceptions and app crashes. At a high level this CL fixes the following: - TeleService should be able to unbind and rebind at any time without causing problems in the UI. (Fixes to InCallPresenter) - On weird occasions we were seeing secondary UIs pop up if we rebound while the older UI was still up and one of the UIs would be orphaned on the foreground. - call notifications can be sent during (1) activity startup, (2) activity lifetime, (3) activity destruction, (4) no activity...and nothing should crash. - Lots of crashes when notifications came during destruction and startup. (Fixed setup in InCallActivity + presenters, and startup/teardown ordering in InCallPresenter) Details: (1) InCallPresenter handed out instances of member classes to the UI classes, but upon unbinding and rebinding, the classes were recreated. This meant that the activity which was potentially still up had stale versions of AudioModeProvider and ProximitySensor. - Classes created/used by CallHandlerService are now singletons so that they do not change from one bind to the other. If the service tries to initialize InCallPresenter while the activity is still up (and so we haven't yet torn down) we reuse the reuse the previous initialization since there is no need to recreate them, and classes in the Activity dont ever become stale. (2) We were recreating new copies of InCallActivity on updates that occur while tearing down the previous activity. This caused weird errors where second emptier activities were up after all calls ended. - Solution to this was to ignore all updates while we are finishing the activity. When the activity is finally finished, we check if we should bring up a new activity in case some update came while we were finishing. (3) We set listeners on presenters from a parent class that wasn't aware of UI transitions. - All Presenters are not responsible for setting and unsetting their listeners. This allows them to unset them before their UI goes away. + renamed HIDDEN to NO_CALLS as hidden was confusing to developers which associated the term with foreground/backgroundness of the app. + Improved some logging bug:10573125 Change-Id: I2d1af6a6e972b3b3bd93af839054e879f0b74b4f
Diffstat (limited to 'InCallUI/src/com/android/incallui/InCallPresenter.java')
-rw-r--r--InCallUI/src/com/android/incallui/InCallPresenter.java131
1 files changed, 86 insertions, 45 deletions
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index cc4cee277..1e2f6755d 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -51,7 +51,7 @@ public class InCallPresenter implements CallList.Listener {
private CallList mCallList;
private InCallActivity mInCallActivity;
private boolean mServiceConnected = false;
- private InCallState mInCallState = InCallState.HIDDEN;
+ private InCallState mInCallState = InCallState.NO_CALLS;
private ProximitySensor mProximitySensor;
public static synchronized InCallPresenter getInstance() {
@@ -62,12 +62,18 @@ public class InCallPresenter implements CallList.Listener {
}
public void setUp(Context context, CallList callList, AudioModeProvider audioModeProvider) {
+ if (mServiceConnected) {
+ Log.i(this, "New service connection replacing existing one.");
+ // retain the current resources, no need to create new ones.
+ Preconditions.checkState(context == mContext);
+ Preconditions.checkState(callList == mCallList);
+ Preconditions.checkState(audioModeProvider == mAudioModeProvider);
+ return;
+ }
+
Preconditions.checkNotNull(context);
mContext = context;
- mCallList = callList;
- mCallList.addListener(this);
-
mContactInfoCache = ContactInfoCache.getInstance(context);
mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache, mCallList);
@@ -76,11 +82,17 @@ public class InCallPresenter implements CallList.Listener {
mAudioModeProvider = audioModeProvider;
+ mProximitySensor = new ProximitySensor(context, mAudioModeProvider);
+ addListener(mProximitySensor);
+
+ mCallList = callList;
+
// This only gets called by the service so this is okay.
mServiceConnected = true;
- mProximitySensor = new ProximitySensor(context, mAudioModeProvider);
- addListener(mProximitySensor);
+ // The final thing we do in this set up is add ourselves as a listener to CallList. This
+ // will kick off an update and the whole process can start.
+ mCallList.addListener(this);
Log.d(this, "Finished InCallPresenter.setUp");
}
@@ -105,18 +117,42 @@ public class InCallPresenter implements CallList.Listener {
* the tear-down process.
*/
public void setActivity(InCallActivity inCallActivity) {
- mInCallActivity = inCallActivity;
-
- if (mInCallActivity != null) {
- Log.i(this, "UI Initialized");
+ boolean updateListeners = false;
+
+ if (inCallActivity != null) {
+ if (mInCallActivity == null) {
+ updateListeners = true;
+ Log.i(this, "UI Initialized");
+ } else if (mInCallActivity != inCallActivity) {
+ Log.wtf(this, "Setting a second activity before destroying the first.");
+ } else {
+ // since setActivity is called onStart(), it can be called multiple times.
+ // This is fine and ignorable, but we do not want to update the world every time
+ // this happens (like going to/from background) so we do not set updateListeners.
+ }
- // Since the UI just came up, imitate an update from the call list
- // to set the proper UI state.
- onCallListChange(mCallList);
+ mInCallActivity = inCallActivity;
} else {
Log.i(this, "UI Destroyed)");
+ updateListeners = true;
+ mInCallActivity = null;
attemptCleanup();
}
+
+
+ // Messages can come from the telephony layer while the activity is coming up
+ // and while the activity is going down. So in both cases we need to recalculate what
+ // state we should be in after they complete.
+ // Examples: (1) A new incoming call could come in and then get disconnected before
+ // the activity is created.
+ // (2) All calls could disconnect and then get a new incoming call before the
+ // activity is destroyed.
+ //
+ // ... but only if we are still connected (or got a new connection) to the service.
+ // Otherwise the service will come back up when it needs us.
+ if (updateListeners && mServiceConnected) {
+ onCallListChange(mCallList);
+ }
}
/**
@@ -148,7 +184,10 @@ public class InCallPresenter implements CallList.Listener {
*/
@Override
public void onIncomingCall(Call call) {
- mInCallState = startOrFinishUi(InCallState.INCOMING);
+ InCallState newState = startOrFinishUi(InCallState.INCOMING);
+
+ Log.i(this, "Phone switching state: " + mInCallState + " -> " + newState);
+ mInCallState = newState;
for (IncomingCallListener listener : mIncomingCallListeners) {
listener.onIncomingCall(call);
@@ -159,7 +198,7 @@ public class InCallPresenter implements CallList.Listener {
* Given the call list, return the state in which the in-call screen should be.
*/
public static InCallState getPotentialStateFromCallList(CallList callList) {
- InCallState newState = InCallState.HIDDEN;
+ InCallState newState = InCallState.NO_CALLS;
if (callList.getIncomingCall() != null) {
newState = InCallState.INCOMING;
@@ -219,8 +258,7 @@ public class InCallPresenter implements CallList.Listener {
* Returns true if the incall app is the foreground application.
*/
public boolean isShowingInCallUi() {
- return (mInCallActivity != null &&
- mInCallActivity.isForegroundActivity());
+ return (isActivityStarted() && mInCallActivity.isForegroundActivity());
}
/**
@@ -257,9 +295,9 @@ public class InCallPresenter implements CallList.Listener {
// 1. there is an activity
// 2. the activity is not already in the foreground
// 3. We are in a state where we want to show the incall ui
- if (mInCallActivity != null &&
- !mInCallActivity.isForegroundActivity() &&
- mInCallState != InCallState.HIDDEN) {
+ if (isActivityStarted() &&
+ !isShowingInCallUi() &&
+ mInCallState != InCallState.NO_CALLS) {
showInCall();
}
}
@@ -312,35 +350,34 @@ public class InCallPresenter implements CallList.Listener {
// user with a top-level notification. Just show the call UI normally.
final boolean showCallUi = (InCallState.OUTGOING == newState);
+ // TODO: Can we be suddenly in a call without it having been in the outgoing or incoming
+ // state? I havent seen that but if it can happen, the code below should be enabled.
+ // showCallUi |= (InCallState.INCALL && !isActivityStarted());
+
+ // The only time that we have an instance of mInCallActivity and it isn't started is
+ // when it is being destroyed. In that case, lets avoid bringing up another instance of
+ // the activity. When it is finally destroyed, we double check if we should bring it back
+ // up so we aren't going to lose anything by avoiding a second startup here.
+ boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
+ if (activityIsFinishing) {
+ Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
+ return mInCallState;
+ }
+
if (showCallUi) {
Log.i(this, "Start in call UI");
showInCall();
} else if (startStartupSequence) {
Log.i(this, "Start Full Screen in call UI");
mStatusBarNotifier.updateNotificationAndLaunchIncomingCallUi(newState, mCallList);
- } else if (newState == InCallState.HIDDEN) {
+ } else if (newState == InCallState.NO_CALLS) {
Log.i(this, "Hide in call UI");
- // The new state is the hidden state (no calls). Tear everything down.
+ // The new state is the no calls state. Tear everything down.
if (mInCallActivity != null) {
- if (mInCallActivity.isFinishing()) {
- // Tear down process:
- // When there are no more calls to handle, two things happen:
- // 1. The binding connection with TeleService ends
- // 2. InCallState changes to HIDDEN and we call activity.finish() here.
- //
- // Without the service connection, we will not get updates from the service
- // and so will never get a new call to move out of the HIDDEN state. Since this
- // code is called when we move from a different state into the HIDDEN state,
- // it should never get hit twice. In case it does, log an error.
- Log.e(this, "Attempting to finish incall activity twice.");
- } else {
+ if (isActivityStarted()) {
mInCallActivity.finish();
}
-
- // blow away stale contact info so that we get fresh data on
- // the next set of calls
- mContactInfoCache.clearCache();
}
}
@@ -352,8 +389,16 @@ public class InCallPresenter implements CallList.Listener {
* down.
*/
private void attemptCleanup() {
- if (mInCallActivity == null && !mServiceConnected) {
- Log.i(this, "Start InCall presenter cleanup.");
+ boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected);
+ Log.i(this, "attemptCleanup? " + shouldCleanup);
+
+ if (shouldCleanup) {
+
+ // blow away stale contact info so that we get fresh data on
+ // the next set of calls
+ mContactInfoCache.clearCache();
+ mContactInfoCache = null;
+
mProximitySensor = null;
mAudioModeProvider = null;
@@ -398,7 +443,7 @@ public class InCallPresenter implements CallList.Listener {
*/
public enum InCallState {
// InCall Screen is off and there are no calls
- HIDDEN,
+ NO_CALLS,
// Incoming-call screen is up
INCOMING,
@@ -413,10 +458,6 @@ public class InCallPresenter implements CallList.Listener {
return (this == INCOMING);
}
- public boolean isHidden() {
- return (this == HIDDEN);
- }
-
public boolean isConnectingOrConnected() {
return (this == INCOMING ||
this == OUTGOING ||