1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.phone;
18
19import com.android.phone.Constants.CallStatusCode;
20
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.drawable.Drawable;
24import android.net.Uri;
25import android.text.TextUtils;
26import android.util.Log;
27
28
29/**
30 * Helper class to keep track of "persistent state" of the in-call UI.
31 *
32 * The onscreen appearance of the in-call UI mostly depends on the current
33 * Call/Connection state, which is owned by the telephony framework.  But
34 * there's some application-level "UI state" too, which lives here in the
35 * phone app.
36 *
37 * This application-level state information is *not* maintained by the
38 * InCallScreen, since it needs to persist throughout an entire phone call,
39 * not just a single resume/pause cycle of the InCallScreen.  So instead, that
40 * state is stored here, in a singleton instance of this class.
41 *
42 * The state kept here is a high-level abstraction of in-call UI state: we
43 * don't know about implementation details like specific widgets or strings or
44 * resources, but we do understand higher level concepts (for example "is the
45 * dialpad visible") and high-level modes (like InCallScreenMode) and error
46 * conditions (like CallStatusCode).
47 *
48 * @see InCallControlState for a separate collection of "UI state" that
49 * controls all the onscreen buttons of the in-call UI, based on the state of
50 * the telephony layer.
51 *
52 * The singleton instance of this class is owned by the PhoneApp instance.
53 */
54public class InCallUiState {
55    private static final String TAG = "InCallUiState";
56    private static final boolean DBG = false;
57
58    /** The singleton InCallUiState instance. */
59    private static InCallUiState sInstance;
60
61    private Context mContext;
62
63    /**
64     * Initialize the singleton InCallUiState instance.
65     *
66     * This is only done once, at startup, from PhoneApp.onCreate().
67     * From then on, the InCallUiState instance is available via the
68     * PhoneApp's public "inCallUiState" field, which is why there's no
69     * getInstance() method here.
70     */
71    /* package */ static InCallUiState init(Context context) {
72        synchronized (InCallUiState.class) {
73            if (sInstance == null) {
74                sInstance = new InCallUiState(context);
75            } else {
76                Log.wtf(TAG, "init() called multiple times!  sInstance = " + sInstance);
77            }
78            return sInstance;
79        }
80    }
81
82    /**
83     * Private constructor (this is a singleton).
84     * @see init()
85     */
86    private InCallUiState(Context context) {
87        mContext = context;
88    }
89
90
91    //
92    // (1) High-level state of the whole in-call UI
93    //
94
95    /** High-level "modes" of the in-call UI. */
96    public enum InCallScreenMode {
97        /**
98         * Normal in-call UI elements visible.
99         */
100        NORMAL,
101        /**
102         * "Manage conference" UI is visible, totally replacing the
103         * normal in-call UI.
104         */
105        MANAGE_CONFERENCE,
106        /**
107         * Non-interactive UI state.  Call card is visible,
108         * displaying information about the call that just ended.
109         */
110        CALL_ENDED,
111        /**
112         * Normal OTA in-call UI elements visible.
113         */
114        OTA_NORMAL,
115        /**
116         * OTA call ended UI visible, replacing normal OTA in-call UI.
117         */
118        OTA_ENDED,
119        /**
120         * Default state when not on call
121         */
122        UNDEFINED
123    }
124
125    /** Current high-level "mode" of the in-call UI. */
126    InCallScreenMode inCallScreenMode = InCallScreenMode.UNDEFINED;
127
128
129    //
130    // (2) State of specific UI elements
131    //
132
133    /**
134     * Is the onscreen twelve-key dialpad visible?
135     */
136    boolean showDialpad;
137
138    /**
139     * The contents of the twelve-key dialpad's "digits" display, which is
140     * visible only when the dialpad itself is visible.
141     *
142     * (This is basically the "history" of DTMF digits you've typed so far
143     * in the current call.  It's cleared out any time a new call starts,
144     * to make sure the digits don't persist between two separate calls.)
145     */
146    String dialpadDigits;
147
148
149    //
150    // (3) Error / diagnostic indications
151    //
152
153    // This section provides an abstract concept of an "error status
154    // indication" for some kind of exceptional condition that needs to be
155    // communicated to the user, in the context of the in-call UI.
156    //
157    // If mPendingCallStatusCode is any value other than SUCCESS, that
158    // indicates that the in-call UI needs to display a dialog to the user
159    // with the specified title and message text.
160    //
161    // When an error occurs outside of the InCallScreen itself (like
162    // during CallController.placeCall() for example), we inform the user
163    // by doing the following steps:
164    //
165    // (1) set the "pending call status code" to a value other than SUCCESS
166    //     (based on the specific error that happened)
167    // (2) force the InCallScreen to be launched (or relaunched)
168    // (3) InCallScreen.onResume() will notice that pending call status code
169    //     is set, and will actually bring up the desired dialog.
170    //
171    // Watch out: any time you set (or change!) the pending call status code
172    // field you must be sure to always (re)launch the InCallScreen.
173    //
174    // Finally, the InCallScreen itself is responsible for resetting the
175    // pending call status code, when the user dismisses the dialog (like by
176    // hitting the OK button or pressing Back).  The pending call status code
177    // field is NOT cleared simply by the InCallScreen being paused or
178    // finished, since the resulting dialog needs to persist across
179    // orientation changes or if the screen turns off.
180
181    // TODO: other features we might eventually need here:
182    //
183    //   - Some error status messages stay in force till reset,
184    //     others may automatically clear themselves after
185    //     a fixed delay
186    //
187    //   - Some error statuses may be visible as a dialog with an OK
188    //     button (like "call failed"), others may be an indefinite
189    //     progress dialog (like "turning on radio for emergency call").
190    //
191    //   - Eventually some error statuses may have extra actions (like a
192    //     "retry call" button that we might provide at the bottom of the
193    //     "call failed because you have no signal" dialog.)
194
195    /**
196     * The current pending "error status indication" that we need to
197     * display to the user.
198     *
199     * If this field is set to a value other than SUCCESS, this indicates to
200     * the InCallScreen that we need to show some kind of message to the user
201     * (usually an error dialog) based on the specified status code.
202     */
203    private CallStatusCode mPendingCallStatusCode = CallStatusCode.SUCCESS;
204
205    /**
206     * @return true if there's a pending "error status indication"
207     * that we need to display to the user.
208     */
209    public boolean hasPendingCallStatusCode() {
210        if (DBG) log("hasPendingCallStatusCode() ==> "
211                     + (mPendingCallStatusCode != CallStatusCode.SUCCESS));
212        return (mPendingCallStatusCode != CallStatusCode.SUCCESS);
213    }
214
215    /**
216     * @return the pending "error status indication" code
217     * that we need to display to the user.
218     */
219    public CallStatusCode getPendingCallStatusCode() {
220        if (DBG) log("getPendingCallStatusCode() ==> " + mPendingCallStatusCode);
221        return mPendingCallStatusCode;
222    }
223
224    /**
225     * Sets the pending "error status indication" code.
226     */
227    public void setPendingCallStatusCode(CallStatusCode status) {
228        if (DBG) log("setPendingCallStatusCode( " + status + " )...");
229        if (mPendingCallStatusCode != CallStatusCode.SUCCESS) {
230            // Uh oh: mPendingCallStatusCode is already set to some value
231            // other than SUCCESS (which indicates that there was some kind of
232            // failure), and now we're trying to indicate another (potentially
233            // different) failure.  But we can only indicate one failure at a
234            // time to the user, so the previous pending code is now going to
235            // be lost.
236            Log.w(TAG, "setPendingCallStatusCode: setting new code " + status
237                  + ", but a previous code " + mPendingCallStatusCode
238                  + " was already pending!");
239        }
240        mPendingCallStatusCode = status;
241    }
242
243    /**
244     * Clears out the pending "error status indication" code.
245     *
246     * This indicates that there's no longer any error or "exceptional
247     * condition" that needs to be displayed to the user.  (Typically, this
248     * method is called when the user dismisses the error dialog that came up
249     * because of a previous call status code.)
250     */
251    public void clearPendingCallStatusCode() {
252        if (DBG) log("clearPendingCallStatusCode()...");
253        mPendingCallStatusCode = CallStatusCode.SUCCESS;
254    }
255
256    /**
257     * Flag used to control the CDMA-specific "call lost" dialog.
258     *
259     * If true, that means that if the *next* outgoing call fails with an
260     * abnormal disconnection cause, we need to display the "call lost"
261     * dialog.  (Normally, in CDMA we handle some types of call failures
262     * by automatically retrying the call.  This flag is set to true when
263     * we're about to auto-retry, which means that if the *retry* also
264     * fails we'll give up and display an error.)
265     * See the logic in InCallScreen.onDisconnect() for the full story.
266     *
267     * TODO: the state machine that maintains the needToShowCallLostDialog
268     * flag in InCallScreen.onDisconnect() should really be moved into the
269     * CallController.  Then we can get rid of this extra flag, and
270     * instead simply use the CallStatusCode value CDMA_CALL_LOST to
271     * trigger the "call lost" dialog.
272     */
273    boolean needToShowCallLostDialog;
274
275
276    //
277    // Progress indications
278    //
279
280    /**
281     * Possible messages we might need to display along with
282     * an indefinite progress spinner.
283     */
284    public enum ProgressIndicationType {
285        /**
286         * No progress indication needs to be shown.
287         */
288        NONE,
289
290        /**
291         * Shown when making an emergency call from airplane mode;
292         * see CallController$EmergencyCallHelper.
293         */
294        TURNING_ON_RADIO,
295
296        /**
297         * Generic "retrying" state.  (Specifically, this is shown while
298         * retrying after an initial failure from the "emergency call from
299         * airplane mode" sequence.)
300         */
301         RETRYING
302    }
303
304    /**
305     * The current progress indication that should be shown
306     * to the user.  Any value other than NONE will cause the InCallScreen
307     * to bring up an indefinite progress spinner along with a message
308     * corresponding to the specified ProgressIndicationType.
309     */
310    private ProgressIndicationType progressIndication = ProgressIndicationType.NONE;
311
312    /** Sets the current progressIndication. */
313    public void setProgressIndication(ProgressIndicationType value) {
314        progressIndication = value;
315    }
316
317    /** Clears the current progressIndication. */
318    public void clearProgressIndication() {
319        progressIndication = ProgressIndicationType.NONE;
320    }
321
322    /**
323     * @return the current progress indication type, or ProgressIndicationType.NONE
324     * if no progress indication is currently active.
325     */
326    public ProgressIndicationType getProgressIndication() {
327        return progressIndication;
328    }
329
330    /** @return true if a progress indication is currently active. */
331    public boolean isProgressIndicationActive() {
332        return (progressIndication != ProgressIndicationType.NONE);
333    }
334
335
336    //
337    // (4) Optional overlay when a 3rd party "provider" is used.
338    //     @see InCallScreen.updateProviderOverlay()
339    //
340
341    // TODO: maybe isolate all the provider-overlay-related stuff out to a
342    //       separate inner class?
343    boolean providerOverlayVisible;
344    CharSequence providerLabel;
345    Drawable providerIcon;
346    Uri providerGatewayUri;
347    // The formatted address extracted from mProviderGatewayUri. User visible.
348    String providerAddress;
349
350    /**
351     * Set the fields related to the provider support
352     * based on the specified intent.
353     */
354    public void setProviderOverlayInfo(Intent intent) {
355        providerLabel = PhoneUtils.getProviderLabel(mContext, intent);
356        providerIcon = PhoneUtils.getProviderIcon(mContext, intent);
357        providerGatewayUri = PhoneUtils.getProviderGatewayUri(intent);
358        providerAddress = PhoneUtils.formatProviderUri(providerGatewayUri);
359        providerOverlayVisible = true;
360
361        // ...but if any of the "required" fields are missing, completely
362        // disable the overlay.
363        if (TextUtils.isEmpty(providerLabel) || providerIcon == null ||
364            providerGatewayUri == null || TextUtils.isEmpty(providerAddress)) {
365            clearProviderOverlayInfo();
366        }
367    }
368
369    /**
370     * Clear all the fields related to the provider support.
371     */
372    public void clearProviderOverlayInfo() {
373        providerOverlayVisible = false;
374        providerLabel = null;
375        providerIcon = null;
376        providerGatewayUri = null;
377        providerAddress = null;
378    }
379
380    /**
381     * "Call origin" of the most recent phone call.
382     *
383     * Watch out: right now this is only used to determine where the user should go after the phone
384     * call. See also {@link InCallScreen} for more detail. There is *no* specific specification
385     * about how this variable will be used.
386     *
387     * @see PhoneApp#setLatestActiveCallOrigin(String)
388     * @see PhoneApp#createPhoneEndIntentUsingCallOrigin()
389     *
390     * TODO: we should determine some public behavior for this variable.
391     */
392    String latestActiveCallOrigin;
393
394    //
395    // Debugging
396    //
397
398    public void dumpState() {
399        log("dumpState():");
400        log("  - showDialpad: " + showDialpad);
401        if (hasPendingCallStatusCode()) {
402            log("  - status indication is pending!");
403            log("    - pending call status code = " + mPendingCallStatusCode);
404        } else {
405            log("  - pending call status code: none");
406        }
407        log("  - progressIndication: " + progressIndication);
408        if (providerOverlayVisible) {
409            log("  - provider overlay VISIBLE: "
410                  + providerLabel + " / "
411                  + providerIcon  + " / "
412                  + providerGatewayUri + " / "
413                  + providerAddress);
414        } else {
415            log("  - provider overlay: none");
416        }
417        log("  - latestActiveCallOrigin: " + latestActiveCallOrigin);
418    }
419
420    private static void log(String msg) {
421        Log.d(TAG, msg);
422    }
423}
424