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