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