CallCardPresenter.java revision e7be13fb0556e62b07bc271b130412d82d7f7521
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            // Safe to assume that the primary call is valid since we would not be in the
79            // OUTGOING state without an outgoing call.
80            secondary = callList.getBackgroundCall();
81        } else if (state == InCallState.INCALL) {
82            primary = callList.getActiveCall();
83            if (primary != null) {
84                secondary = callList.getBackgroundCall();
85            } else {
86                primary = callList.getBackgroundCall();
87                secondary = callList.getSecondBackgroundCall();
88            }
89        }
90
91        Logger.d(this, "Primary call: " + primary);
92        Logger.d(this, "Secondary call: " + secondary);
93
94        // Set primary call data
95        final CallerInfo primaryCallInfo = CallerInfoUtils.getCallerInfoForCall(mContext, primary,
96                null, this);
97        updateDisplayByCallerInfo(primary, primaryCallInfo, primary.getNumberPresentation(), true);
98
99        // Set secondary call data
100        if (secondary != null) {
101            ui.setSecondaryCallInfo(true, secondary.getNumber());
102        } else {
103            ui.setSecondaryCallInfo(false, null);
104        }
105    }
106
107    public interface CallCardUi extends Ui {
108        // TODO(klp): Consider passing in the Call object directly in these methods.
109        void setVisible(boolean on);
110        void setNumber(String number);
111        void setNumberLabel(String label);
112        void setName(String name);
113        void setName(String name, boolean isNumber);
114        void setImage(int resource);
115        void setImage(Drawable drawable);
116        void setImage(Bitmap bitmap);
117        void setSecondaryCallInfo(boolean show, String number);
118    }
119
120    @Override
121    public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
122        if (cookie instanceof Call) {
123            final Call call = (Call) cookie;
124            if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) {
125                updateDisplayByCallerInfo(call, ci, Call.PRESENTATION_ALLOWED, true);
126            } else {
127                // If the contact doesn't exist, we can still use information from the
128                // returned caller info (geodescription, etc).
129                updateDisplayByCallerInfo(call, ci, call.getNumberPresentation(), true);
130            }
131
132            // Todo (klp): updatePhotoForCallState(call);
133        }
134    }
135
136    /**
137     * Based on the given caller info, determine a suitable name, phone number and label
138     * to be passed to the CallCardUI.
139     *
140     * If the current call is a conference call, use
141     * updateDisplayForConference() instead.
142     */
143    private void updateDisplayByCallerInfo(Call call, CallerInfo info, int presentation,
144            boolean isPrimary) {
145
146        // Inform the state machine that we are displaying a photo.
147        mPhotoTracker.setPhotoRequest(info);
148        mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
149
150        // The actual strings we're going to display onscreen:
151        String displayName;
152        String displayNumber = null;
153        String label = null;
154        Uri personUri = null;
155
156        // Gather missing info unless the call is generic, in which case we wouldn't use
157        // the gathered information anyway.
158        if (info != null) {
159
160            // It appears that there is a small change in behaviour with the
161            // PhoneUtils' startGetCallerInfo whereby if we query with an
162            // empty number, we will get a valid CallerInfo object, but with
163            // fields that are all null, and the isTemporary boolean input
164            // parameter as true.
165
166            // In the past, we would see a NULL callerinfo object, but this
167            // ends up causing null pointer exceptions elsewhere down the
168            // line in other cases, so we need to make this fix instead. It
169            // appears that this was the ONLY call to PhoneUtils
170            // .getCallerInfo() that relied on a NULL CallerInfo to indicate
171            // an unknown contact.
172
173            // Currently, infi.phoneNumber may actually be a SIP address, and
174            // if so, it might sometimes include the "sip:" prefix. That
175            // prefix isn't really useful to the user, though, so strip it off
176            // if present. (For any other URI scheme, though, leave the
177            // prefix alone.)
178            // TODO: It would be cleaner for CallerInfo to explicitly support
179            // SIP addresses instead of overloading the "phoneNumber" field.
180            // Then we could remove this hack, and instead ask the CallerInfo
181            // for a "user visible" form of the SIP address.
182            String number = info.phoneNumber;
183            if ((number != null) && number.startsWith("sip:")) {
184                number = number.substring(4);
185            }
186
187            if (TextUtils.isEmpty(info.name)) {
188                // No valid "name" in the CallerInfo, so fall back to
189                // something else.
190                // (Typically, we promote the phone number up to the "name" slot
191                // onscreen, and possibly display a descriptive string in the
192                // "number" slot.)
193                if (TextUtils.isEmpty(number)) {
194                    // No name *or* number! Display a generic "unknown" string
195                    // (or potentially some other default based on the presentation.)
196                    displayName = getPresentationString(presentation);
197                    Logger.d(this, "  ==> no name *or* number! displayName = " + displayName);
198                } else if (presentation != Call.PRESENTATION_ALLOWED) {
199                    // This case should never happen since the network should never send a phone #
200                    // AND a restricted presentation. However we leave it here in case of weird
201                    // network behavior
202                    displayName = getPresentationString(presentation);
203                    Logger.d(this, "  ==> presentation not allowed! displayName = " + displayName);
204                } else if (!TextUtils.isEmpty(info.cnapName)) {
205                    // No name, but we do have a valid CNAP name, so use that.
206                    displayName = info.cnapName;
207                    info.name = info.cnapName;
208                    displayNumber = number;
209                    Logger.d(this, "  ==> cnapName available: displayName '"
210                            + displayName + "', displayNumber '" + displayNumber + "'");
211                } else {
212                    // No name; all we have is a number. This is the typical
213                    // case when an incoming call doesn't match any contact,
214                    // or if you manually dial an outgoing number using the
215                    // dialpad.
216
217                    // Promote the phone number up to the "name" slot:
218                    displayName = number;
219
220                    // ...and use the "number" slot for a geographical description
221                    // string if available (but only for incoming calls.)
222                    if ((call != null) && (call.getState() == Call.State.INCOMING)) {
223                        // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
224                        // query to only do the geoDescription lookup in the first
225                        // place for incoming calls.
226                        displayNumber = info.geoDescription; // may be null
227                        Logger.d(this, "Geodescrption: " + info.geoDescription);
228                    }
229
230                    Logger.d(this, "  ==>  no name; falling back to number: displayName '"
231                            + displayName + "', displayNumber '" + displayNumber + "'");
232                }
233            } else {
234                // We do have a valid "name" in the CallerInfo. Display that
235                // in the "name" slot, and the phone number in the "number" slot.
236                if (presentation != Call.PRESENTATION_ALLOWED) {
237                    // This case should never happen since the network should never send a name
238                    // AND a restricted presentation. However we leave it here in case of weird
239                    // network behavior
240                    displayName = getPresentationString(presentation);
241                    Logger.d(this, "  ==> valid name, but presentation not allowed!"
242                            + " displayName = " + displayName);
243                } else {
244                    displayName = info.name;
245                    displayNumber = number;
246                    label = info.phoneLabel;
247                    Logger.d(this, "  ==>  name is present in CallerInfo: displayName '"
248                            + displayName + "', displayNumber '" + displayNumber + "'");
249                }
250            }
251            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
252            Logger.d(this, "- got personUri: '" + personUri
253                    + "', based on info.person_id: " + info.person_id);
254        } else {
255            displayName = getPresentationString(presentation);
256        }
257
258        // TODO (klp): Update secondary user call info as well.
259        if (isPrimary) {
260            updateInfoUiForPrimary(displayName, displayNumber, label);
261        }
262
263        // If the photoResource is filled in for the CallerInfo, (like with the
264        // Emergency Number case), then we can just set the photo image without
265        // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
266        // for cases where CallerInfo.photoResource may be set. We can also avoid
267        // the image load step if the image data is cached.
268        final CallCardUi ui = getUi();
269        if (info == null) return;
270
271        // This will only be true for emergency numbers
272        if (info.photoResource != 0) {
273            ui.setImage(info.photoResource);
274        } else if (info.isCachedPhotoCurrent) {
275            if (info.cachedPhoto != null) {
276                ui.setImage(info.cachedPhoto);
277            } else {
278                ui.setImage(R.drawable.picture_unknown);
279            }
280        } else {
281            if (personUri == null) {
282                Logger.v(this, "personUri is null. Just use unknown picture.");
283                ui.setImage(R.drawable.picture_unknown);
284            } else if (personUri.equals(mLoadingPersonUri)) {
285                Logger.v(this, "The requested Uri (" + personUri + ") is being loaded already."
286                        + " Ignore the duplicate load request.");
287            } else {
288                // Remember which person's photo is being loaded right now so that we won't issue
289                // unnecessary load request multiple times, which will mess up animation around
290                // the contact photo.
291                mLoadingPersonUri = personUri;
292
293                // Load the image with a callback to update the image state.
294                // When the load is finished, onImageLoadComplete() will be called.
295                ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
296                        mContext, personUri, this, call);
297
298                // If the image load is too slow, we show a default avatar icon afterward.
299                // If it is fast enough, this message will be canceled on onImageLoadComplete().
300                // TODO (klp): Figure out if this handler is still needed.
301                // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
302                // mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
303            }
304        }
305        // TODO (klp): Update other fields - photo, sip label, etc.
306    }
307
308    /**
309     * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
310     * make sure that the call state is reflected after the image is loaded.
311     */
312    @Override
313    public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
314        // mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
315        if (mLoadingPersonUri != null) {
316            // Start sending view notification after the current request being done.
317            // New image may possibly be available from the next phone calls.
318            //
319            // TODO: may be nice to update the image view again once the newer one
320            // is available on contacts database.
321            // TODO (klp): What is this, and why does it need the write_contacts permission?
322            // CallerInfoUtils.sendViewNotificationAsync(mContext, mLoadingPersonUri);
323        } else {
324            // This should not happen while we need some verbose info if it happens..
325            Logger.v(this, "Person Uri isn't available while Image is successfully loaded.");
326        }
327        mLoadingPersonUri = null;
328
329        Call call = (Call) cookie;
330
331        // TODO (klp): Handle conference calls
332
333        final CallCardUi ui = getUi();
334        if (photo != null) {
335            ui.setImage(photo);
336        } else if (photoIcon != null) {
337            ui.setImage(photoIcon);
338        } else {
339            ui.setImage(R.drawable.picture_unknown);
340        }
341    }
342
343    /**
344     * Updates the info portion of the call card with passed in values for the primary user.
345     */
346    private void updateInfoUiForPrimary(String displayName, String displayNumber, String label) {
347        final CallCardUi ui = getUi();
348        ui.setName(displayName);
349        ui.setNumber(displayNumber);
350        ui.setNumberLabel(label);
351    }
352
353    public String getPresentationString(int presentation) {
354        String name = mContext.getString(R.string.unknown);
355        if (presentation == Call.PRESENTATION_RESTRICTED) {
356            name = mContext.getString(R.string.private_num);
357        } else if (presentation == Call.PRESENTATION_PAYPHONE) {
358            name = mContext.getString(R.string.payphone);
359        }
360        return name;
361    }
362}
363