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