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