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.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.res.Resources;
     26 import android.database.Cursor;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.provider.CallLog;
     31 import android.provider.CallLog.Calls;
     32 import android.provider.ContactsContract.CommonDataKinds.Phone;
     33 import android.provider.VoicemailContract.Voicemails;
     34 import android.telecom.PhoneAccount;
     35 import android.telecom.PhoneAccountHandle;
     36 import android.telephony.TelephonyManager;
     37 import android.text.BidiFormatter;
     38 import android.text.TextDirectionHeuristics;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 import android.view.KeyEvent;
     42 import android.view.LayoutInflater;
     43 import android.view.Menu;
     44 import android.view.MenuItem;
     45 import android.view.View;
     46 import android.widget.LinearLayout;
     47 import android.widget.ListView;
     48 import android.widget.QuickContactBadge;
     49 import android.widget.TextView;
     50 import android.widget.Toast;
     51 
     52 import com.android.contacts.common.ContactPhotoManager;
     53 import com.android.contacts.common.CallUtil;
     54 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
     55 import com.android.contacts.common.GeoUtil;
     56 import com.android.dialer.calllog.CallDetailHistoryAdapter;
     57 import com.android.dialer.calllog.CallTypeHelper;
     58 import com.android.dialer.calllog.ContactInfo;
     59 import com.android.dialer.calllog.ContactInfoHelper;
     60 import com.android.dialer.calllog.PhoneAccountUtils;
     61 import com.android.dialer.calllog.PhoneNumberDisplayHelper;
     62 import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
     63 import com.android.dialer.util.AsyncTaskExecutor;
     64 import com.android.dialer.util.AsyncTaskExecutors;
     65 import com.android.dialer.util.DialerUtils;
     66 import com.android.dialer.voicemail.VoicemailPlaybackFragment;
     67 import com.android.dialer.voicemail.VoicemailStatusHelper;
     68 import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
     69 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
     70 
     71 import java.util.List;
     72 
     73 /**
     74  * Displays the details of a specific call log entry.
     75  * <p>
     76  * This activity can be either started with the URI of a single call log entry, or with the
     77  * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
     78  */
     79 public class CallDetailActivity extends Activity implements ProximitySensorAware {
     80     private static final String TAG = "CallDetail";
     81 
     82     private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A';
     83     private static final char POP_DIRECTIONAL_FORMATTING = '\u202C';
     84 
     85     /** The time to wait before enabling the blank the screen due to the proximity sensor. */
     86     private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100;
     87     /** The time to wait before disabling the blank the screen due to the proximity sensor. */
     88     private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500;
     89 
     90     /** The enumeration of {@link AsyncTask} objects used in this class. */
     91     public enum Tasks {
     92         MARK_VOICEMAIL_READ,
     93         DELETE_VOICEMAIL_AND_FINISH,
     94         REMOVE_FROM_CALL_LOG_AND_FINISH,
     95         UPDATE_PHONE_CALL_DETAILS,
     96     }
     97 
     98     /** A long array extra containing ids of call log entries to display. */
     99     public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
    100     /** If we are started with a voicemail, we'll find the uri to play with this extra. */
    101     public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
    102     /** If we should immediately start playback of the voicemail, this extra will be set to true. */
    103     public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK";
    104     /** If the activity was triggered from a notification. */
    105     public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
    106 
    107     public static final String VOICEMAIL_FRAGMENT_TAG = "voicemail_fragment";
    108 
    109     private CallTypeHelper mCallTypeHelper;
    110     private PhoneNumberDisplayHelper mPhoneNumberHelper;
    111     private QuickContactBadge mQuickContactBadge;
    112     private TextView mCallerName;
    113     private TextView mCallerNumber;
    114     private TextView mAccountLabel;
    115     private AsyncTaskExecutor mAsyncTaskExecutor;
    116     private ContactInfoHelper mContactInfoHelper;
    117 
    118     private String mNumber = null;
    119     private String mDefaultCountryIso;
    120 
    121     /* package */ LayoutInflater mInflater;
    122     /* package */ Resources mResources;
    123     /** Helper to load contact photos. */
    124     private ContactPhotoManager mContactPhotoManager;
    125     /** Helper to make async queries to content resolver. */
    126     private CallDetailActivityQueryHandler mAsyncQueryHandler;
    127     /** Helper to get voicemail status messages. */
    128     private VoicemailStatusHelper mVoicemailStatusHelper;
    129     // Views related to voicemail status message.
    130     private View mStatusMessageView;
    131     private TextView mStatusMessageText;
    132     private TextView mStatusMessageAction;
    133     private TextView mVoicemailTranscription;
    134     private LinearLayout mVoicemailHeader;
    135 
    136     private Uri mVoicemailUri;
    137     private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
    138 
    139     /** Whether we should show "edit number before call" in the options menu. */
    140     private boolean mHasEditNumberBeforeCallOption;
    141     /** Whether we should show "trash" in the options menu. */
    142     private boolean mHasTrashOption;
    143     /** Whether we should show "remove from call log" in the options menu. */
    144     private boolean mHasRemoveFromCallLogOption;
    145 
    146     private ProximitySensorManager mProximitySensorManager;
    147     private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener();
    148 
    149     /** Listener to changes in the proximity sensor state. */
    150     private class ProximitySensorListener implements ProximitySensorManager.Listener {
    151         /** Used to show a blank view and hide the action bar. */
    152         private final Runnable mBlankRunnable = new Runnable() {
    153             @Override
    154             public void run() {
    155                 View blankView = findViewById(R.id.blank);
    156                 blankView.setVisibility(View.VISIBLE);
    157                 getActionBar().hide();
    158             }
    159         };
    160         /** Used to remove the blank view and show the action bar. */
    161         private final Runnable mUnblankRunnable = new Runnable() {
    162             @Override
    163             public void run() {
    164                 View blankView = findViewById(R.id.blank);
    165                 blankView.setVisibility(View.GONE);
    166                 getActionBar().show();
    167             }
    168         };
    169 
    170         @Override
    171         public synchronized void onNear() {
    172             clearPendingRequests();
    173             postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS);
    174         }
    175 
    176         @Override
    177         public synchronized void onFar() {
    178             clearPendingRequests();
    179             postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS);
    180         }
    181 
    182         /** Removed any delayed requests that may be pending. */
    183         public synchronized void clearPendingRequests() {
    184             View blankView = findViewById(R.id.blank);
    185             blankView.removeCallbacks(mBlankRunnable);
    186             blankView.removeCallbacks(mUnblankRunnable);
    187         }
    188 
    189         /** Post a {@link Runnable} with a delay on the main thread. */
    190         private synchronized void postDelayed(Runnable runnable, long delayMillis) {
    191             // Post these instead of executing immediately so that:
    192             // - They are guaranteed to be executed on the main thread.
    193             // - If the sensor values changes rapidly for some time, the UI will not be
    194             //   updated immediately.
    195             View blankView = findViewById(R.id.blank);
    196             blankView.postDelayed(runnable, delayMillis);
    197         }
    198     }
    199 
    200     static final String[] CALL_LOG_PROJECTION = new String[] {
    201         CallLog.Calls.DATE,
    202         CallLog.Calls.DURATION,
    203         CallLog.Calls.NUMBER,
    204         CallLog.Calls.TYPE,
    205         CallLog.Calls.COUNTRY_ISO,
    206         CallLog.Calls.GEOCODED_LOCATION,
    207         CallLog.Calls.NUMBER_PRESENTATION,
    208         CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
    209         CallLog.Calls.PHONE_ACCOUNT_ID,
    210         CallLog.Calls.FEATURES,
    211         CallLog.Calls.DATA_USAGE,
    212         CallLog.Calls.TRANSCRIPTION
    213     };
    214 
    215     static final int DATE_COLUMN_INDEX = 0;
    216     static final int DURATION_COLUMN_INDEX = 1;
    217     static final int NUMBER_COLUMN_INDEX = 2;
    218     static final int CALL_TYPE_COLUMN_INDEX = 3;
    219     static final int COUNTRY_ISO_COLUMN_INDEX = 4;
    220     static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
    221     static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
    222     static final int ACCOUNT_COMPONENT_NAME = 7;
    223     static final int ACCOUNT_ID = 8;
    224     static final int FEATURES = 9;
    225     static final int DATA_USAGE = 10;
    226     static final int TRANSCRIPTION_COLUMN_INDEX = 11;
    227 
    228     @Override
    229     protected void onCreate(Bundle icicle) {
    230         super.onCreate(icicle);
    231 
    232         setContentView(R.layout.call_detail);
    233 
    234         mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
    235         mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
    236         mResources = getResources();
    237 
    238         mCallTypeHelper = new CallTypeHelper(getResources());
    239         mPhoneNumberHelper = new PhoneNumberDisplayHelper(this, mResources);
    240         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
    241         mAsyncQueryHandler = new CallDetailActivityQueryHandler(this);
    242 
    243         mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
    244 
    245         mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
    246         mQuickContactBadge.setOverlay(null);
    247         mCallerName = (TextView) findViewById(R.id.caller_name);
    248         mCallerNumber = (TextView) findViewById(R.id.caller_number);
    249         mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
    250         mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this);
    251         mContactPhotoManager = ContactPhotoManager.getInstance(this);
    252         mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener);
    253         mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
    254         getActionBar().setDisplayHomeAsUpEnabled(true);
    255 
    256         optionallyHandleVoicemail();
    257         if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
    258             closeSystemDialogs();
    259         }
    260     }
    261 
    262     @Override
    263     public void onResume() {
    264         super.onResume();
    265         updateData(getCallLogEntryUris());
    266     }
    267 
    268     /**
    269      * Handle voicemail playback or hide voicemail ui.
    270      * <p>
    271      * If the Intent used to start this Activity contains the suitable extras, then start voicemail
    272      * playback.  If it doesn't, then don't inflate the voicemail ui.
    273      */
    274     private void optionallyHandleVoicemail() {
    275 
    276         if (hasVoicemail()) {
    277             LayoutInflater inflater =
    278                     (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    279             mVoicemailHeader =
    280                     (LinearLayout) inflater.inflate(R.layout.call_details_voicemail_header, null);
    281             View voicemailContainer = mVoicemailHeader.findViewById(R.id.voicemail_container);
    282             mStatusMessageView = mVoicemailHeader.findViewById(R.id.voicemail_status);
    283             mStatusMessageText =
    284                     (TextView) mVoicemailHeader.findViewById(R.id.voicemail_status_message);
    285             mStatusMessageAction =
    286                     (TextView) mVoicemailHeader.findViewById(R.id.voicemail_status_action);
    287             mVoicemailTranscription = (
    288                     TextView) mVoicemailHeader.findViewById(R.id.voicemail_transcription);
    289             ListView historyList = (ListView) findViewById(R.id.history);
    290             historyList.addHeaderView(mVoicemailHeader);
    291             // Has voicemail: add the voicemail fragment.  Add suitable arguments to set the uri
    292             // to play and optionally start the playback.
    293             // Do a query to fetch the voicemail status messages.
    294             VoicemailPlaybackFragment playbackFragment;
    295 
    296             playbackFragment = (VoicemailPlaybackFragment) getFragmentManager().findFragmentByTag(
    297                     VOICEMAIL_FRAGMENT_TAG);
    298 
    299             if (playbackFragment == null) {
    300                 playbackFragment = new VoicemailPlaybackFragment();
    301                 Bundle fragmentArguments = new Bundle();
    302                 fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, mVoicemailUri);
    303                 if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) {
    304                     fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true);
    305                 }
    306                 playbackFragment.setArguments(fragmentArguments);
    307                 getFragmentManager().beginTransaction()
    308                         .add(R.id.voicemail_container, playbackFragment, VOICEMAIL_FRAGMENT_TAG)
    309                                 .commitAllowingStateLoss();
    310             }
    311 
    312             voicemailContainer.setVisibility(View.VISIBLE);
    313             mAsyncQueryHandler.startVoicemailStatusQuery(mVoicemailUri);
    314             markVoicemailAsRead(mVoicemailUri);
    315         }
    316     }
    317 
    318     private boolean hasVoicemail() {
    319         return mVoicemailUri != null;
    320     }
    321 
    322     private void markVoicemailAsRead(final Uri voicemailUri) {
    323         mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
    324             @Override
    325             public Void doInBackground(Void... params) {
    326                 ContentValues values = new ContentValues();
    327                 values.put(Voicemails.IS_READ, true);
    328                 getContentResolver().update(voicemailUri, values,
    329                         Voicemails.IS_READ + " = 0", null);
    330                 return null;
    331             }
    332         });
    333     }
    334 
    335     /**
    336      * Returns the list of URIs to show.
    337      * <p>
    338      * There are two ways the URIs can be provided to the activity: as the data on the intent, or as
    339      * a list of ids in the call log added as an extra on the URI.
    340      * <p>
    341      * If both are available, the data on the intent takes precedence.
    342      */
    343     private Uri[] getCallLogEntryUris() {
    344         final Uri uri = getIntent().getData();
    345         if (uri != null) {
    346             // If there is a data on the intent, it takes precedence over the extra.
    347             return new Uri[]{ uri };
    348         }
    349         final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
    350         final int numIds = ids == null ? 0 : ids.length;
    351         final Uri[] uris = new Uri[numIds];
    352         for (int index = 0; index < numIds; ++index) {
    353             uris[index] = ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, ids[index]);
    354         }
    355         return uris;
    356     }
    357 
    358     @Override
    359     public boolean onKeyDown(int keyCode, KeyEvent event) {
    360         switch (keyCode) {
    361             case KeyEvent.KEYCODE_CALL: {
    362                 // Make sure phone isn't already busy before starting direct call
    363                 TelephonyManager tm = (TelephonyManager)
    364                         getSystemService(Context.TELEPHONY_SERVICE);
    365                 if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
    366                     DialerUtils.startActivityWithErrorToast(this,
    367                             CallUtil.getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_TEL, mNumber,
    368                                     null)), R.string.call_not_available);
    369                     return true;
    370                 }
    371             }
    372         }
    373 
    374         return super.onKeyDown(keyCode, event);
    375     }
    376 
    377     /**
    378      * Update user interface with details of given call.
    379      *
    380      * @param callUris URIs into {@link android.provider.CallLog.Calls} of the calls to be displayed
    381      */
    382     private void updateData(final Uri... callUris) {
    383         class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> {
    384             @Override
    385             public PhoneCallDetails[] doInBackground(Void... params) {
    386                 // TODO: All phone calls correspond to the same person, so we can make a single
    387                 // lookup.
    388                 final int numCalls = callUris.length;
    389                 PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
    390                 try {
    391                     for (int index = 0; index < numCalls; ++index) {
    392                         details[index] = getPhoneCallDetailsForUri(callUris[index]);
    393                     }
    394                     return details;
    395                 } catch (IllegalArgumentException e) {
    396                     // Something went wrong reading in our primary data.
    397                     Log.w(TAG, "invalid URI starting call details", e);
    398                     return null;
    399                 }
    400             }
    401 
    402             @Override
    403             public void onPostExecute(PhoneCallDetails[] details) {
    404                 Context context = CallDetailActivity.this;
    405 
    406                 if (details == null) {
    407                     // Somewhere went wrong: we're going to bail out and show error to users.
    408                     Toast.makeText(context, R.string.toast_call_detail_error,
    409                             Toast.LENGTH_SHORT).show();
    410                     finish();
    411                     return;
    412                 }
    413 
    414                 // We know that all calls are from the same number and the same contact, so pick the
    415                 // first.
    416                 PhoneCallDetails firstDetails = details[0];
    417                 mNumber = firstDetails.number.toString();
    418                 final int numberPresentation = firstDetails.numberPresentation;
    419                 final Uri contactUri = firstDetails.contactUri;
    420                 final Uri photoUri = firstDetails.photoUri;
    421                 final PhoneAccountHandle accountHandle = firstDetails.accountHandle;
    422 
    423                 // Cache the details about the phone number.
    424                 final boolean canPlaceCallsTo =
    425                     PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation);
    426                 final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper(context);
    427                 final boolean isVoicemailNumber =
    428                         phoneUtils.isVoicemailNumber(accountHandle, mNumber);
    429                 final boolean isSipNumber = PhoneNumberUtilsWrapper.isSipNumber(mNumber);
    430 
    431                 final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails);
    432 
    433                 final CharSequence displayNumber =
    434                         mPhoneNumberHelper.getDisplayNumber(
    435                                 firstDetails.accountHandle,
    436                                 firstDetails.number,
    437                                 firstDetails.numberPresentation,
    438                                 firstDetails.formattedNumber);
    439                 final String displayNumberStr = mBidiFormatter.unicodeWrap(
    440                         displayNumber.toString(), TextDirectionHeuristics.LTR);
    441 
    442                 if (!TextUtils.isEmpty(firstDetails.name)) {
    443                     mCallerName.setText(firstDetails.name);
    444                     mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
    445                 } else {
    446                     mCallerName.setText(displayNumberStr);
    447                     if (!TextUtils.isEmpty(callLocationOrType)) {
    448                         mCallerNumber.setText(callLocationOrType);
    449                         mCallerNumber.setVisibility(View.VISIBLE);
    450                     } else {
    451                         mCallerNumber.setVisibility(View.GONE);
    452                     }
    453                 }
    454 
    455                 String accountLabel = PhoneAccountUtils.getAccountLabel(context, accountHandle);
    456                 if (!TextUtils.isEmpty(accountLabel)) {
    457                     mAccountLabel.setText(accountLabel);
    458                     mAccountLabel.setVisibility(View.VISIBLE);
    459                 } else {
    460                     mAccountLabel.setVisibility(View.GONE);
    461                 }
    462 
    463                 mHasEditNumberBeforeCallOption =
    464                         canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
    465                 mHasTrashOption = hasVoicemail();
    466                 mHasRemoveFromCallLogOption = !hasVoicemail();
    467                 invalidateOptionsMenu();
    468 
    469                 ListView historyList = (ListView) findViewById(R.id.history);
    470                 historyList.setAdapter(
    471                         new CallDetailHistoryAdapter(context, mInflater, mCallTypeHelper, details));
    472 
    473                 String lookupKey = contactUri == null ? null
    474                         : ContactInfoHelper.getLookupKeyFromUri(contactUri);
    475 
    476                 final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType);
    477 
    478                 final int contactType =
    479                         isVoicemailNumber? ContactPhotoManager.TYPE_VOICEMAIL :
    480                         isBusiness ? ContactPhotoManager.TYPE_BUSINESS :
    481                         ContactPhotoManager.TYPE_DEFAULT;
    482 
    483                 String nameForDefaultImage;
    484                 if (TextUtils.isEmpty(firstDetails.name)) {
    485                     nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(
    486                             firstDetails.accountHandle,
    487                             firstDetails.number,
    488                             firstDetails.numberPresentation,
    489                             firstDetails.formattedNumber).toString();
    490                 } else {
    491                     nameForDefaultImage = firstDetails.name.toString();
    492                 }
    493 
    494                 if (hasVoicemail() && !TextUtils.isEmpty(firstDetails.transcription)) {
    495                     mVoicemailTranscription.setText(firstDetails.transcription);
    496                     mVoicemailTranscription.setVisibility(View.VISIBLE);
    497                 }
    498 
    499                 loadContactPhotos(
    500                         contactUri, photoUri, nameForDefaultImage, lookupKey, contactType);
    501                 findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
    502             }
    503 
    504             /**
    505              * Determines the location geocode text for a call, or the phone number type
    506              * (if available).
    507              *
    508              * @param details The call details.
    509              * @return The phone number type or location.
    510              */
    511             private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
    512                 if (!TextUtils.isEmpty(details.name)) {
    513                     return Phone.getTypeLabel(mResources, details.numberType,
    514                             details.numberLabel);
    515                 } else {
    516                     return details.geocode;
    517                 }
    518             }
    519         }
    520         mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask());
    521     }
    522 
    523     /** Return the phone call details for a given call log URI. */
    524     private PhoneCallDetails getPhoneCallDetailsForUri(Uri callUri) {
    525         ContentResolver resolver = getContentResolver();
    526         Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null);
    527         try {
    528             if (callCursor == null || !callCursor.moveToFirst()) {
    529                 throw new IllegalArgumentException("Cannot find content: " + callUri);
    530             }
    531 
    532             // Read call log specifics.
    533             final String number = callCursor.getString(NUMBER_COLUMN_INDEX);
    534             final int numberPresentation = callCursor.getInt(
    535                     NUMBER_PRESENTATION_COLUMN_INDEX);
    536             final long date = callCursor.getLong(DATE_COLUMN_INDEX);
    537             final long duration = callCursor.getLong(DURATION_COLUMN_INDEX);
    538             final int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX);
    539             String countryIso = callCursor.getString(COUNTRY_ISO_COLUMN_INDEX);
    540             final String geocode = callCursor.getString(GEOCODED_LOCATION_COLUMN_INDEX);
    541             final String transcription = callCursor.getString(TRANSCRIPTION_COLUMN_INDEX);
    542 
    543             final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
    544                     callCursor.getString(ACCOUNT_COMPONENT_NAME),
    545                     callCursor.getString(ACCOUNT_ID));
    546 
    547             if (TextUtils.isEmpty(countryIso)) {
    548                 countryIso = mDefaultCountryIso;
    549             }
    550 
    551             // Formatted phone number.
    552             final CharSequence formattedNumber;
    553             // Read contact specifics.
    554             final CharSequence nameText;
    555             final int numberType;
    556             final CharSequence numberLabel;
    557             final Uri photoUri;
    558             final Uri lookupUri;
    559             int sourceType;
    560             // If this is not a regular number, there is no point in looking it up in the contacts.
    561             ContactInfo info =
    562                     PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
    563                     && !new PhoneNumberUtilsWrapper(this).isVoicemailNumber(accountHandle, number)
    564                             ? mContactInfoHelper.lookupNumber(number, countryIso)
    565                             : null;
    566             if (info == null) {
    567                 formattedNumber = mPhoneNumberHelper.getDisplayNumber(accountHandle, number,
    568                         numberPresentation, null);
    569                 nameText = "";
    570                 numberType = 0;
    571                 numberLabel = "";
    572                 photoUri = null;
    573                 lookupUri = null;
    574                 sourceType = 0;
    575             } else {
    576                 formattedNumber = info.formattedNumber;
    577                 nameText = info.name;
    578                 numberType = info.type;
    579                 numberLabel = info.label;
    580                 photoUri = info.photoUri;
    581                 lookupUri = info.lookupUri;
    582                 sourceType = info.sourceType;
    583             }
    584             final int features = callCursor.getInt(FEATURES);
    585             Long dataUsage = null;
    586             if (!callCursor.isNull(DATA_USAGE)) {
    587                 dataUsage = callCursor.getLong(DATA_USAGE);
    588             }
    589             return new PhoneCallDetails(number, numberPresentation,
    590                     formattedNumber, countryIso, geocode,
    591                     new int[]{ callType }, date, duration,
    592                     nameText, numberType, numberLabel, lookupUri, photoUri, sourceType,
    593                     accountHandle, features, dataUsage, transcription);
    594         } finally {
    595             if (callCursor != null) {
    596                 callCursor.close();
    597             }
    598         }
    599     }
    600 
    601     /** Load the contact photos and places them in the corresponding views. */
    602     private void loadContactPhotos(Uri contactUri, Uri photoUri, String displayName,
    603             String lookupKey, int contactType) {
    604 
    605         final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey,
    606                 contactType, true /* isCircular */);
    607 
    608         mQuickContactBadge.assignContactUri(contactUri);
    609         mQuickContactBadge.setContentDescription(
    610                 mResources.getString(R.string.description_contact_details, displayName));
    611 
    612         mContactPhotoManager.loadDirectoryPhoto(mQuickContactBadge, photoUri,
    613                 false /* darkTheme */, true /* isCircular */, request);
    614     }
    615 
    616     static final class ViewEntry {
    617         public final String text;
    618         public final Intent primaryIntent;
    619         /** The description for accessibility of the primary action. */
    620         public final String primaryDescription;
    621 
    622         public CharSequence label = null;
    623         /** Icon for the secondary action. */
    624         public int secondaryIcon = 0;
    625         /** Intent for the secondary action. If not null, an icon must be defined. */
    626         public Intent secondaryIntent = null;
    627         /** The description for accessibility of the secondary action. */
    628         public String secondaryDescription = null;
    629 
    630         public ViewEntry(String text, Intent intent, String description) {
    631             this.text = text;
    632             primaryIntent = intent;
    633             primaryDescription = description;
    634         }
    635 
    636         public void setSecondaryAction(int icon, Intent intent, String description) {
    637             secondaryIcon = icon;
    638             secondaryIntent = intent;
    639             secondaryDescription = description;
    640         }
    641     }
    642 
    643     protected void updateVoicemailStatusMessage(Cursor statusCursor) {
    644         if (statusCursor == null) {
    645             mStatusMessageView.setVisibility(View.GONE);
    646             return;
    647         }
    648         final StatusMessage message = getStatusMessage(statusCursor);
    649         if (message == null || !message.showInCallDetails()) {
    650             mStatusMessageView.setVisibility(View.GONE);
    651             return;
    652         }
    653 
    654         mStatusMessageView.setVisibility(View.VISIBLE);
    655         mStatusMessageText.setText(message.callDetailsMessageId);
    656         if (message.actionMessageId != -1) {
    657             mStatusMessageAction.setText(message.actionMessageId);
    658         }
    659         if (message.actionUri != null) {
    660             mStatusMessageAction.setClickable(true);
    661             mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
    662                 @Override
    663                 public void onClick(View v) {
    664                     DialerUtils.startActivityWithErrorToast(CallDetailActivity.this,
    665                             new Intent(Intent.ACTION_VIEW, message.actionUri));
    666                 }
    667             });
    668         } else {
    669             mStatusMessageAction.setClickable(false);
    670         }
    671     }
    672 
    673     private StatusMessage getStatusMessage(Cursor statusCursor) {
    674         List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
    675         if (messages.size() == 0) {
    676             return null;
    677         }
    678         // There can only be a single status message per source package, so num of messages can
    679         // at most be 1.
    680         if (messages.size() > 1) {
    681             Log.w(TAG, String.format("Expected 1, found (%d) num of status messages." +
    682                     " Will use the first one.", messages.size()));
    683         }
    684         return messages.get(0);
    685     }
    686 
    687     @Override
    688     public boolean onCreateOptionsMenu(Menu menu) {
    689         getMenuInflater().inflate(R.menu.call_details_options, menu);
    690         return super.onCreateOptionsMenu(menu);
    691     }
    692 
    693     @Override
    694     public boolean onPrepareOptionsMenu(Menu menu) {
    695         // This action deletes all elements in the group from the call log.
    696         // We don't have this action for voicemails, because you can just use the trash button.
    697         menu.findItem(R.id.menu_remove_from_call_log).setVisible(mHasRemoveFromCallLogOption);
    698         menu.findItem(R.id.menu_edit_number_before_call).setVisible(mHasEditNumberBeforeCallOption);
    699         menu.findItem(R.id.menu_trash).setVisible(mHasTrashOption);
    700         return super.onPrepareOptionsMenu(menu);
    701     }
    702 
    703     public void onMenuRemoveFromCallLog(MenuItem menuItem) {
    704         final StringBuilder callIds = new StringBuilder();
    705         for (Uri callUri : getCallLogEntryUris()) {
    706             if (callIds.length() != 0) {
    707                 callIds.append(",");
    708             }
    709             callIds.append(ContentUris.parseId(callUri));
    710         }
    711         mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH,
    712                 new AsyncTask<Void, Void, Void>() {
    713                     @Override
    714                     public Void doInBackground(Void... params) {
    715                         getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL,
    716                                 Calls._ID + " IN (" + callIds + ")", null);
    717                         return null;
    718                     }
    719 
    720                     @Override
    721                     public void onPostExecute(Void result) {
    722                         finish();
    723                     }
    724                 }
    725         );
    726     }
    727 
    728     public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
    729         startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber)));
    730     }
    731 
    732     public void onMenuTrashVoicemail(MenuItem menuItem) {
    733         mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH,
    734                 new AsyncTask<Void, Void, Void>() {
    735                     @Override
    736                     public Void doInBackground(Void... params) {
    737                         getContentResolver().delete(mVoicemailUri, null, null);
    738                         return null;
    739                     }
    740 
    741                     @Override
    742                     public void onPostExecute(Void result) {
    743                         finish();
    744                     }
    745                 }
    746         );
    747     }
    748 
    749     @Override
    750     protected void onPause() {
    751         // Immediately stop the proximity sensor.
    752         disableProximitySensor(false);
    753         mProximitySensorListener.clearPendingRequests();
    754         super.onPause();
    755     }
    756 
    757     @Override
    758     public void enableProximitySensor() {
    759         mProximitySensorManager.enable();
    760     }
    761 
    762     @Override
    763     public void disableProximitySensor(boolean waitForFarState) {
    764         mProximitySensorManager.disable(waitForFarState);
    765     }
    766 
    767     private void closeSystemDialogs() {
    768         sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
    769     }
    770 
    771     /** Returns the given text, forced to be left-to-right. */
    772     private static CharSequence forceLeftToRight(CharSequence text) {
    773         StringBuilder sb = new StringBuilder();
    774         sb.append(LEFT_TO_RIGHT_EMBEDDING);
    775         sb.append(text);
    776         sb.append(POP_DIRECTIONAL_FORMATTING);
    777         return sb.toString();
    778     }
    779 }
    780