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