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.app.LoaderManager.LoaderCallbacks; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.graphics.drawable.Drawable; 30 import android.net.Uri; 31 import android.os.AsyncTask; 32 import android.os.Bundle; 33 import android.provider.CallLog; 34 import android.provider.CallLog.Calls; 35 import android.provider.ContactsContract.CommonDataKinds.Phone; 36 import android.provider.ContactsContract.Contacts; 37 import android.provider.ContactsContract.DisplayNameSources; 38 import android.provider.ContactsContract.Intents.Insert; 39 import android.provider.VoicemailContract.Voicemails; 40 import android.telephony.PhoneNumberUtils; 41 import android.telephony.TelephonyManager; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.view.ActionMode; 45 import android.view.KeyEvent; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.widget.ImageButton; 51 import android.widget.ImageView; 52 import android.widget.ListView; 53 import android.widget.TextView; 54 import android.widget.Toast; 55 56 import com.android.contacts.common.ContactPhotoManager; 57 import com.android.contacts.common.CallUtil; 58 import com.android.contacts.common.ClipboardUtils; 59 import com.android.contacts.common.GeoUtil; 60 import com.android.contacts.common.model.Contact; 61 import com.android.contacts.common.model.ContactLoader; 62 import com.android.contacts.common.util.UriUtils; 63 import com.android.dialer.BackScrollManager.ScrollableHeader; 64 import com.android.dialer.calllog.CallDetailHistoryAdapter; 65 import com.android.dialer.calllog.CallTypeHelper; 66 import com.android.dialer.calllog.ContactInfo; 67 import com.android.dialer.calllog.ContactInfoHelper; 68 import com.android.dialer.calllog.PhoneNumberHelper; 69 import com.android.dialer.calllog.PhoneNumberUtilsWrapper; 70 import com.android.dialer.util.AsyncTaskExecutor; 71 import com.android.dialer.util.AsyncTaskExecutors; 72 import com.android.dialer.voicemail.VoicemailPlaybackFragment; 73 import com.android.dialer.voicemail.VoicemailStatusHelper; 74 import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 75 import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 76 77 import java.util.List; 78 79 /** 80 * Displays the details of a specific call log entry. 81 * <p> 82 * This activity can be either started with the URI of a single call log entry, or with the 83 * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries. 84 */ 85 public class CallDetailActivity extends Activity implements ProximitySensorAware { 86 private static final String TAG = "CallDetail"; 87 88 private static final int LOADER_ID = 0; 89 private static final String BUNDLE_CONTACT_URI_EXTRA = "contact_uri_extra"; 90 91 private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A'; 92 private static final char POP_DIRECTIONAL_FORMATTING = '\u202C'; 93 94 /** The time to wait before enabling the blank the screen due to the proximity sensor. */ 95 private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100; 96 /** The time to wait before disabling the blank the screen due to the proximity sensor. */ 97 private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500; 98 99 /** The enumeration of {@link AsyncTask} objects used in this class. */ 100 public enum Tasks { 101 MARK_VOICEMAIL_READ, 102 DELETE_VOICEMAIL_AND_FINISH, 103 REMOVE_FROM_CALL_LOG_AND_FINISH, 104 UPDATE_PHONE_CALL_DETAILS, 105 } 106 107 /** A long array extra containing ids of call log entries to display. */ 108 public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS"; 109 /** If we are started with a voicemail, we'll find the uri to play with this extra. */ 110 public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI"; 111 /** If we should immediately start playback of the voicemail, this extra will be set to true. */ 112 public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK"; 113 /** If the activity was triggered from a notification. */ 114 public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION"; 115 116 private CallTypeHelper mCallTypeHelper; 117 private PhoneNumberHelper mPhoneNumberHelper; 118 private PhoneCallDetailsHelper mPhoneCallDetailsHelper; 119 private TextView mHeaderTextView; 120 private View mHeaderOverlayView; 121 private ImageView mMainActionView; 122 private ImageButton mMainActionPushLayerView; 123 private ImageView mContactBackgroundView; 124 private AsyncTaskExecutor mAsyncTaskExecutor; 125 private ContactInfoHelper mContactInfoHelper; 126 127 private String mNumber = null; 128 private String mDefaultCountryIso; 129 130 /* package */ LayoutInflater mInflater; 131 /* package */ Resources mResources; 132 /** Helper to load contact photos. */ 133 private ContactPhotoManager mContactPhotoManager; 134 /** Helper to make async queries to content resolver. */ 135 private CallDetailActivityQueryHandler mAsyncQueryHandler; 136 /** Helper to get voicemail status messages. */ 137 private VoicemailStatusHelper mVoicemailStatusHelper; 138 // Views related to voicemail status message. 139 private View mStatusMessageView; 140 private TextView mStatusMessageText; 141 private TextView mStatusMessageAction; 142 143 /** Whether we should show "edit number before call" in the options menu. */ 144 private boolean mHasEditNumberBeforeCallOption; 145 /** Whether we should show "trash" in the options menu. */ 146 private boolean mHasTrashOption; 147 /** Whether we should show "remove from call log" in the options menu. */ 148 private boolean mHasRemoveFromCallLogOption; 149 150 private ProximitySensorManager mProximitySensorManager; 151 private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener(); 152 153 /** 154 * The action mode used when the phone number is selected. This will be non-null only when the 155 * phone number is selected. 156 */ 157 private ActionMode mPhoneNumberActionMode; 158 159 private CharSequence mPhoneNumberLabelToCopy; 160 private CharSequence mPhoneNumberToCopy; 161 162 /** Listener to changes in the proximity sensor state. */ 163 private class ProximitySensorListener implements ProximitySensorManager.Listener { 164 /** Used to show a blank view and hide the action bar. */ 165 private final Runnable mBlankRunnable = new Runnable() { 166 @Override 167 public void run() { 168 View blankView = findViewById(R.id.blank); 169 blankView.setVisibility(View.VISIBLE); 170 getActionBar().hide(); 171 } 172 }; 173 /** Used to remove the blank view and show the action bar. */ 174 private final Runnable mUnblankRunnable = new Runnable() { 175 @Override 176 public void run() { 177 View blankView = findViewById(R.id.blank); 178 blankView.setVisibility(View.GONE); 179 getActionBar().show(); 180 } 181 }; 182 183 @Override 184 public synchronized void onNear() { 185 clearPendingRequests(); 186 postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS); 187 } 188 189 @Override 190 public synchronized void onFar() { 191 clearPendingRequests(); 192 postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS); 193 } 194 195 /** Removed any delayed requests that may be pending. */ 196 public synchronized void clearPendingRequests() { 197 View blankView = findViewById(R.id.blank); 198 blankView.removeCallbacks(mBlankRunnable); 199 blankView.removeCallbacks(mUnblankRunnable); 200 } 201 202 /** Post a {@link Runnable} with a delay on the main thread. */ 203 private synchronized void postDelayed(Runnable runnable, long delayMillis) { 204 // Post these instead of executing immediately so that: 205 // - They are guaranteed to be executed on the main thread. 206 // - If the sensor values changes rapidly for some time, the UI will not be 207 // updated immediately. 208 View blankView = findViewById(R.id.blank); 209 blankView.postDelayed(runnable, delayMillis); 210 } 211 } 212 213 static final String[] CALL_LOG_PROJECTION = new String[] { 214 CallLog.Calls.DATE, 215 CallLog.Calls.DURATION, 216 CallLog.Calls.NUMBER, 217 CallLog.Calls.TYPE, 218 CallLog.Calls.COUNTRY_ISO, 219 CallLog.Calls.GEOCODED_LOCATION, 220 CallLog.Calls.NUMBER_PRESENTATION, 221 }; 222 223 static final int DATE_COLUMN_INDEX = 0; 224 static final int DURATION_COLUMN_INDEX = 1; 225 static final int NUMBER_COLUMN_INDEX = 2; 226 static final int CALL_TYPE_COLUMN_INDEX = 3; 227 static final int COUNTRY_ISO_COLUMN_INDEX = 4; 228 static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; 229 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; 230 231 private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() { 232 @Override 233 public void onClick(View view) { 234 if (finishPhoneNumerSelectedActionModeIfShown()) { 235 return; 236 } 237 startActivity(((ViewEntry) view.getTag()).primaryIntent); 238 } 239 }; 240 241 private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() { 242 @Override 243 public void onClick(View view) { 244 if (finishPhoneNumerSelectedActionModeIfShown()) { 245 return; 246 } 247 startActivity(((ViewEntry) view.getTag()).secondaryIntent); 248 } 249 }; 250 251 private final View.OnLongClickListener mPrimaryLongClickListener = 252 new View.OnLongClickListener() { 253 @Override 254 public boolean onLongClick(View v) { 255 if (finishPhoneNumerSelectedActionModeIfShown()) { 256 return true; 257 } 258 startPhoneNumberSelectedActionMode(v); 259 return true; 260 } 261 }; 262 263 private final LoaderCallbacks<Contact> mLoaderCallbacks = new LoaderCallbacks<Contact>() { 264 @Override 265 public void onLoaderReset(Loader<Contact> loader) { 266 } 267 268 @Override 269 public void onLoadFinished(Loader<Contact> loader, Contact data) { 270 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 271 intent.setType(Contacts.CONTENT_ITEM_TYPE); 272 if (data.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) { 273 intent.putExtra(Insert.NAME, data.getDisplayName()); 274 } 275 intent.putExtra(Insert.DATA, data.getContentValues()); 276 bindContactPhotoAction(intent, R.drawable.ic_add_contact_holo_dark, 277 getString(R.string.description_add_contact)); 278 } 279 280 @Override 281 public Loader<Contact> onCreateLoader(int id, Bundle args) { 282 final Uri contactUri = args.getParcelable(BUNDLE_CONTACT_URI_EXTRA); 283 if (contactUri == null) { 284 Log.wtf(TAG, "No contact lookup uri provided."); 285 } 286 return new ContactLoader(CallDetailActivity.this, contactUri, 287 false /* loadGroupMetaData */, false /* loadInvitableAccountTypes */, 288 false /* postViewNotification */, true /* computeFormattedPhoneNumber */); 289 } 290 }; 291 292 @Override 293 protected void onCreate(Bundle icicle) { 294 super.onCreate(icicle); 295 296 setContentView(R.layout.call_detail); 297 298 mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); 299 mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); 300 mResources = getResources(); 301 302 mCallTypeHelper = new CallTypeHelper(getResources()); 303 mPhoneNumberHelper = new PhoneNumberHelper(mResources); 304 mPhoneCallDetailsHelper = new PhoneCallDetailsHelper(mResources, mCallTypeHelper, 305 new PhoneNumberUtilsWrapper()); 306 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 307 mAsyncQueryHandler = new CallDetailActivityQueryHandler(this); 308 mHeaderTextView = (TextView) findViewById(R.id.header_text); 309 mHeaderOverlayView = findViewById(R.id.photo_text_bar); 310 mStatusMessageView = findViewById(R.id.voicemail_status); 311 mStatusMessageText = (TextView) findViewById(R.id.voicemail_status_message); 312 mStatusMessageAction = (TextView) findViewById(R.id.voicemail_status_action); 313 mMainActionView = (ImageView) findViewById(R.id.main_action); 314 mMainActionPushLayerView = (ImageButton) findViewById(R.id.main_action_push_layer); 315 mContactBackgroundView = (ImageView) findViewById(R.id.contact_background); 316 mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this); 317 mContactPhotoManager = ContactPhotoManager.getInstance(this); 318 mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener); 319 mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); 320 getActionBar().setDisplayHomeAsUpEnabled(true); 321 optionallyHandleVoicemail(); 322 if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { 323 closeSystemDialogs(); 324 } 325 } 326 327 @Override 328 public void onResume() { 329 super.onResume(); 330 updateData(getCallLogEntryUris()); 331 } 332 333 /** 334 * Handle voicemail playback or hide voicemail ui. 335 * <p> 336 * If the Intent used to start this Activity contains the suitable extras, then start voicemail 337 * playback. If it doesn't, then hide the voicemail ui. 338 */ 339 private void optionallyHandleVoicemail() { 340 View voicemailContainer = findViewById(R.id.voicemail_container); 341 if (hasVoicemail()) { 342 // Has voicemail: add the voicemail fragment. Add suitable arguments to set the uri 343 // to play and optionally start the playback. 344 // Do a query to fetch the voicemail status messages. 345 VoicemailPlaybackFragment playbackFragment = new VoicemailPlaybackFragment(); 346 Bundle fragmentArguments = new Bundle(); 347 fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, getVoicemailUri()); 348 if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) { 349 fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true); 350 } 351 playbackFragment.setArguments(fragmentArguments); 352 voicemailContainer.setVisibility(View.VISIBLE); 353 getFragmentManager().beginTransaction() 354 .add(R.id.voicemail_container, playbackFragment) 355 .commitAllowingStateLoss(); 356 mAsyncQueryHandler.startVoicemailStatusQuery(getVoicemailUri()); 357 markVoicemailAsRead(getVoicemailUri()); 358 } else { 359 // No voicemail uri: hide the status view. 360 mStatusMessageView.setVisibility(View.GONE); 361 voicemailContainer.setVisibility(View.GONE); 362 } 363 } 364 365 private boolean hasVoicemail() { 366 return getVoicemailUri() != null; 367 } 368 369 private Uri getVoicemailUri() { 370 return getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); 371 } 372 373 private void markVoicemailAsRead(final Uri voicemailUri) { 374 mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() { 375 @Override 376 public Void doInBackground(Void... params) { 377 ContentValues values = new ContentValues(); 378 values.put(Voicemails.IS_READ, true); 379 getContentResolver().update(voicemailUri, values, 380 Voicemails.IS_READ + " = 0", null); 381 return null; 382 } 383 }); 384 } 385 386 /** 387 * Returns the list of URIs to show. 388 * <p> 389 * There are two ways the URIs can be provided to the activity: as the data on the intent, or as 390 * a list of ids in the call log added as an extra on the URI. 391 * <p> 392 * If both are available, the data on the intent takes precedence. 393 */ 394 private Uri[] getCallLogEntryUris() { 395 Uri uri = getIntent().getData(); 396 if (uri != null) { 397 // If there is a data on the intent, it takes precedence over the extra. 398 return new Uri[]{ uri }; 399 } 400 long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); 401 Uri[] uris = new Uri[ids.length]; 402 for (int index = 0; index < ids.length; ++index) { 403 uris[index] = ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, ids[index]); 404 } 405 return uris; 406 } 407 408 @Override 409 public boolean onKeyDown(int keyCode, KeyEvent event) { 410 switch (keyCode) { 411 case KeyEvent.KEYCODE_CALL: { 412 // Make sure phone isn't already busy before starting direct call 413 TelephonyManager tm = (TelephonyManager) 414 getSystemService(Context.TELEPHONY_SERVICE); 415 if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { 416 startActivity(CallUtil.getCallIntent( 417 Uri.fromParts(CallUtil.SCHEME_TEL, mNumber, null))); 418 return true; 419 } 420 } 421 } 422 423 return super.onKeyDown(keyCode, event); 424 } 425 426 /** 427 * Update user interface with details of given call. 428 * 429 * @param callUris URIs into {@link CallLog.Calls} of the calls to be displayed 430 */ 431 private void updateData(final Uri... callUris) { 432 class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> { 433 @Override 434 public PhoneCallDetails[] doInBackground(Void... params) { 435 // TODO: All phone calls correspond to the same person, so we can make a single 436 // lookup. 437 final int numCalls = callUris.length; 438 PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; 439 try { 440 for (int index = 0; index < numCalls; ++index) { 441 details[index] = getPhoneCallDetailsForUri(callUris[index]); 442 } 443 return details; 444 } catch (IllegalArgumentException e) { 445 // Something went wrong reading in our primary data. 446 Log.w(TAG, "invalid URI starting call details", e); 447 return null; 448 } 449 } 450 451 @Override 452 public void onPostExecute(PhoneCallDetails[] details) { 453 if (details == null) { 454 // Somewhere went wrong: we're going to bail out and show error to users. 455 Toast.makeText(CallDetailActivity.this, R.string.toast_call_detail_error, 456 Toast.LENGTH_SHORT).show(); 457 finish(); 458 return; 459 } 460 461 // We know that all calls are from the same number and the same contact, so pick the 462 // first. 463 PhoneCallDetails firstDetails = details[0]; 464 mNumber = firstDetails.number.toString(); 465 final int numberPresentation = firstDetails.numberPresentation; 466 final Uri contactUri = firstDetails.contactUri; 467 final Uri photoUri = firstDetails.photoUri; 468 469 // Set the details header, based on the first phone call. 470 mPhoneCallDetailsHelper.setCallDetailsHeader(mHeaderTextView, firstDetails); 471 472 // Cache the details about the phone number. 473 final boolean canPlaceCallsTo = 474 PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation); 475 final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper(); 476 final boolean isVoicemailNumber = phoneUtils.isVoicemailNumber(mNumber); 477 final boolean isSipNumber = phoneUtils.isSipNumber(mNumber); 478 479 // Let user view contact details if they exist, otherwise add option to create new 480 // contact from this number. 481 final Intent mainActionIntent; 482 final int mainActionIcon; 483 final String mainActionDescription; 484 485 final CharSequence nameOrNumber; 486 if (!TextUtils.isEmpty(firstDetails.name)) { 487 nameOrNumber = firstDetails.name; 488 } else { 489 nameOrNumber = firstDetails.number; 490 } 491 492 boolean skipBind = false; 493 494 if (contactUri != null && !UriUtils.isEncodedContactUri(contactUri)) { 495 mainActionIntent = new Intent(Intent.ACTION_VIEW, contactUri); 496 // This will launch People's detail contact screen, so we probably want to 497 // treat it as a separate People task. 498 mainActionIntent.setFlags( 499 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 500 mainActionIcon = R.drawable.ic_contacts_holo_dark; 501 mainActionDescription = 502 getString(R.string.description_view_contact, nameOrNumber); 503 } else if (UriUtils.isEncodedContactUri(contactUri)) { 504 final Bundle bundle = new Bundle(1); 505 bundle.putParcelable(BUNDLE_CONTACT_URI_EXTRA, contactUri); 506 getLoaderManager().initLoader(LOADER_ID, bundle, mLoaderCallbacks); 507 mainActionIntent = null; 508 mainActionIcon = R.drawable.ic_add_contact_holo_dark; 509 mainActionDescription = getString(R.string.description_add_contact); 510 skipBind = true; 511 } else if (isVoicemailNumber) { 512 mainActionIntent = null; 513 mainActionIcon = 0; 514 mainActionDescription = null; 515 } else if (isSipNumber) { 516 // TODO: This item is currently disabled for SIP addresses, because 517 // the Insert.PHONE extra only works correctly for PSTN numbers. 518 // 519 // To fix this for SIP addresses, we need to: 520 // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if 521 // the current number is a SIP address 522 // - update the contacts UI code to handle Insert.SIP_ADDRESS by 523 // updating the SipAddress field 524 // and then we can remove the "!isSipNumber" check above. 525 mainActionIntent = null; 526 mainActionIcon = 0; 527 mainActionDescription = null; 528 } else if (canPlaceCallsTo) { 529 mainActionIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 530 mainActionIntent.setType(Contacts.CONTENT_ITEM_TYPE); 531 mainActionIntent.putExtra(Insert.PHONE, mNumber); 532 mainActionIcon = R.drawable.ic_add_contact_holo_dark; 533 mainActionDescription = getString(R.string.description_add_contact); 534 } else { 535 // If we cannot call the number, when we probably cannot add it as a contact 536 // either. This is usually the case of private, unknown, or payphone numbers. 537 mainActionIntent = null; 538 mainActionIcon = 0; 539 mainActionDescription = null; 540 } 541 542 if (!skipBind) { 543 bindContactPhotoAction(mainActionIntent, mainActionIcon, 544 mainActionDescription); 545 } 546 547 // This action allows to call the number that places the call. 548 if (canPlaceCallsTo) { 549 final CharSequence displayNumber = 550 mPhoneNumberHelper.getDisplayNumber( 551 firstDetails.number, 552 firstDetails.numberPresentation, 553 firstDetails.formattedNumber); 554 555 ViewEntry entry = new ViewEntry( 556 getString(R.string.menu_callNumber, 557 forceLeftToRight(displayNumber)), 558 CallUtil.getCallIntent(mNumber), 559 getString(R.string.description_call, nameOrNumber)); 560 561 // Only show a label if the number is shown and it is not a SIP address. 562 if (!TextUtils.isEmpty(firstDetails.name) 563 && !TextUtils.isEmpty(firstDetails.number) 564 && !PhoneNumberUtils.isUriNumber(firstDetails.number.toString())) { 565 entry.label = Phone.getTypeLabel(mResources, firstDetails.numberType, 566 firstDetails.numberLabel); 567 } 568 569 // The secondary action allows to send an SMS to the number that placed the 570 // call. 571 if (phoneUtils.canSendSmsTo(mNumber, numberPresentation)) { 572 entry.setSecondaryAction( 573 R.drawable.ic_text_holo_light, 574 new Intent(Intent.ACTION_SENDTO, 575 Uri.fromParts("sms", mNumber, null)), 576 getString(R.string.description_send_text_message, nameOrNumber)); 577 } 578 579 configureCallButton(entry); 580 mPhoneNumberToCopy = displayNumber; 581 mPhoneNumberLabelToCopy = entry.label; 582 } else { 583 disableCallButton(); 584 mPhoneNumberToCopy = null; 585 mPhoneNumberLabelToCopy = null; 586 } 587 588 mHasEditNumberBeforeCallOption = 589 canPlaceCallsTo && !isSipNumber && !isVoicemailNumber; 590 mHasTrashOption = hasVoicemail(); 591 mHasRemoveFromCallLogOption = !hasVoicemail(); 592 invalidateOptionsMenu(); 593 594 ListView historyList = (ListView) findViewById(R.id.history); 595 historyList.setAdapter( 596 new CallDetailHistoryAdapter(CallDetailActivity.this, mInflater, 597 mCallTypeHelper, details, hasVoicemail(), canPlaceCallsTo, 598 findViewById(R.id.controls))); 599 BackScrollManager.bind( 600 new ScrollableHeader() { 601 private View mControls = findViewById(R.id.controls); 602 private View mPhoto = findViewById(R.id.contact_background_sizer); 603 private View mHeader = findViewById(R.id.photo_text_bar); 604 private View mSeparator = findViewById(R.id.separator); 605 606 @Override 607 public void setOffset(int offset) { 608 mControls.setY(-offset); 609 } 610 611 @Override 612 public int getMaximumScrollableHeaderOffset() { 613 // We can scroll the photo out, but we should keep the header if 614 // present. 615 if (mHeader.getVisibility() == View.VISIBLE) { 616 return mPhoto.getHeight() - mHeader.getHeight(); 617 } else { 618 // If the header is not present, we should also scroll out the 619 // separator line. 620 return mPhoto.getHeight() + mSeparator.getHeight(); 621 } 622 } 623 }, 624 historyList); 625 loadContactPhotos(photoUri); 626 findViewById(R.id.call_detail).setVisibility(View.VISIBLE); 627 } 628 } 629 mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask()); 630 } 631 632 private void bindContactPhotoAction(final Intent actionIntent, int actionIcon, 633 String actionDescription) { 634 if (actionIntent == null) { 635 mMainActionView.setVisibility(View.INVISIBLE); 636 mMainActionPushLayerView.setVisibility(View.GONE); 637 mHeaderTextView.setVisibility(View.INVISIBLE); 638 mHeaderOverlayView.setVisibility(View.INVISIBLE); 639 } else { 640 mMainActionView.setVisibility(View.VISIBLE); 641 mMainActionView.setImageResource(actionIcon); 642 mMainActionPushLayerView.setVisibility(View.VISIBLE); 643 mMainActionPushLayerView.setOnClickListener(new View.OnClickListener() { 644 @Override 645 public void onClick(View v) { 646 startActivity(actionIntent); 647 } 648 }); 649 mMainActionPushLayerView.setContentDescription(actionDescription); 650 mHeaderTextView.setVisibility(View.VISIBLE); 651 mHeaderOverlayView.setVisibility(View.VISIBLE); 652 } 653 } 654 655 /** Return the phone call details for a given call log URI. */ 656 private PhoneCallDetails getPhoneCallDetailsForUri(Uri callUri) { 657 ContentResolver resolver = getContentResolver(); 658 Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null); 659 try { 660 if (callCursor == null || !callCursor.moveToFirst()) { 661 throw new IllegalArgumentException("Cannot find content: " + callUri); 662 } 663 664 // Read call log specifics. 665 final String number = callCursor.getString(NUMBER_COLUMN_INDEX); 666 final int numberPresentation = callCursor.getInt( 667 NUMBER_PRESENTATION_COLUMN_INDEX); 668 final long date = callCursor.getLong(DATE_COLUMN_INDEX); 669 final long duration = callCursor.getLong(DURATION_COLUMN_INDEX); 670 final int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX); 671 String countryIso = callCursor.getString(COUNTRY_ISO_COLUMN_INDEX); 672 final String geocode = callCursor.getString(GEOCODED_LOCATION_COLUMN_INDEX); 673 674 if (TextUtils.isEmpty(countryIso)) { 675 countryIso = mDefaultCountryIso; 676 } 677 678 // Formatted phone number. 679 final CharSequence formattedNumber; 680 // Read contact specifics. 681 final CharSequence nameText; 682 final int numberType; 683 final CharSequence numberLabel; 684 final Uri photoUri; 685 final Uri lookupUri; 686 // If this is not a regular number, there is no point in looking it up in the contacts. 687 ContactInfo info = 688 PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) 689 && !new PhoneNumberUtilsWrapper().isVoicemailNumber(number) 690 ? mContactInfoHelper.lookupNumber(number, countryIso) 691 : null; 692 if (info == null) { 693 formattedNumber = mPhoneNumberHelper.getDisplayNumber(number, 694 numberPresentation, null); 695 nameText = ""; 696 numberType = 0; 697 numberLabel = ""; 698 photoUri = null; 699 lookupUri = null; 700 } else { 701 formattedNumber = info.formattedNumber; 702 nameText = info.name; 703 numberType = info.type; 704 numberLabel = info.label; 705 photoUri = info.photoUri; 706 lookupUri = info.lookupUri; 707 } 708 return new PhoneCallDetails(number, numberPresentation, 709 formattedNumber, countryIso, geocode, 710 new int[]{ callType }, date, duration, 711 nameText, numberType, numberLabel, lookupUri, photoUri); 712 } finally { 713 if (callCursor != null) { 714 callCursor.close(); 715 } 716 } 717 } 718 719 /** Load the contact photos and places them in the corresponding views. */ 720 private void loadContactPhotos(Uri photoUri) { 721 mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri, 722 mContactBackgroundView.getWidth(), true); 723 } 724 725 static final class ViewEntry { 726 public final String text; 727 public final Intent primaryIntent; 728 /** The description for accessibility of the primary action. */ 729 public final String primaryDescription; 730 731 public CharSequence label = null; 732 /** Icon for the secondary action. */ 733 public int secondaryIcon = 0; 734 /** Intent for the secondary action. If not null, an icon must be defined. */ 735 public Intent secondaryIntent = null; 736 /** The description for accessibility of the secondary action. */ 737 public String secondaryDescription = null; 738 739 public ViewEntry(String text, Intent intent, String description) { 740 this.text = text; 741 primaryIntent = intent; 742 primaryDescription = description; 743 } 744 745 public void setSecondaryAction(int icon, Intent intent, String description) { 746 secondaryIcon = icon; 747 secondaryIntent = intent; 748 secondaryDescription = description; 749 } 750 } 751 752 /** Disables the call button area, e.g., for private numbers. */ 753 private void disableCallButton() { 754 findViewById(R.id.call_and_sms).setVisibility(View.GONE); 755 } 756 757 /** Configures the call button area using the given entry. */ 758 private void configureCallButton(ViewEntry entry) { 759 View convertView = findViewById(R.id.call_and_sms); 760 convertView.setVisibility(View.VISIBLE); 761 762 ImageView icon = (ImageView) convertView.findViewById(R.id.call_and_sms_icon); 763 View divider = convertView.findViewById(R.id.call_and_sms_divider); 764 TextView text = (TextView) convertView.findViewById(R.id.call_and_sms_text); 765 766 View mainAction = convertView.findViewById(R.id.call_and_sms_main_action); 767 mainAction.setOnClickListener(mPrimaryActionListener); 768 mainAction.setTag(entry); 769 mainAction.setContentDescription(entry.primaryDescription); 770 mainAction.setOnLongClickListener(mPrimaryLongClickListener); 771 772 if (entry.secondaryIntent != null) { 773 icon.setOnClickListener(mSecondaryActionListener); 774 icon.setImageResource(entry.secondaryIcon); 775 icon.setVisibility(View.VISIBLE); 776 icon.setTag(entry); 777 icon.setContentDescription(entry.secondaryDescription); 778 divider.setVisibility(View.VISIBLE); 779 } else { 780 icon.setVisibility(View.GONE); 781 divider.setVisibility(View.GONE); 782 } 783 text.setText(entry.text); 784 785 TextView label = (TextView) convertView.findViewById(R.id.call_and_sms_label); 786 if (TextUtils.isEmpty(entry.label)) { 787 label.setVisibility(View.GONE); 788 } else { 789 label.setText(entry.label); 790 label.setVisibility(View.VISIBLE); 791 } 792 } 793 794 protected void updateVoicemailStatusMessage(Cursor statusCursor) { 795 if (statusCursor == null) { 796 mStatusMessageView.setVisibility(View.GONE); 797 return; 798 } 799 final StatusMessage message = getStatusMessage(statusCursor); 800 if (message == null || !message.showInCallDetails()) { 801 mStatusMessageView.setVisibility(View.GONE); 802 return; 803 } 804 805 mStatusMessageView.setVisibility(View.VISIBLE); 806 mStatusMessageText.setText(message.callDetailsMessageId); 807 if (message.actionMessageId != -1) { 808 mStatusMessageAction.setText(message.actionMessageId); 809 } 810 if (message.actionUri != null) { 811 mStatusMessageAction.setClickable(true); 812 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 813 @Override 814 public void onClick(View v) { 815 startActivity(new Intent(Intent.ACTION_VIEW, message.actionUri)); 816 } 817 }); 818 } else { 819 mStatusMessageAction.setClickable(false); 820 } 821 } 822 823 private StatusMessage getStatusMessage(Cursor statusCursor) { 824 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 825 if (messages.size() == 0) { 826 return null; 827 } 828 // There can only be a single status message per source package, so num of messages can 829 // at most be 1. 830 if (messages.size() > 1) { 831 Log.w(TAG, String.format("Expected 1, found (%d) num of status messages." + 832 " Will use the first one.", messages.size())); 833 } 834 return messages.get(0); 835 } 836 837 @Override 838 public boolean onCreateOptionsMenu(Menu menu) { 839 getMenuInflater().inflate(R.menu.call_details_options, menu); 840 return super.onCreateOptionsMenu(menu); 841 } 842 843 @Override 844 public boolean onPrepareOptionsMenu(Menu menu) { 845 // This action deletes all elements in the group from the call log. 846 // We don't have this action for voicemails, because you can just use the trash button. 847 menu.findItem(R.id.menu_remove_from_call_log).setVisible(mHasRemoveFromCallLogOption); 848 menu.findItem(R.id.menu_edit_number_before_call).setVisible(mHasEditNumberBeforeCallOption); 849 menu.findItem(R.id.menu_trash).setVisible(mHasTrashOption); 850 return super.onPrepareOptionsMenu(menu); 851 } 852 853 public void onMenuRemoveFromCallLog(MenuItem menuItem) { 854 final StringBuilder callIds = new StringBuilder(); 855 for (Uri callUri : getCallLogEntryUris()) { 856 if (callIds.length() != 0) { 857 callIds.append(","); 858 } 859 callIds.append(ContentUris.parseId(callUri)); 860 } 861 mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH, 862 new AsyncTask<Void, Void, Void>() { 863 @Override 864 public Void doInBackground(Void... params) { 865 getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL, 866 Calls._ID + " IN (" + callIds + ")", null); 867 return null; 868 } 869 870 @Override 871 public void onPostExecute(Void result) { 872 finish(); 873 } 874 }); 875 } 876 877 public void onMenuEditNumberBeforeCall(MenuItem menuItem) { 878 startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber))); 879 } 880 881 public void onMenuTrashVoicemail(MenuItem menuItem) { 882 final Uri voicemailUri = getVoicemailUri(); 883 mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH, 884 new AsyncTask<Void, Void, Void>() { 885 @Override 886 public Void doInBackground(Void... params) { 887 getContentResolver().delete(voicemailUri, null, null); 888 return null; 889 } 890 @Override 891 public void onPostExecute(Void result) { 892 finish(); 893 } 894 }); 895 } 896 897 /** Invoked when the user presses the home button in the action bar. */ 898 private void onHomeSelected() { 899 Intent intent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI); 900 // This will open the call log even if the detail view has been opened directly. 901 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 902 startActivity(intent); 903 finish(); 904 } 905 906 @Override 907 protected void onPause() { 908 // Immediately stop the proximity sensor. 909 disableProximitySensor(false); 910 mProximitySensorListener.clearPendingRequests(); 911 super.onPause(); 912 } 913 914 @Override 915 public void enableProximitySensor() { 916 mProximitySensorManager.enable(); 917 } 918 919 @Override 920 public void disableProximitySensor(boolean waitForFarState) { 921 mProximitySensorManager.disable(waitForFarState); 922 } 923 924 /** 925 * If the phone number is selected, unselect it and return {@code true}. 926 * Otherwise, just {@code false}. 927 */ 928 private boolean finishPhoneNumerSelectedActionModeIfShown() { 929 if (mPhoneNumberActionMode == null) return false; 930 mPhoneNumberActionMode.finish(); 931 return true; 932 } 933 934 private void startPhoneNumberSelectedActionMode(View targetView) { 935 mPhoneNumberActionMode = startActionMode(new PhoneNumberActionModeCallback(targetView)); 936 } 937 938 private void closeSystemDialogs() { 939 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 940 } 941 942 private class PhoneNumberActionModeCallback implements ActionMode.Callback { 943 private final View mTargetView; 944 private final Drawable mOriginalViewBackground; 945 946 public PhoneNumberActionModeCallback(View targetView) { 947 mTargetView = targetView; 948 949 // Highlight the phone number view. Remember the old background, and put a new one. 950 mOriginalViewBackground = mTargetView.getBackground(); 951 mTargetView.setBackgroundColor(getResources().getColor(R.color.item_selected)); 952 } 953 954 @Override 955 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 956 if (TextUtils.isEmpty(mPhoneNumberToCopy)) return false; 957 958 getMenuInflater().inflate(R.menu.call_details_cab, menu); 959 return true; 960 } 961 962 @Override 963 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 964 return true; 965 } 966 967 @Override 968 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 969 switch (item.getItemId()) { 970 case R.id.copy_phone_number: 971 ClipboardUtils.copyText(CallDetailActivity.this, mPhoneNumberLabelToCopy, 972 mPhoneNumberToCopy, true); 973 mode.finish(); // Close the CAB 974 return true; 975 } 976 return false; 977 } 978 979 @Override 980 public void onDestroyActionMode(ActionMode mode) { 981 mPhoneNumberActionMode = null; 982 983 // Restore the view background. 984 mTargetView.setBackground(mOriginalViewBackground); 985 } 986 } 987 988 /** Returns the given text, forced to be left-to-right. */ 989 private static CharSequence forceLeftToRight(CharSequence text) { 990 StringBuilder sb = new StringBuilder(); 991 sb.append(LEFT_TO_RIGHT_EMBEDDING); 992 sb.append(text); 993 sb.append(POP_DIRECTIONAL_FORMATTING); 994 return sb.toString(); 995 } 996 } 997