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