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
onNear()171         public synchronized void onNear() {
172             clearPendingRequests();
173             postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS);
174         }
175 
176         @Override
onFar()177         public synchronized void onFar() {
178             clearPendingRequests();
179             postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS);
180         }
181 
182         /** Removed any delayed requests that may be pending. */
clearPendingRequests()183         public synchronized void clearPendingRequests() {
184             View blankView = findViewById(R.id.blank);
185             blankView.removeCallbacks(mBlankRunnable);
186             blankView.removeCallbacks(mUnblankRunnable);
187         }
188 
189         /** Post a {@link Runnable} with a delay on the main thread. */
postDelayed(Runnable runnable, long delayMillis)190         private synchronized void postDelayed(Runnable runnable, long delayMillis) {
191             // Post these instead of executing immediately so that:
192             // - They are guaranteed to be executed on the main thread.
193             // - If the sensor values changes rapidly for some time, the UI will not be
194             //   updated immediately.
195             View blankView = findViewById(R.id.blank);
196             blankView.postDelayed(runnable, delayMillis);
197         }
198     }
199 
200     static final String[] CALL_LOG_PROJECTION = new String[] {
201         CallLog.Calls.DATE,
202         CallLog.Calls.DURATION,
203         CallLog.Calls.NUMBER,
204         CallLog.Calls.TYPE,
205         CallLog.Calls.COUNTRY_ISO,
206         CallLog.Calls.GEOCODED_LOCATION,
207         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
onCreate(Bundle icicle)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
onResume()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      */
optionallyHandleVoicemail()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 
hasVoicemail()318     private boolean hasVoicemail() {
319         return mVoicemailUri != null;
320     }
321 
markVoicemailAsRead(final Uri voicemailUri)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      */
getCallLogEntryUris()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
onKeyDown(int keyCode, KeyEvent event)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      */
updateData(final Uri... callUris)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. */
getPhoneCallDetailsForUri(Uri callUri)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. */
loadContactPhotos(Uri contactUri, Uri photoUri, String displayName, String lookupKey, int contactType)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 
ViewEntry(String text, Intent intent, String description)630         public ViewEntry(String text, Intent intent, String description) {
631             this.text = text;
632             primaryIntent = intent;
633             primaryDescription = description;
634         }
635 
setSecondaryAction(int icon, Intent intent, String description)636         public void setSecondaryAction(int icon, Intent intent, String description) {
637             secondaryIcon = icon;
638             secondaryIntent = intent;
639             secondaryDescription = description;
640         }
641     }
642 
updateVoicemailStatusMessage(Cursor statusCursor)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 
getStatusMessage(Cursor statusCursor)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
onCreateOptionsMenu(Menu menu)688     public boolean onCreateOptionsMenu(Menu menu) {
689         getMenuInflater().inflate(R.menu.call_details_options, menu);
690         return super.onCreateOptionsMenu(menu);
691     }
692 
693     @Override
onPrepareOptionsMenu(Menu menu)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 
onMenuRemoveFromCallLog(MenuItem menuItem)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 
onMenuEditNumberBeforeCall(MenuItem menuItem)728     public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
729         startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber)));
730     }
731 
onMenuTrashVoicemail(MenuItem menuItem)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
onPause()750     protected void onPause() {
751         // Immediately stop the proximity sensor.
752         disableProximitySensor(false);
753         mProximitySensorListener.clearPendingRequests();
754         super.onPause();
755     }
756 
757     @Override
enableProximitySensor()758     public void enableProximitySensor() {
759         mProximitySensorManager.enable();
760     }
761 
762     @Override
disableProximitySensor(boolean waitForFarState)763     public void disableProximitySensor(boolean waitForFarState) {
764         mProximitySensorManager.disable(waitForFarState);
765     }
766 
closeSystemDialogs()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. */
forceLeftToRight(CharSequence text)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