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