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