CallCardPresenter.java revision b6ec8a55702f69c1bcb7b3eb1646c363ad9b4d10
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.ContentUris;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.drawable.Drawable;
23import android.net.Uri;
24import android.provider.ContactsContract.Contacts;
25import android.text.TextUtils;
26
27import com.android.incallui.InCallPresenter.InCallState;
28import com.android.incallui.InCallPresenter.InCallStateListener;
29
30import com.android.services.telephony.common.Call;
31
32/**
33 * Presenter for the Call Card Fragment.
34 * This class listens for changes to InCallState and passes it along to the fragment.
35 */
36public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> implements
37        InCallStateListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
38        ContactsAsyncHelper.OnImageLoadCompleteListener {
39
40    private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
41
42    private Context mContext;
43
44    /**
45     * Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded,
46     * or a photo is already loaded.
47     */
48    private Uri mLoadingPersonUri;
49
50    // Track the state for the photo.
51    private ContactsAsyncHelper.ImageTracker mPhotoTracker;
52
53    public CallCardPresenter() {
54        mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
55    }
56
57    @Override
58    public void onUiReady(CallCardUi ui) {
59        super.onUiReady(ui);
60    }
61
62    public void setContext(Context context) {
63        mContext = context;
64    }
65
66    @Override
67    public void onStateChange(InCallState state, CallList callList) {
68        final CallCardUi ui = getUi();
69
70        Call primary = null;
71        Call secondary = null;
72
73        if (state == InCallState.INCOMING) {
74            primary = callList.getIncomingCall();
75        } else if (state == InCallState.OUTGOING) {
76            primary = callList.getOutgoingCall();
77
78            // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
79            // highest priority call to display as the secondary call.
80            secondary = getCallToDisplay(callList, null);
81        } else if (state == InCallState.INCALL) {
82            primary = getCallToDisplay(callList, null);
83            secondary = getCallToDisplay(callList, primary);
84        }
85
86        Logger.d(this, "Primary call: " + primary);
87        Logger.d(this, "Secondary call: " + secondary);
88
89
90        if (primary != null) {
91            // Set primary call data
92            final CallerInfo primaryCallInfo = CallerInfoUtils.getCallerInfoForCall(mContext,
93                    primary, null, this);
94            updateDisplayByCallerInfo(primary, primaryCallInfo, primary.getNumberPresentation(),
95                    true);
96
97            ui.setNumber(primary.getNumber());
98            ui.setCallState(primary.getState(), primary.getDisconnectCause());
99        } else {
100            ui.setNumber("");
101            ui.setCallState(Call.State.INVALID, Call.DisconnectCause.UNKNOWN);
102        }
103
104        // Set secondary call data
105        if (secondary != null) {
106            ui.setSecondaryCallInfo(true, secondary.getNumber());
107        } else {
108            ui.setSecondaryCallInfo(false, null);
109        }
110    }
111
112    /**
113     * Get the highest priority call to display.
114     * Goes through the calls and chooses which to return based on priority of which type of call
115     * to display to the user. Callers can use the "ignore" feature to get the second best call
116     * by passing a previously found primary call as ignore.
117     *
118     * @param ignore A call to ignore if found.
119     */
120    private Call getCallToDisplay(CallList callList, Call ignore) {
121
122        // Disconnected calls get primary position to let user know quickly
123        // what call has disconnected. Disconnected calls are very short lived.
124        Call retval = callList.getDisconnectedCall();
125        if (retval != null && retval != ignore) {
126            return retval;
127        }
128
129        // Active calls come second.  An active call always gets precedent.
130        retval = callList.getActiveCall();
131        if (retval != null && retval != ignore) {
132            return retval;
133        }
134
135        // Then we go to background call (calls on hold)
136        retval = callList.getBackgroundCall();
137        if (retval != null && retval != ignore) {
138            return retval;
139        }
140
141        // Lastly, we go to a second background call.
142        retval = callList.getSecondBackgroundCall();
143
144        return retval;
145    }
146
147    public interface CallCardUi extends Ui {
148        // TODO(klp): Consider passing in the Call object directly in these methods.
149        void setVisible(boolean on);
150        void setNumber(String number);
151        void setNumberLabel(String label);
152        void setName(String name);
153        void setName(String name, boolean isNumber);
154        void setImage(int resource);
155        void setImage(Drawable drawable);
156        void setImage(Bitmap bitmap);
157        void setSecondaryCallInfo(boolean show, String number);
158        void setCallState(int state, Call.DisconnectCause cause);
159    }
160
161    @Override
162    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
163        if (cookie instanceof Call) {
164            final Call call = (Call) cookie;
165            if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) {
166                updateDisplayByCallerInfo(call, ci, Call.PRESENTATION_ALLOWED, true);
167            } else {
168                // If the contact doesn't exist, we can still use information from the
169                // returned caller info (geodescription, etc).
170                updateDisplayByCallerInfo(call, ci, call.getNumberPresentation(), true);
171            }
172
173            // Todo (klp): updatePhotoForCallState(call);
174        }
175    }
176
177    /**
178     * Based on the given caller info, determine a suitable name, phone number and label
179     * to be passed to the CallCardUI.
180     *
181     * If the current call is a conference call, use
182     * updateDisplayForConference() instead.
183     */
184    private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation,
185            boolean isPrimary) {
186
187        // Inform the state machine that we are displaying a photo.
188        mPhotoTracker.setPhotoRequest(info);
189        mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
190
191        // The actual strings we're going to display onscreen:
192        String displayName;
193        String displayNumber = null;
194        String label = null;
195        Uri personUri = null;
196
197        // Gather missing info unless the call is generic, in which case we wouldn't use
198        // the gathered information anyway.
199        if (info != null) {
200
201            // It appears that there is a small change in behaviour with the
202            // PhoneUtils' startGetCallerInfo whereby if we query with an
203            // empty number, we will get a valid CallerInfo object, but with
204            // fields that are all null, and the isTemporary boolean input
205            // parameter as true.
206
207            // In the past, we would see a NULL callerinfo object, but this
208            // ends up causing null pointer exceptions elsewhere down the
209            // line in other cases, so we need to make this fix instead. It
210            // appears that this was the ONLY call to PhoneUtils
211            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
212            // an unknown contact.
213
214            // Currently, infi.phoneNumber may actually be a SIP address, and
215            // if so, it might sometimes include the "sip:" prefix. That
216            // prefix isn't really useful to the user, though, so strip it off
217            // if present. (For any other URI scheme, though, leave the
218            // prefix alone.)
219            // TODO: It would be cleaner for CallerInfo to explicitly support
220            // SIP addresses instead of overloading the "phoneNumber" field.
221            // Then we could remove this hack, and instead ask the CallerInfo
222            // for a "user visible" form of the SIP address.
223            String number = info.phoneNumber;
224            if ((number != null) && number.startsWith("sip:")) {
225                number = number.substring(4);
226            }
227
228            if (TextUtils.isEmpty(info.name)) {
229                // No valid "name" in the CallerInfo, so fall back to
230                // something else.
231                // (Typically, we promote the phone number up to the "name" slot
232                // onscreen, and possibly display a descriptive string in the
233                // "number" slot.)
234                if (TextUtils.isEmpty(number)) {
235                    // No name *or* number! Display a generic "unknown" string
236                    // (or potentially some other default based on the presentation.)
237                    displayName = getPresentationString(presentation);
238                    Logger.d(this, "  ==> no name *or* number! displayName = " + displayName);
239                } else if (presentation != Call.PRESENTATION_ALLOWED) {
240                    // This case should never happen since the network should never send a phone #
241                    // AND a restricted presentation. However we leave it here in case of weird
242                    // network behavior
243                    displayName = getPresentationString(presentation);
244                    Logger.d(this, "  ==> presentation not allowed! displayName = " + displayName);
245                } else if (!TextUtils.isEmpty(info.cnapName)) {
246                    // No name, but we do have a valid CNAP name, so use that.
247                    displayName = info.cnapName;
248                    info.name = info.cnapName;
249                    displayNumber = number;
250                    Logger.d(this, "  ==> cnapName available: displayName '"
251                            + displayName + "', displayNumber '" + displayNumber + "'");
252                } else {
253                    // No name; all we have is a number. This is the typical
254                    // case when an incoming call doesn't match any contact,
255                    // or if you manually dial an outgoing number using the
256                    // dialpad.
257
258                    // Promote the phone number up to the "name" slot:
259                    displayName = number;
260
261                    // ...and use the "number" slot for a geographical description
262                    // string if available (but only for incoming calls.)
263                    if ((call != null) && (call.getState() == Call.State.INCOMING)) {
264                        // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
265                        // query to only do the geoDescription lookup in the first
266                        // place for incoming calls.
267                        displayNumber = info.geoDescription; // may be null
268                        Logger.d(this, "Geodescrption: " + info.geoDescription);
269                    }
270
271                    Logger.d(this, "  ==>  no name; falling back to number: displayName '"
272                            + displayName + "', displayNumber '" + displayNumber + "'");
273                }
274            } else {
275                // We do have a valid "name" in the CallerInfo. Display that
276                // in the "name" slot, and the phone number in the "number" slot.
277                if (presentation != Call.PRESENTATION_ALLOWED) {
278                    // This case should never happen since the network should never send a name
279                    // AND a restricted presentation. However we leave it here in case of weird
280                    // network behavior
281                    displayName = getPresentationString(presentation);
282                    Logger.d(this, "  ==> valid name, but presentation not allowed!"
283                            + " displayName = " + displayName);
284                } else {
285                    displayName = info.name;
286                    displayNumber = number;
287                    label = info.phoneLabel;
288                    Logger.d(this, "  ==>  name is present in CallerInfo: displayName '"
289                            + displayName + "', displayNumber '" + displayNumber + "'");
290                }
291            }
292            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
293            Logger.d(this, "- got personUri: '" + personUri
294                    + "', based on info.person_id: " + info.person_id);
295        } else {
296            displayName = getPresentationString(presentation);
297        }
298
299        // TODO (klp): Update secondary user call info as well.
300        if (isPrimary) {
301            updateInfoUiForPrimary(displayName, displayNumber, label);
302        }
303
304        // If the photoResource is filled in for the CallerInfo, (like with the
305        // Emergency Number case), then we can just set the photo image without
306        // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
307        // for cases where CallerInfo.photoResource may be set. We can also avoid
308        // the image load step if the image data is cached.
309        final CallCardUi ui = getUi();
310        if (info == null) return;
311
312        // This will only be true for emergency numbers
313        if (info.photoResource != 0) {
314            ui.setImage(info.photoResource);
315        } else if (info.isCachedPhotoCurrent) {
316            if (info.cachedPhoto != null) {
317                ui.setImage(info.cachedPhoto);
318            } else {
319                ui.setImage(R.drawable.picture_unknown);
320            }
321        } else {
322            if (personUri == null) {
323                Logger.v(this, "personUri is null. Just use unknown picture.");
324                ui.setImage(R.drawable.picture_unknown);
325            } else if (personUri.equals(mLoadingPersonUri)) {
326                Logger.v(this, "The requested Uri (" + personUri + ") is being loaded already."
327                        + " Ignore the duplicate load request.");
328            } else {
329                // Remember which person's photo is being loaded right now so that we won't issue
330                // unnecessary load request multiple times, which will mess up animation around
331                // the contact photo.
332                mLoadingPersonUri = personUri;
333
334                // Load the image with a callback to update the image state.
335                // When the load is finished, onImageLoadComplete() will be called.
336                ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
337                        mContext, personUri, this, call);
338
339                // If the image load is too slow, we show a default avatar icon afterward.
340                // If it is fast enough, this message will be canceled on onImageLoadComplete().
341                // TODO (klp): Figure out if this handler is still needed.
342                // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
343                // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
344            }
345        }
346        // TODO (klp): Update other fields - photo, sip label, etc.
347    }
348
349    /**
350     * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
351     * make sure that the call state is reflected after the image is loaded.
352     */
353    @Override
354    public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
355        // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
356        if (mLoadingPersonUri != null) {
357            // Start sending view notification after the current request being done.
358            // New image may possibly be available from the next phone calls.
359            //
360            // TODO: may be nice to update the image view again once the newer one
361            // is available on contacts database.
362            // TODO (klp): What is this, and why does it need the write_contacts permission?
363            // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri);
364        } else {
365            // This should not happen while we need some verbose info if it happens..
366            Logger.v(this, "Person Uri isn't available while Image is successfully loaded.");
367        }
368        mLoadingPersonUri = null;
369
370        Call call = (Call) cookie;
371
372        // TODO (klp): Handle conference calls
373
374        final CallCardUi ui = getUi();
375        if (photo != null) {
376            ui.setImage(photo);
377        } else if (photoIcon != null) {
378            ui.setImage(photoIcon);
379        } else {
380            ui.setImage(R.drawable.picture_unknown);
381        }
382    }
383
384    /**
385     * Updates the info portion of the call card with passed in values for the primary user.
386     */
387    private void updateInfoUiForPrimary(String displayName, String displayNumber, String label) {
388        final CallCardUi ui = getUi();
389        ui.setName(displayName);
390        ui.setNumber(displayNumber);
391        ui.setNumberLabel(label);
392    }
393
394    public String getPresentationString(int presentation) {
395        String name = mContext.getString(R.string.unknown);
396        if (presentation == Call.PRESENTATION_RESTRICTED) {
397            name = mContext.getString(R.string.private_num);
398        } else if (presentation == Call.PRESENTATION_PAYPHONE) {
399            name = mContext.getString(R.string.payphone);
400        }
401        return name;
402    }
403}
404