1/* 2 * Copyright (C) 2009 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.dialer; 18 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.content.Context; 23import android.content.Intent; 24import android.content.res.Resources; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.AsyncTask; 28import android.os.Bundle; 29import android.provider.CallLog; 30import android.provider.CallLog.Calls; 31import android.provider.ContactsContract.CommonDataKinds.Phone; 32import android.provider.VoicemailContract.Voicemails; 33import android.telecom.PhoneAccount; 34import android.telephony.TelephonyManager; 35import android.text.BidiFormatter; 36import android.text.TextDirectionHeuristics; 37import android.text.TextUtils; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.LayoutInflater; 41import android.view.Menu; 42import android.view.MenuItem; 43import android.view.View; 44import android.widget.LinearLayout; 45import android.widget.ListView; 46import android.widget.QuickContactBadge; 47import android.widget.TextView; 48import android.widget.Toast; 49 50import com.android.contacts.common.ContactPhotoManager; 51import com.android.contacts.common.CallUtil; 52import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 53import com.android.contacts.common.GeoUtil; 54import com.android.dialer.calllog.CallDetailHistoryAdapter; 55import com.android.dialer.calllog.CallTypeHelper; 56import com.android.dialer.calllog.ContactInfo; 57import com.android.dialer.calllog.ContactInfoHelper; 58import com.android.dialer.calllog.PhoneAccountUtils; 59import com.android.dialer.calllog.PhoneNumberDisplayHelper; 60import com.android.dialer.calllog.PhoneNumberUtilsWrapper; 61import com.android.dialer.util.AsyncTaskExecutor; 62import com.android.dialer.util.AsyncTaskExecutors; 63import com.android.dialer.util.DialerUtils; 64import com.android.dialer.voicemail.VoicemailPlaybackFragment; 65import com.android.dialer.voicemail.VoicemailStatusHelper; 66import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 67import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 68import com.android.dialerbind.analytics.AnalyticsActivity; 69 70import java.util.List; 71 72/** 73 * Displays the details of a specific call log entry. 74 * <p> 75 * This activity can be either started with the URI of a single call log entry, or with the 76 * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries. 77 */ 78public class CallDetailActivity extends AnalyticsActivity implements ProximitySensorAware { 79 private static final String TAG = "CallDetail"; 80 81 private static final int LOADER_ID = 0; 82 private static final String BUNDLE_CONTACT_URI_EXTRA = "contact_uri_extra"; 83 84 private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A'; 85 private static final char POP_DIRECTIONAL_FORMATTING = '\u202C'; 86 87 /** The time to wait before enabling the blank the screen due to the proximity sensor. */ 88 private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100; 89 /** The time to wait before disabling the blank the screen due to the proximity sensor. */ 90 private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500; 91 92 /** The enumeration of {@link AsyncTask} objects used in this class. */ 93 public enum Tasks { 94 MARK_VOICEMAIL_READ, 95 DELETE_VOICEMAIL_AND_FINISH, 96 REMOVE_FROM_CALL_LOG_AND_FINISH, 97 UPDATE_PHONE_CALL_DETAILS, 98 } 99 100 /** A long array extra containing ids of call log entries to display. */ 101 public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS"; 102 /** If we are started with a voicemail, we'll find the uri to play with this extra. */ 103 public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI"; 104 /** If we should immediately start playback of the voicemail, this extra will be set to true. */ 105 public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK"; 106 /** If the activity was triggered from a notification. */ 107 public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION"; 108 109 public static final String VOICEMAIL_FRAGMENT_TAG = "voicemail_fragment"; 110 111 private CallTypeHelper mCallTypeHelper; 112 private PhoneNumberDisplayHelper mPhoneNumberHelper; 113 private QuickContactBadge mQuickContactBadge; 114 private TextView mCallerName; 115 private TextView mCallerNumber; 116 private TextView mAccountLabel; 117 private AsyncTaskExecutor mAsyncTaskExecutor; 118 private ContactInfoHelper mContactInfoHelper; 119 120 private String mNumber = null; 121 private String mDefaultCountryIso; 122 123 /* package */ LayoutInflater mInflater; 124 /* package */ Resources mResources; 125 /** Helper to load contact photos. */ 126 private ContactPhotoManager mContactPhotoManager; 127 /** Helper to make async queries to content resolver. */ 128 private CallDetailActivityQueryHandler mAsyncQueryHandler; 129 /** Helper to get voicemail status messages. */ 130 private VoicemailStatusHelper mVoicemailStatusHelper; 131 // Views related to voicemail status message. 132 private View mStatusMessageView; 133 private TextView mStatusMessageText; 134 private TextView mStatusMessageAction; 135 private TextView mVoicemailTranscription; 136 private LinearLayout mVoicemailHeader; 137 138 private Uri mVoicemailUri; 139 private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); 140 141 /** Whether we should show "edit number before call" in the options menu. */ 142 private boolean mHasEditNumberBeforeCallOption; 143 /** Whether we should show "trash" in the options menu. */ 144 private boolean mHasTrashOption; 145 /** Whether we should show "remove from call log" in the options menu. */ 146 private boolean mHasRemoveFromCallLogOption; 147 148 private ProximitySensorManager mProximitySensorManager; 149 private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener(); 150 151 /** Listener to changes in the proximity sensor state. */ 152 private class ProximitySensorListener implements ProximitySensorManager.Listener { 153 /** Used to show a blank view and hide the action bar. */ 154 private final Runnable mBlankRunnable = new Runnable() { 155 @Override 156 public void run() { 157 View blankView = findViewById(R.id.blank); 158 blankView.setVisibility(View.VISIBLE); 159 getActionBar().hide(); 160 } 161 }; 162 /** Used to remove the blank view and show the action bar. */ 163 private final Runnable mUnblankRunnable = new Runnable() { 164 @Override 165 public void run() { 166 View blankView = findViewById(R.id.blank); 167 blankView.setVisibility(View.GONE); 168 getActionBar().show(); 169 } 170 }; 171 172 @Override 173 public synchronized void onNear() { 174 clearPendingRequests(); 175 postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS); 176 } 177 178 @Override 179 public synchronized void onFar() { 180 clearPendingRequests(); 181 postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS); 182 } 183 184 /** Removed any delayed requests that may be pending. */ 185 public synchronized void clearPendingRequests() { 186 View blankView = findViewById(R.id.blank); 187 blankView.removeCallbacks(mBlankRunnable); 188 blankView.removeCallbacks(mUnblankRunnable); 189 } 190 191 /** Post a {@link Runnable} with a delay on the main thread. */ 192 private synchronized void postDelayed(Runnable runnable, long delayMillis) { 193 // Post these instead of executing immediately so that: 194 // - They are guaranteed to be executed on the main thread. 195 // - If the sensor values changes rapidly for some time, the UI will not be 196 // updated immediately. 197 View blankView = findViewById(R.id.blank); 198 blankView.postDelayed(runnable, delayMillis); 199 } 200 } 201 202 static final String[] CALL_LOG_PROJECTION = new String[] { 203 CallLog.Calls.DATE, 204 CallLog.Calls.DURATION, 205 CallLog.Calls.NUMBER, 206 CallLog.Calls.TYPE, 207 CallLog.Calls.COUNTRY_ISO, 208 CallLog.Calls.GEOCODED_LOCATION, 209 CallLog.Calls.NUMBER_PRESENTATION, 210 CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, 211 CallLog.Calls.PHONE_ACCOUNT_ID, 212 CallLog.Calls.FEATURES, 213 CallLog.Calls.DATA_USAGE, 214 CallLog.Calls.TRANSCRIPTION 215 }; 216 217 static final int DATE_COLUMN_INDEX = 0; 218 static final int DURATION_COLUMN_INDEX = 1; 219 static final int NUMBER_COLUMN_INDEX = 2; 220 static final int CALL_TYPE_COLUMN_INDEX = 3; 221 static final int COUNTRY_ISO_COLUMN_INDEX = 4; 222 static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; 223 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; 224 static final int ACCOUNT_COMPONENT_NAME = 7; 225 static final int ACCOUNT_ID = 8; 226 static final int FEATURES = 9; 227 static final int DATA_USAGE = 10; 228 static final int TRANSCRIPTION_COLUMN_INDEX = 11; 229 230 @Override 231 protected void onCreate(Bundle icicle) { 232 super.onCreate(icicle); 233 234 setContentView(R.layout.call_detail); 235 236 mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); 237 mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); 238 mResources = getResources(); 239 240 mCallTypeHelper = new CallTypeHelper(getResources()); 241 mPhoneNumberHelper = new PhoneNumberDisplayHelper(mResources); 242 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 243 mAsyncQueryHandler = new CallDetailActivityQueryHandler(this); 244 245 mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); 246 247 mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); 248 mQuickContactBadge.setOverlay(null); 249 mCallerName = (TextView) findViewById(R.id.caller_name); 250 mCallerNumber = (TextView) findViewById(R.id.caller_number); 251 mAccountLabel = (TextView) findViewById(R.id.phone_account_label); 252 mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this); 253 mContactPhotoManager = ContactPhotoManager.getInstance(this); 254 mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener); 255 mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); 256 getActionBar().setDisplayHomeAsUpEnabled(true); 257 258 optionallyHandleVoicemail(); 259 if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { 260 closeSystemDialogs(); 261 } 262 } 263 264 @Override 265 public void onResume() { 266 super.onResume(); 267 updateData(getCallLogEntryUris()); 268 } 269 270 /** 271 * Handle voicemail playback or hide voicemail ui. 272 * <p> 273 * If the Intent used to start this Activity contains the suitable extras, then start voicemail 274 * playback. If it doesn't, then don't inflate the voicemail ui. 275 */ 276 private void optionallyHandleVoicemail() { 277 278 if (hasVoicemail()) { 279 LayoutInflater inflater = 280 (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 281 mVoicemailHeader = 282 (LinearLayout) inflater.inflate(R.layout.call_details_voicemail_header, null); 283 View voicemailContainer = mVoicemailHeader.findViewById(R.id.voicemail_container); 284 mStatusMessageView = mVoicemailHeader.findViewById(R.id.voicemail_status); 285 mStatusMessageText = 286 (TextView) mVoicemailHeader.findViewById(R.id.voicemail_status_message); 287 mStatusMessageAction = 288 (TextView) mVoicemailHeader.findViewById(R.id.voicemail_status_action); 289 mVoicemailTranscription = ( 290 TextView) mVoicemailHeader.findViewById(R.id.voicemail_transcription); 291 ListView historyList = (ListView) findViewById(R.id.history); 292 historyList.addHeaderView(mVoicemailHeader); 293 // Has voicemail: add the voicemail fragment. Add suitable arguments to set the uri 294 // to play and optionally start the playback. 295 // Do a query to fetch the voicemail status messages. 296 VoicemailPlaybackFragment playbackFragment; 297 298 playbackFragment = (VoicemailPlaybackFragment) getFragmentManager().findFragmentByTag( 299 VOICEMAIL_FRAGMENT_TAG); 300 301 if (playbackFragment == null) { 302 playbackFragment = new VoicemailPlaybackFragment(); 303 Bundle fragmentArguments = new Bundle(); 304 fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, mVoicemailUri); 305 if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) { 306 fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true); 307 } 308 playbackFragment.setArguments(fragmentArguments); 309 getFragmentManager().beginTransaction() 310 .add(R.id.voicemail_container, playbackFragment, VOICEMAIL_FRAGMENT_TAG) 311 .commitAllowingStateLoss(); 312 } 313 314 voicemailContainer.setVisibility(View.VISIBLE); 315 mAsyncQueryHandler.startVoicemailStatusQuery(mVoicemailUri); 316 markVoicemailAsRead(mVoicemailUri); 317 } 318 } 319 320 private boolean hasVoicemail() { 321 return mVoicemailUri != null; 322 } 323 324 private void markVoicemailAsRead(final Uri voicemailUri) { 325 mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() { 326 @Override 327 public Void doInBackground(Void... params) { 328 ContentValues values = new ContentValues(); 329 values.put(Voicemails.IS_READ, true); 330 getContentResolver().update(voicemailUri, values, 331 Voicemails.IS_READ + " = 0", null); 332 return null; 333 } 334 }); 335 } 336 337 /** 338 * Returns the list of URIs to show. 339 * <p> 340 * There are two ways the URIs can be provided to the activity: as the data on the intent, or as 341 * a list of ids in the call log added as an extra on the URI. 342 * <p> 343 * If both are available, the data on the intent takes precedence. 344 */ 345 private Uri[] getCallLogEntryUris() { 346 final Uri uri = getIntent().getData(); 347 if (uri != null) { 348 // If there is a data on the intent, it takes precedence over the extra. 349 return new Uri[]{ uri }; 350 } 351 final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); 352 final int numIds = ids == null ? 0 : ids.length; 353 final Uri[] uris = new Uri[numIds]; 354 for (int index = 0; index < numIds; ++index) { 355 uris[index] = ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, ids[index]); 356 } 357 return uris; 358 } 359 360 @Override 361 public boolean onKeyDown(int keyCode, KeyEvent event) { 362 switch (keyCode) { 363 case KeyEvent.KEYCODE_CALL: { 364 // Make sure phone isn't already busy before starting direct call 365 TelephonyManager tm = (TelephonyManager) 366 getSystemService(Context.TELEPHONY_SERVICE); 367 if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { 368 DialerUtils.startActivityWithErrorToast(this, 369 CallUtil.getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_TEL, mNumber, 370 null)), R.string.call_not_available); 371 return true; 372 } 373 } 374 } 375 376 return super.onKeyDown(keyCode, event); 377 } 378 379 /** 380 * Update user interface with details of given call. 381 * 382 * @param callUris URIs into {@link CallLog.Calls} of the calls to be displayed 383 */ 384 private void updateData(final Uri... callUris) { 385 class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> { 386 @Override 387 public PhoneCallDetails[] doInBackground(Void... params) { 388 // TODO: All phone calls correspond to the same person, so we can make a single 389 // lookup. 390 final int numCalls = callUris.length; 391 PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; 392 try { 393 for (int index = 0; index < numCalls; ++index) { 394 details[index] = getPhoneCallDetailsForUri(callUris[index]); 395 } 396 return details; 397 } catch (IllegalArgumentException e) { 398 // Something went wrong reading in our primary data. 399 Log.w(TAG, "invalid URI starting call details", e); 400 return null; 401 } 402 } 403 404 @Override 405 public void onPostExecute(PhoneCallDetails[] details) { 406 if (details == null) { 407 // Somewhere went wrong: we're going to bail out and show error to users. 408 Toast.makeText(CallDetailActivity.this, R.string.toast_call_detail_error, 409 Toast.LENGTH_SHORT).show(); 410 finish(); 411 return; 412 } 413 414 // We know that all calls are from the same number and the same contact, so pick the 415 // first. 416 PhoneCallDetails firstDetails = details[0]; 417 mNumber = firstDetails.number.toString(); 418 final int numberPresentation = firstDetails.numberPresentation; 419 final Uri contactUri = firstDetails.contactUri; 420 final Uri photoUri = firstDetails.photoUri; 421 422 // Cache the details about the phone number. 423 final boolean canPlaceCallsTo = 424 PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation); 425 final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper(); 426 final boolean isVoicemailNumber = phoneUtils.isVoicemailNumber(mNumber); 427 final boolean isSipNumber = phoneUtils.isSipNumber(mNumber); 428 429 final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails); 430 431 final CharSequence displayNumber = mPhoneNumberHelper.getDisplayNumber( 432 firstDetails.number, 433 firstDetails.numberPresentation, 434 firstDetails.formattedNumber); 435 final String displayNumberStr = mBidiFormatter.unicodeWrap( 436 displayNumber.toString(), TextDirectionHeuristics.LTR); 437 438 439 if (!TextUtils.isEmpty(firstDetails.name)) { 440 mCallerName.setText(firstDetails.name); 441 mCallerNumber.setText(callLocationOrType + " " + displayNumberStr); 442 } else { 443 mCallerName.setText(displayNumberStr); 444 if (!TextUtils.isEmpty(callLocationOrType)) { 445 mCallerNumber.setText(callLocationOrType); 446 mCallerNumber.setVisibility(View.VISIBLE); 447 } else { 448 mCallerNumber.setVisibility(View.GONE); 449 } 450 } 451 452 if (!TextUtils.isEmpty(firstDetails.accountLabel)) { 453 mAccountLabel.setText(firstDetails.accountLabel); 454 mAccountLabel.setVisibility(View.VISIBLE); 455 } else { 456 mAccountLabel.setVisibility(View.GONE); 457 } 458 459 mHasEditNumberBeforeCallOption = 460 canPlaceCallsTo && !isSipNumber && !isVoicemailNumber; 461 mHasTrashOption = hasVoicemail(); 462 mHasRemoveFromCallLogOption = !hasVoicemail(); 463 invalidateOptionsMenu(); 464 465 ListView historyList = (ListView) findViewById(R.id.history); 466 historyList.setAdapter( 467 new CallDetailHistoryAdapter(CallDetailActivity.this, mInflater, 468 mCallTypeHelper, details)); 469 470 String lookupKey = contactUri == null ? null 471 : ContactInfoHelper.getLookupKeyFromUri(contactUri); 472 473 final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType); 474 475 final int contactType = 476 isVoicemailNumber? ContactPhotoManager.TYPE_VOICEMAIL : 477 isBusiness ? ContactPhotoManager.TYPE_BUSINESS : 478 ContactPhotoManager.TYPE_DEFAULT; 479 480 String nameForDefaultImage; 481 if (TextUtils.isEmpty(firstDetails.name)) { 482 nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(firstDetails.number, 483 firstDetails.numberPresentation, 484 firstDetails.formattedNumber).toString(); 485 } else { 486 nameForDefaultImage = firstDetails.name.toString(); 487 } 488 489 if (hasVoicemail() && !TextUtils.isEmpty(firstDetails.transcription)) { 490 mVoicemailTranscription.setText(firstDetails.transcription); 491 mVoicemailTranscription.setVisibility(View.VISIBLE); 492 } 493 494 loadContactPhotos( 495 contactUri, photoUri, nameForDefaultImage, lookupKey, contactType); 496 findViewById(R.id.call_detail).setVisibility(View.VISIBLE); 497 } 498 499 /** 500 * Determines the location geocode text for a call, or the phone number type 501 * (if available). 502 * 503 * @param details The call details. 504 * @return The phone number type or location. 505 */ 506 private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) { 507 if (!TextUtils.isEmpty(details.name)) { 508 return Phone.getTypeLabel(mResources, details.numberType, 509 details.numberLabel); 510 } else { 511 return details.geocode; 512 } 513 } 514 } 515 mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask()); 516 } 517 518 /** Return the phone call details for a given call log URI. */ 519 private PhoneCallDetails getPhoneCallDetailsForUri(Uri callUri) { 520 ContentResolver resolver = getContentResolver(); 521 Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null); 522 try { 523 if (callCursor == null || !callCursor.moveToFirst()) { 524 throw new IllegalArgumentException("Cannot find content: " + callUri); 525 } 526 527 // Read call log specifics. 528 final String number = callCursor.getString(NUMBER_COLUMN_INDEX); 529 final int numberPresentation = callCursor.getInt( 530 NUMBER_PRESENTATION_COLUMN_INDEX); 531 final long date = callCursor.getLong(DATE_COLUMN_INDEX); 532 final long duration = callCursor.getLong(DURATION_COLUMN_INDEX); 533 final int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX); 534 String countryIso = callCursor.getString(COUNTRY_ISO_COLUMN_INDEX); 535 final String geocode = callCursor.getString(GEOCODED_LOCATION_COLUMN_INDEX); 536 final String transcription = callCursor.getString(TRANSCRIPTION_COLUMN_INDEX); 537 538 final String accountLabel = PhoneAccountUtils.getAccountLabel(this, 539 PhoneAccountUtils.getAccount( 540 callCursor.getString(ACCOUNT_COMPONENT_NAME), 541 callCursor.getString(ACCOUNT_ID))); 542 543 if (TextUtils.isEmpty(countryIso)) { 544 countryIso = mDefaultCountryIso; 545 } 546 547 // Formatted phone number. 548 final CharSequence formattedNumber; 549 // Read contact specifics. 550 final CharSequence nameText; 551 final int numberType; 552 final CharSequence numberLabel; 553 final Uri photoUri; 554 final Uri lookupUri; 555 int sourceType; 556 // If this is not a regular number, there is no point in looking it up in the contacts. 557 ContactInfo info = 558 PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) 559 && !new PhoneNumberUtilsWrapper().isVoicemailNumber(number) 560 ? mContactInfoHelper.lookupNumber(number, countryIso) 561 : null; 562 if (info == null) { 563 formattedNumber = mPhoneNumberHelper.getDisplayNumber(number, 564 numberPresentation, null); 565 nameText = ""; 566 numberType = 0; 567 numberLabel = ""; 568 photoUri = null; 569 lookupUri = null; 570 sourceType = 0; 571 } else { 572 formattedNumber = info.formattedNumber; 573 nameText = info.name; 574 numberType = info.type; 575 numberLabel = info.label; 576 photoUri = info.photoUri; 577 lookupUri = info.lookupUri; 578 sourceType = info.sourceType; 579 } 580 final int features = callCursor.getInt(FEATURES); 581 Long dataUsage = null; 582 if (!callCursor.isNull(DATA_USAGE)) { 583 dataUsage = callCursor.getLong(DATA_USAGE); 584 } 585 return new PhoneCallDetails(number, numberPresentation, 586 formattedNumber, countryIso, geocode, 587 new int[]{ callType }, date, duration, 588 nameText, numberType, numberLabel, lookupUri, photoUri, sourceType, 589 accountLabel, null, features, dataUsage, transcription); 590 } finally { 591 if (callCursor != null) { 592 callCursor.close(); 593 } 594 } 595 } 596 597 /** Load the contact photos and places them in the corresponding views. */ 598 private void loadContactPhotos(Uri contactUri, Uri photoUri, String displayName, 599 String lookupKey, int contactType) { 600 601 final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey, 602 contactType, true /* isCircular */); 603 604 mQuickContactBadge.assignContactUri(contactUri); 605 mQuickContactBadge.setContentDescription( 606 mResources.getString(R.string.description_contact_details, displayName)); 607 608 mContactPhotoManager.loadDirectoryPhoto(mQuickContactBadge, photoUri, 609 false /* darkTheme */, true /* isCircular */, request); 610 } 611 612 static final class ViewEntry { 613 public final String text; 614 public final Intent primaryIntent; 615 /** The description for accessibility of the primary action. */ 616 public final String primaryDescription; 617 618 public CharSequence label = null; 619 /** Icon for the secondary action. */ 620 public int secondaryIcon = 0; 621 /** Intent for the secondary action. If not null, an icon must be defined. */ 622 public Intent secondaryIntent = null; 623 /** The description for accessibility of the secondary action. */ 624 public String secondaryDescription = null; 625 626 public ViewEntry(String text, Intent intent, String description) { 627 this.text = text; 628 primaryIntent = intent; 629 primaryDescription = description; 630 } 631 632 public void setSecondaryAction(int icon, Intent intent, String description) { 633 secondaryIcon = icon; 634 secondaryIntent = intent; 635 secondaryDescription = description; 636 } 637 } 638 639 protected void updateVoicemailStatusMessage(Cursor statusCursor) { 640 if (statusCursor == null) { 641 mStatusMessageView.setVisibility(View.GONE); 642 return; 643 } 644 final StatusMessage message = getStatusMessage(statusCursor); 645 if (message == null || !message.showInCallDetails()) { 646 mStatusMessageView.setVisibility(View.GONE); 647 return; 648 } 649 650 mStatusMessageView.setVisibility(View.VISIBLE); 651 mStatusMessageText.setText(message.callDetailsMessageId); 652 if (message.actionMessageId != -1) { 653 mStatusMessageAction.setText(message.actionMessageId); 654 } 655 if (message.actionUri != null) { 656 mStatusMessageAction.setClickable(true); 657 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 658 @Override 659 public void onClick(View v) { 660 DialerUtils.startActivityWithErrorToast(CallDetailActivity.this, 661 new Intent(Intent.ACTION_VIEW, message.actionUri)); 662 } 663 }); 664 } else { 665 mStatusMessageAction.setClickable(false); 666 } 667 } 668 669 private StatusMessage getStatusMessage(Cursor statusCursor) { 670 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 671 if (messages.size() == 0) { 672 return null; 673 } 674 // There can only be a single status message per source package, so num of messages can 675 // at most be 1. 676 if (messages.size() > 1) { 677 Log.w(TAG, String.format("Expected 1, found (%d) num of status messages." + 678 " Will use the first one.", messages.size())); 679 } 680 return messages.get(0); 681 } 682 683 @Override 684 public boolean onCreateOptionsMenu(Menu menu) { 685 getMenuInflater().inflate(R.menu.call_details_options, menu); 686 return super.onCreateOptionsMenu(menu); 687 } 688 689 @Override 690 public boolean onPrepareOptionsMenu(Menu menu) { 691 // This action deletes all elements in the group from the call log. 692 // We don't have this action for voicemails, because you can just use the trash button. 693 menu.findItem(R.id.menu_remove_from_call_log).setVisible(mHasRemoveFromCallLogOption); 694 menu.findItem(R.id.menu_edit_number_before_call).setVisible(mHasEditNumberBeforeCallOption); 695 menu.findItem(R.id.menu_trash).setVisible(mHasTrashOption); 696 return super.onPrepareOptionsMenu(menu); 697 } 698 699 public void onMenuRemoveFromCallLog(MenuItem menuItem) { 700 final StringBuilder callIds = new StringBuilder(); 701 for (Uri callUri : getCallLogEntryUris()) { 702 if (callIds.length() != 0) { 703 callIds.append(","); 704 } 705 callIds.append(ContentUris.parseId(callUri)); 706 } 707 mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH, 708 new AsyncTask<Void, Void, Void>() { 709 @Override 710 public Void doInBackground(Void... params) { 711 getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL, 712 Calls._ID + " IN (" + callIds + ")", null); 713 return null; 714 } 715 716 @Override 717 public void onPostExecute(Void result) { 718 finish(); 719 } 720 } 721 ); 722 } 723 724 public void onMenuEditNumberBeforeCall(MenuItem menuItem) { 725 startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber))); 726 } 727 728 public void onMenuTrashVoicemail(MenuItem menuItem) { 729 mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH, 730 new AsyncTask<Void, Void, Void>() { 731 @Override 732 public Void doInBackground(Void... params) { 733 getContentResolver().delete(mVoicemailUri, null, null); 734 return null; 735 } 736 737 @Override 738 public void onPostExecute(Void result) { 739 finish(); 740 } 741 } 742 ); 743 } 744 745 @Override 746 protected void onPause() { 747 // Immediately stop the proximity sensor. 748 disableProximitySensor(false); 749 mProximitySensorListener.clearPendingRequests(); 750 super.onPause(); 751 } 752 753 @Override 754 public void enableProximitySensor() { 755 mProximitySensorManager.enable(); 756 } 757 758 @Override 759 public void disableProximitySensor(boolean waitForFarState) { 760 mProximitySensorManager.disable(waitForFarState); 761 } 762 763 private void closeSystemDialogs() { 764 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 765 } 766 767 /** Returns the given text, forced to be left-to-right. */ 768 private static CharSequence forceLeftToRight(CharSequence text) { 769 StringBuilder sb = new StringBuilder(); 770 sb.append(LEFT_TO_RIGHT_EMBEDDING); 771 sb.append(text); 772 sb.append(POP_DIRECTIONAL_FORMATTING); 773 return sb.toString(); 774 } 775} 776