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