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