CallCardPresenter.java revision b48e2280930b5ed0b04dd34d59dc5980b379a475
1/*
2 * Copyright (C) 2013 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.incallui;
18
19import android.content.Context;
20import android.content.pm.ApplicationInfo;
21import android.content.pm.PackageManager;
22import android.graphics.drawable.Drawable;
23import android.graphics.Bitmap;
24import android.net.wifi.WifiInfo;
25import android.net.wifi.WifiManager;
26import android.telecomm.CallCapabilities;
27import android.telephony.DisconnectCause;
28import android.telephony.PhoneNumberUtils;
29import android.text.TextUtils;
30import android.text.format.DateUtils;
31
32import com.android.incallui.AudioModeProvider.AudioModeListener;
33import com.android.incallui.ContactInfoCache.ContactCacheEntry;
34import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
35import com.android.incallui.InCallPresenter.InCallState;
36import com.android.incallui.InCallPresenter.InCallStateListener;
37import com.android.incallui.InCallPresenter.IncomingCallListener;
38import com.android.services.telephony.common.AudioMode;
39import com.google.common.base.Preconditions;
40
41/**
42 * Presenter for the Call Card Fragment.
43 * <p>
44 * This class listens for changes to InCallState and passes it along to the fragment.
45 */
46public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
47        implements InCallStateListener, AudioModeListener, IncomingCallListener {
48
49    private static final String TAG = CallCardPresenter.class.getSimpleName();
50    private static final long CALL_TIME_UPDATE_INTERVAL = 1000; // in milliseconds
51
52    private Call mPrimary;
53    private Call mSecondary;
54    private ContactCacheEntry mPrimaryContactInfo;
55    private ContactCacheEntry mSecondaryContactInfo;
56    private CallTimer mCallTimer;
57    private Context mContext;
58
59    public CallCardPresenter() {
60        // create the call timer
61        mCallTimer = new CallTimer(new Runnable() {
62            @Override
63            public void run() {
64                updateCallTime();
65            }
66        });
67    }
68
69
70    public void init(Context context, Call call) {
71        mContext = Preconditions.checkNotNull(context);
72
73        // Call may be null if disconnect happened already.
74        if (call != null) {
75            mPrimary = call;
76
77            // start processing lookups right away.
78            if (!call.isConferenceCall()) {
79                startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING);
80            } else {
81                updateContactEntry(null, true, true);
82            }
83        }
84    }
85
86    @Override
87    public void onUiReady(CallCardUi ui) {
88        super.onUiReady(ui);
89
90        AudioModeProvider.getInstance().addListener(this);
91
92        // Contact search may have completed before ui is ready.
93        if (mPrimaryContactInfo != null) {
94            updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
95        }
96
97        // Register for call state changes last
98        InCallPresenter.getInstance().addListener(this);
99        InCallPresenter.getInstance().addIncomingCallListener(this);
100    }
101
102    @Override
103    public void onUiUnready(CallCardUi ui) {
104        super.onUiUnready(ui);
105
106        // stop getting call state changes
107        InCallPresenter.getInstance().removeListener(this);
108        InCallPresenter.getInstance().removeIncomingCallListener(this);
109
110        AudioModeProvider.getInstance().removeListener(this);
111
112        mPrimary = null;
113        mPrimaryContactInfo = null;
114        mSecondaryContactInfo = null;
115    }
116
117    @Override
118    public void onIncomingCall(InCallState state, Call call) {
119        // same logic should happen as with onStateChange()
120        onStateChange(state, CallList.getInstance());
121    }
122
123    @Override
124    public void onStateChange(InCallState state, CallList callList) {
125        Log.d(this, "onStateChange() " + state);
126        final CallCardUi ui = getUi();
127        if (ui == null) {
128            return;
129        }
130
131        Call primary = null;
132        Call secondary = null;
133
134        if (state == InCallState.INCOMING) {
135            primary = callList.getIncomingCall();
136        } else if (state == InCallState.OUTGOING) {
137            primary = callList.getOutgoingCall();
138
139            // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
140            // highest priority call to display as the secondary call.
141            secondary = getCallToDisplay(callList, null, true);
142        } else if (state == InCallState.INCALL) {
143            primary = getCallToDisplay(callList, null, false);
144            secondary = getCallToDisplay(callList, primary, true);
145        }
146
147        Log.d(this, "Primary call: " + primary);
148        Log.d(this, "Secondary call: " + secondary);
149
150        final boolean primaryChanged = !areCallsSame(mPrimary, primary);
151        final boolean secondaryChanged = !areCallsSame(mSecondary, secondary);
152        mSecondary = secondary;
153        mPrimary = primary;
154
155        if (primaryChanged && mPrimary != null) {
156            // primary call has changed
157            mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
158                    mPrimary.getState() == Call.State.INCOMING);
159            updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
160            maybeStartSearch(mPrimary, true);
161        }
162
163        if (mSecondary == null) {
164            // Secondary call may have ended.  Update the ui.
165            mSecondaryContactInfo = null;
166            updateSecondaryDisplayInfo(false);
167        } else if (secondaryChanged) {
168            // secondary call has changed
169            mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
170                    mSecondary.getState() == Call.State.INCOMING);
171            updateSecondaryDisplayInfo(mSecondary.isConferenceCall());
172            maybeStartSearch(mSecondary, false);
173        }
174
175        // Start/Stop the call time update timer
176        if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) {
177            Log.d(this, "Starting the calltime timer");
178            mCallTimer.start(CALL_TIME_UPDATE_INTERVAL);
179        } else {
180            Log.d(this, "Canceling the calltime timer");
181            mCallTimer.cancel();
182            ui.setPrimaryCallElapsedTime(false, null);
183        }
184
185        // Set the call state
186        if (mPrimary != null) {
187            final boolean bluetoothOn =
188                    (AudioModeProvider.getInstance().getAudioMode() == AudioMode.BLUETOOTH);
189            ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn,
190                    getGatewayLabel(), getGatewayNumber(), getWifiConnection());
191        } else {
192            ui.setCallState(Call.State.IDLE, DisconnectCause.NOT_VALID, false, null, null, null);
193        }
194    }
195
196    @Override
197    public void onAudioMode(int mode) {
198        if (mPrimary != null && getUi() != null) {
199            final boolean bluetoothOn = (AudioMode.BLUETOOTH == mode);
200
201            getUi().setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn,
202                    getGatewayLabel(), getGatewayNumber(), getWifiConnection());
203        }
204    }
205
206    private String getWifiConnection() {
207        if (mPrimary.isWifiCall()) {
208            final WifiManager wifiManager = (WifiManager) mContext.getSystemService(
209                    Context.WIFI_SERVICE);
210            final WifiInfo info = wifiManager.getConnectionInfo();
211            if (info != null) {
212                return info.getSSID();
213            }
214        }
215        return null;
216    }
217
218    @Override
219    public void onSupportedAudioMode(int mask) {
220    }
221
222    @Override
223    public void onMute(boolean muted) {
224    }
225
226    public void updateCallTime() {
227        final CallCardUi ui = getUi();
228
229        if (ui == null || mPrimary == null || mPrimary.getState() != Call.State.ACTIVE) {
230            if (ui != null) {
231                ui.setPrimaryCallElapsedTime(false, null);
232            }
233            mCallTimer.cancel();
234        } else {
235            final long callStart = mPrimary.getConnectTime();
236            final long duration = System.currentTimeMillis() - callStart;
237            ui.setPrimaryCallElapsedTime(true, DateUtils.formatElapsedTime(duration / 1000));
238        }
239    }
240
241    private boolean areCallsSame(Call call1, Call call2) {
242        if (call1 == null && call2 == null) {
243            return true;
244        } else if (call1 == null || call2 == null) {
245            return false;
246        }
247
248        // otherwise compare call Ids
249        return call1.getCallId().equals(call2.getCallId());
250    }
251
252    private void maybeStartSearch(Call call, boolean isPrimary) {
253        // no need to start search for conference calls which show generic info.
254        if (call != null && !call.isConferenceCall()) {
255            startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING);
256        }
257    }
258
259    /**
260     * Starts a query for more contact data for the save primary and secondary calls.
261     */
262    private void startContactInfoSearch(final Call call, final boolean isPrimary,
263            boolean isIncoming) {
264        final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
265
266        cache.findInfo(call, isIncoming, new ContactInfoCacheCallback() {
267                @Override
268                public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
269                    updateContactEntry(entry, isPrimary, false);
270                    if (entry.name != null) {
271                        Log.d(TAG, "Contact found: " + entry);
272                    }
273                    if (entry.personUri != null) {
274                        CallerInfoUtils.sendViewNotification(mContext, entry.personUri);
275                    }
276                }
277
278                @Override
279                public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
280                    if (getUi() == null) {
281                        return;
282                    }
283                    if (entry.photo != null) {
284                        if (mPrimary != null && callId.equals(mPrimary.getCallId())) {
285                            getUi().setPrimaryImage(entry.photo);
286                        } else if (mSecondary != null && callId.equals(mSecondary.getCallId())) {
287                            getUi().setSecondaryImage(entry.photo);
288                        }
289                    }
290                }
291            });
292    }
293
294    private static boolean isConference(Call call) {
295        return call != null && call.isConferenceCall();
296    }
297
298    private static boolean isGenericConference(Call call) {
299        return call != null && call.can(CallCapabilities.GENERIC_CONFERENCE);
300    }
301
302    private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary,
303            boolean isConference) {
304        if (isPrimary) {
305            mPrimaryContactInfo = entry;
306            updatePrimaryDisplayInfo(entry, isConference);
307        } else {
308            mSecondaryContactInfo = entry;
309            updateSecondaryDisplayInfo(isConference);
310        }
311    }
312
313    /**
314     * Get the highest priority call to display.
315     * Goes through the calls and chooses which to return based on priority of which type of call
316     * to display to the user. Callers can use the "ignore" feature to get the second best call
317     * by passing a previously found primary call as ignore.
318     *
319     * @param ignore A call to ignore if found.
320     */
321    private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
322
323        // Active calls come second.  An active call always gets precedent.
324        Call retval = callList.getActiveCall();
325        if (retval != null && retval != ignore) {
326            return retval;
327        }
328
329        // Disconnected calls get primary position if there are no active calls
330        // to let user know quickly what call has disconnected. Disconnected
331        // calls are very short lived.
332        if (!skipDisconnected) {
333            retval = callList.getDisconnectingCall();
334            if (retval != null && retval != ignore) {
335                return retval;
336            }
337            retval = callList.getDisconnectedCall();
338            if (retval != null && retval != ignore) {
339                return retval;
340            }
341        }
342
343        // Then we go to background call (calls on hold)
344        retval = callList.getBackgroundCall();
345        if (retval != null && retval != ignore) {
346            return retval;
347        }
348
349        // Lastly, we go to a second background call.
350        retval = callList.getSecondBackgroundCall();
351
352        return retval;
353    }
354
355    private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) {
356        Log.d(TAG, "Update primary display " + entry);
357        final CallCardUi ui = getUi();
358        if (ui == null) {
359            // TODO: May also occur if search result comes back after ui is destroyed. Look into
360            // removing that case completely.
361            Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
362            return;
363        }
364
365        final boolean isGenericConf = isGenericConference(mPrimary);
366        if (entry != null) {
367            final String name = getNameForCall(entry);
368            final String number = getNumberForCall(entry);
369            final boolean nameIsNumber = name != null && name.equals(entry.number);
370            ui.setPrimary(number, name, nameIsNumber, entry.label,
371                    entry.photo, isConference, isGenericConf, entry.isSipCall);
372        } else {
373            ui.setPrimary(null, null, false, null, null, isConference, isGenericConf, false);
374        }
375
376    }
377
378    private void updateSecondaryDisplayInfo(boolean isConference) {
379
380        final CallCardUi ui = getUi();
381        if (ui == null) {
382            return;
383        }
384
385        final boolean isGenericConf = isGenericConference(mSecondary);
386        if (mSecondaryContactInfo != null) {
387            Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
388            final String nameForCall = getNameForCall(mSecondaryContactInfo);
389
390            final boolean nameIsNumber = nameForCall != null && nameForCall.equals(
391                    mSecondaryContactInfo.number);
392            ui.setSecondary(true, nameForCall, nameIsNumber, mSecondaryContactInfo.label,
393                    mSecondaryContactInfo.photo, isConference, isGenericConf);
394        } else {
395            // reset to nothing so that it starts off blank next time we use it.
396            ui.setSecondary(false, null, false, null, null, isConference, isGenericConf);
397        }
398    }
399
400    /**
401     * Returns the gateway number for any existing outgoing call.
402     */
403    private String getGatewayNumber() {
404        if (hasOutgoingGatewayCall()) {
405            return mPrimary.getGatewayInfo().getGatewayHandle().getSchemeSpecificPart();
406        }
407        return null;
408    }
409
410    /**
411     * Returns the label for the gateway app for any existing outgoing call.
412     */
413    private String getGatewayLabel() {
414        if (hasOutgoingGatewayCall() && getUi() != null) {
415            final PackageManager pm = mContext.getPackageManager();
416            try {
417                ApplicationInfo info = pm.getApplicationInfo(
418                        mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
419                return mContext.getString(R.string.calling_via_template,
420                        pm.getApplicationLabel(info).toString());
421            } catch (PackageManager.NameNotFoundException e) {
422            }
423        }
424        return null;
425    }
426
427    private boolean hasOutgoingGatewayCall() {
428        // We only display the gateway information while DIALING so return false for any othe
429        // call state.
430        // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
431        // is also called after a contact search completes (call is not present yet).  Split the
432        // UI update so it can receive independent updates.
433        if (mPrimary == null) {
434            return false;
435        }
436        return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null &&
437                !mPrimary.getGatewayInfo().isEmpty();
438    }
439
440    /**
441     * Gets the name to display for the call.
442     */
443    private static String getNameForCall(ContactCacheEntry contactInfo) {
444        if (TextUtils.isEmpty(contactInfo.name)) {
445            return contactInfo.number;
446        }
447        return contactInfo.name;
448    }
449
450    /**
451     * Gets the number to display for a call.
452     */
453    private static String getNumberForCall(ContactCacheEntry contactInfo) {
454        // If the name is empty, we use the number for the name...so dont show a second
455        // number in the number field
456        if (TextUtils.isEmpty(contactInfo.name)) {
457            return contactInfo.location;
458        }
459        return contactInfo.number;
460    }
461
462    public void secondaryPhotoClicked() {
463        if (mSecondary == null) {
464            Log.wtf(this, "Secondary photo clicked but no secondary call.");
465            return;
466        }
467
468        Log.i(this, "Swapping call to foreground: " + mSecondary);
469        TelecommAdapter.getInstance().unholdCall(mSecondary.getCallId());
470    }
471
472    public interface CallCardUi extends Ui {
473        void setVisible(boolean on);
474        void setPrimary(String number, String name, boolean nameIsNumber, String label,
475                Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall);
476        void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
477                Drawable photo, boolean isConference, boolean isGeneric);
478        void setSecondaryImage(Drawable image);
479        void setCallState(int state, int cause, boolean bluetoothOn,
480                String gatewayLabel, String gatewayNumber, String wifiConnection);
481        void setPrimaryCallElapsedTime(boolean show, String duration);
482        void setPrimaryName(String name, boolean nameIsNumber);
483        void setPrimaryImage(Drawable image);
484        void setPrimaryPhoneNumber(String phoneNumber);
485        void setPrimaryLabel(String label);
486    }
487}
488