Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 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.app.calllog;
     18 
     19 import android.app.AlertDialog;
     20 import android.content.Context;
     21 import android.content.DialogInterface;
     22 import android.content.res.Resources;
     23 import android.graphics.Typeface;
     24 import android.net.Uri;
     25 import android.provider.CallLog.Calls;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.support.v4.content.ContextCompat;
     28 import android.telecom.PhoneAccount;
     29 import android.telecom.PhoneAccountHandle;
     30 import android.text.SpannableString;
     31 import android.text.Spanned;
     32 import android.text.TextUtils;
     33 import android.text.format.DateUtils;
     34 import android.text.method.LinkMovementMethod;
     35 import android.text.style.TextAppearanceSpan;
     36 import android.text.style.URLSpan;
     37 import android.text.util.Linkify;
     38 import android.util.TypedValue;
     39 import android.view.Gravity;
     40 import android.view.View;
     41 import android.widget.Button;
     42 import android.widget.TextView;
     43 import android.widget.Toast;
     44 import com.android.dialer.app.R;
     45 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
     46 import com.android.dialer.calllogutils.PhoneCallDetails;
     47 import com.android.dialer.common.LogUtil;
     48 import com.android.dialer.compat.android.provider.VoicemailCompat;
     49 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
     50 import com.android.dialer.logging.ContactSource;
     51 import com.android.dialer.oem.MotorolaUtils;
     52 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     53 import com.android.dialer.storage.StorageComponent;
     54 import com.android.dialer.util.DialerUtils;
     55 import com.android.voicemail.VoicemailClient;
     56 import com.android.voicemail.VoicemailComponent;
     57 import com.android.voicemail.impl.transcribe.TranscriptionRatingHelper;
     58 import com.google.internal.communications.voicemailtranscription.v1.TranscriptionRatingValue;
     59 import java.util.ArrayList;
     60 import java.util.Calendar;
     61 import java.util.concurrent.TimeUnit;
     62 
     63 /** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */
     64 public class PhoneCallDetailsHelper
     65     implements TranscriptionRatingHelper.SuccessListener,
     66         TranscriptionRatingHelper.FailureListener {
     67   /** The maximum number of icons will be shown to represent the call types in a group. */
     68   private static final int MAX_CALL_TYPE_ICONS = 3;
     69 
     70   private static final String PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY =
     71       "pref_voicemail_donation_promo_shown_key";
     72 
     73   private final Context context;
     74   private final Resources resources;
     75   private final CallLogCache callLogCache;
     76   /** Calendar used to construct dates */
     77   private final Calendar calendar;
     78   /** The injected current time in milliseconds since the epoch. Used only by tests. */
     79   private Long currentTimeMillisForTest;
     80 
     81   private CharSequence phoneTypeLabelForTest;
     82   /** List of items to be concatenated together for accessibility descriptions */
     83   private ArrayList<CharSequence> descriptionItems = new ArrayList<>();
     84 
     85   /**
     86    * Creates a new instance of the helper.
     87    *
     88    * <p>Generally you should have a single instance of this helper in any context.
     89    *
     90    * @param resources used to look up strings
     91    */
     92   public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) {
     93     this.context = context;
     94     this.resources = resources;
     95     this.callLogCache = callLogCache;
     96     calendar = Calendar.getInstance();
     97   }
     98 
     99   /** Fills the call details views with content. */
    100   public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) {
    101     // Display up to a given number of icons.
    102     views.callTypeIcons.clear();
    103     int count = details.callTypes.length;
    104     boolean isVoicemail = false;
    105     for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) {
    106       views.callTypeIcons.add(details.callTypes[index]);
    107       if (index == 0) {
    108         isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE;
    109       }
    110     }
    111 
    112     // Show the video icon if the call had video enabled.
    113     views.callTypeIcons.setShowVideo(
    114         (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
    115     views.callTypeIcons.setShowHd(
    116         (details.features & Calls.FEATURES_HD_CALL) == Calls.FEATURES_HD_CALL);
    117     views.callTypeIcons.setShowWifi(
    118         MotorolaUtils.shouldShowWifiIconInCallLog(context, details.features));
    119     views.callTypeIcons.setShowAssistedDialed(
    120         (details.features & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
    121             == TelephonyManagerCompat.FEATURES_ASSISTED_DIALING);
    122     views.callTypeIcons.requestLayout();
    123     views.callTypeIcons.setVisibility(View.VISIBLE);
    124 
    125     // Show the total call count only if there are more than the maximum number of icons.
    126     final Integer callCount;
    127     if (count > MAX_CALL_TYPE_ICONS) {
    128       callCount = count;
    129     } else {
    130       callCount = null;
    131     }
    132 
    133     // Set the call count, location, date and if voicemail, set the duration.
    134     setDetailText(views, callCount, details);
    135 
    136     // Set the account label if it exists.
    137     String accountLabel = callLogCache.getAccountLabel(details.accountHandle);
    138     if (!TextUtils.isEmpty(details.viaNumber)) {
    139       if (!TextUtils.isEmpty(accountLabel)) {
    140         accountLabel =
    141             resources.getString(
    142                 R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber);
    143       } else {
    144         accountLabel = resources.getString(R.string.call_log_via_number, details.viaNumber);
    145       }
    146     }
    147     if (!TextUtils.isEmpty(accountLabel)) {
    148       views.callAccountLabel.setVisibility(View.VISIBLE);
    149       views.callAccountLabel.setText(accountLabel);
    150       int color = callLogCache.getAccountColor(details.accountHandle);
    151       if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
    152         int defaultColor = R.color.dialer_secondary_text_color;
    153         views.callAccountLabel.setTextColor(context.getResources().getColor(defaultColor));
    154       } else {
    155         views.callAccountLabel.setTextColor(color);
    156       }
    157     } else {
    158       views.callAccountLabel.setVisibility(View.GONE);
    159     }
    160 
    161     final CharSequence nameText;
    162     final CharSequence displayNumber = details.displayNumber;
    163     if (TextUtils.isEmpty(details.getPreferredName())) {
    164       nameText = displayNumber;
    165       // We have a real phone number as "nameView" so make it always LTR
    166       views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR);
    167     } else {
    168       nameText = details.getPreferredName();
    169       // "nameView" is updated from phone number to contact name after number matching.
    170       // Since TextDirection remains at View.TEXT_DIRECTION_LTR, initialize it.
    171       views.nameView.setTextDirection(View.TEXT_DIRECTION_INHERIT);
    172     }
    173 
    174     views.nameView.setText(nameText);
    175 
    176     if (isVoicemail) {
    177       int relevantLinkTypes = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS;
    178       views.voicemailTranscriptionView.setAutoLinkMask(relevantLinkTypes);
    179 
    180       String transcript = "";
    181       String branding = "";
    182       if (!TextUtils.isEmpty(details.transcription)) {
    183         transcript = details.transcription;
    184 
    185         if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE
    186             || details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE_AND_RATED) {
    187           branding = resources.getString(R.string.voicemail_transcription_branding_text);
    188         }
    189       } else {
    190         switch (details.transcriptionState) {
    191           case VoicemailCompat.TRANSCRIPTION_IN_PROGRESS:
    192             branding = resources.getString(R.string.voicemail_transcription_in_progress);
    193             break;
    194           case VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED:
    195             branding = resources.getString(R.string.voicemail_transcription_failed_no_speech);
    196             break;
    197           case VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED:
    198             branding =
    199                 resources.getString(R.string.voicemail_transcription_failed_language_not_supported);
    200             break;
    201           case VoicemailCompat.TRANSCRIPTION_FAILED:
    202             branding = resources.getString(R.string.voicemail_transcription_failed);
    203             break;
    204           default:
    205             break; // Fall through
    206         }
    207       }
    208 
    209       views.voicemailTranscriptionView.setText(transcript);
    210       views.voicemailTranscriptionBrandingView.setText(branding);
    211 
    212       View ratingView = views.voicemailTranscriptionRatingView;
    213       if (shouldShowTranscriptionRating(details.transcriptionState, details.accountHandle)) {
    214         ratingView.setVisibility(View.VISIBLE);
    215         ratingView
    216             .findViewById(R.id.voicemail_transcription_rating_good)
    217             .setOnClickListener(
    218                 view ->
    219                     recordTranscriptionRating(
    220                         TranscriptionRatingValue.GOOD_TRANSCRIPTION, details, ratingView));
    221         ratingView
    222             .findViewById(R.id.voicemail_transcription_rating_bad)
    223             .setOnClickListener(
    224                 view ->
    225                     recordTranscriptionRating(
    226                         TranscriptionRatingValue.BAD_TRANSCRIPTION, details, ratingView));
    227       } else {
    228         ratingView.setVisibility(View.GONE);
    229       }
    230     }
    231 
    232     // Bold if not read
    233     Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD;
    234     views.nameView.setTypeface(typeface);
    235     views.voicemailTranscriptionView.setTypeface(typeface);
    236     views.voicemailTranscriptionBrandingView.setTypeface(typeface);
    237     views.callLocationAndDate.setTypeface(typeface);
    238     views.callLocationAndDate.setTextColor(
    239         ContextCompat.getColor(
    240             context,
    241             details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color));
    242   }
    243 
    244   private boolean shouldShowTranscriptionRating(
    245       int transcriptionState, PhoneAccountHandle account) {
    246     if (transcriptionState != VoicemailCompat.TRANSCRIPTION_AVAILABLE) {
    247       return false;
    248     }
    249 
    250     VoicemailClient client = VoicemailComponent.get(context).getVoicemailClient();
    251     if (client.isVoicemailDonationEnabled(context, account)) {
    252       return true;
    253     }
    254 
    255     // Also show the rating option if voicemail transcription is available (but not enabled)
    256     // and the donation promo has not yet been shown.
    257     if (client.isVoicemailDonationAvailable(context) && !hasSeenVoicemailDonationPromo(context)) {
    258       return true;
    259     }
    260 
    261     return false;
    262   }
    263 
    264   private void recordTranscriptionRating(
    265       TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView) {
    266     LogUtil.enterBlock("PhoneCallDetailsHelper.recordTranscriptionRating");
    267 
    268     if (shouldShowVoicemailDonationPromo(context)) {
    269       showVoicemailDonationPromo(ratingValue, details, ratingView);
    270     } else {
    271       TranscriptionRatingHelper.sendRating(
    272           context,
    273           ratingValue,
    274           Uri.parse(details.voicemailUri),
    275           this::onRatingSuccess,
    276           this::onRatingFailure);
    277     }
    278   }
    279 
    280   static boolean shouldShowVoicemailDonationPromo(Context context) {
    281     VoicemailClient client = VoicemailComponent.get(context).getVoicemailClient();
    282     return client.isVoicemailTranscriptionAvailable(context)
    283         && client.isVoicemailDonationAvailable(context)
    284         && !hasSeenVoicemailDonationPromo(context);
    285   }
    286 
    287   static boolean hasSeenVoicemailDonationPromo(Context context) {
    288     return StorageComponent.get(context.getApplicationContext())
    289         .unencryptedSharedPrefs()
    290         .getBoolean(PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY, false);
    291   }
    292 
    293   private void showVoicemailDonationPromo(
    294       TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView) {
    295     AlertDialog.Builder builder = new AlertDialog.Builder(context);
    296     builder.setMessage(getVoicemailDonationPromoContent());
    297     builder.setPositiveButton(
    298         R.string.voicemail_donation_promo_opt_in,
    299         new DialogInterface.OnClickListener() {
    300           @Override
    301           public void onClick(final DialogInterface dialog, final int button) {
    302             LogUtil.i("PhoneCallDetailsHelper.showVoicemailDonationPromo", "onClick");
    303             dialog.cancel();
    304             recordPromoShown(context);
    305             VoicemailComponent.get(context)
    306                 .getVoicemailClient()
    307                 .setVoicemailDonationEnabled(context, details.accountHandle, true);
    308             TranscriptionRatingHelper.sendRating(
    309                 context,
    310                 ratingValue,
    311                 Uri.parse(details.voicemailUri),
    312                 PhoneCallDetailsHelper.this::onRatingSuccess,
    313                 PhoneCallDetailsHelper.this::onRatingFailure);
    314             ratingView.setVisibility(View.GONE);
    315           }
    316         });
    317     builder.setNegativeButton(
    318         R.string.voicemail_donation_promo_opt_out,
    319         new DialogInterface.OnClickListener() {
    320           @Override
    321           public void onClick(final DialogInterface dialog, final int button) {
    322             dialog.cancel();
    323             recordPromoShown(context);
    324             ratingView.setVisibility(View.GONE);
    325           }
    326         });
    327     builder.setCancelable(true);
    328     AlertDialog dialog = builder.create();
    329 
    330     // Use a custom title to prevent truncation, sigh
    331     TextView title = new TextView(context);
    332     title.setText(R.string.voicemail_donation_promo_title);
    333 
    334     title.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
    335     title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
    336     title.setTextColor(ContextCompat.getColor(context, R.color.dialer_primary_text_color));
    337     title.setPadding(
    338         dpsToPixels(context, 24), /* left */
    339         dpsToPixels(context, 10), /* top */
    340         dpsToPixels(context, 24), /* right */
    341         dpsToPixels(context, 0)); /* bottom */
    342     dialog.setCustomTitle(title);
    343 
    344     dialog.show();
    345 
    346     // Make the message link clickable and adjust the appearance of the message and buttons
    347     TextView textView = (TextView) dialog.findViewById(android.R.id.message);
    348     textView.setLineSpacing(0, 1.2f);
    349     textView.setMovementMethod(LinkMovementMethod.getInstance());
    350     Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
    351     if (positiveButton != null) {
    352       positiveButton.setTextColor(
    353           context
    354               .getResources()
    355               .getColor(R.color.voicemail_donation_promo_positive_button_text_color));
    356     }
    357     Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
    358     if (negativeButton != null) {
    359       negativeButton.setTextColor(
    360           context
    361               .getResources()
    362               .getColor(R.color.voicemail_donation_promo_negative_button_text_color));
    363     }
    364   }
    365 
    366   private static int dpsToPixels(Context context, int dps) {
    367     return (int)
    368         (TypedValue.applyDimension(
    369             TypedValue.COMPLEX_UNIT_DIP, dps, context.getResources().getDisplayMetrics()));
    370   }
    371 
    372   private static void recordPromoShown(Context context) {
    373     StorageComponent.get(context.getApplicationContext())
    374         .unencryptedSharedPrefs()
    375         .edit()
    376         .putBoolean(PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY, true)
    377         .apply();
    378   }
    379 
    380   private SpannableString getVoicemailDonationPromoContent() {
    381     CharSequence content = context.getString(R.string.voicemail_donation_promo_content);
    382     CharSequence learnMore = context.getString(R.string.voicemail_donation_promo_learn_more);
    383     String learnMoreUrl = context.getString(R.string.voicemail_donation_promo_learn_more_url);
    384     SpannableString span = new SpannableString(content + " " + learnMore);
    385     int end = span.length();
    386     int start = end - learnMore.length();
    387     span.setSpan(new URLSpan(learnMoreUrl), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    388     span.setSpan(
    389         new TextAppearanceSpan(context, R.style.PromoLinkStyle),
    390         start,
    391         end,
    392         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    393     return span;
    394   }
    395 
    396   @Override
    397   public void onRatingSuccess(Uri voicemailUri) {
    398     LogUtil.enterBlock("PhoneCallDetailsHelper.onRatingSuccess");
    399     Toast toast =
    400         Toast.makeText(context, R.string.voicemail_transcription_rating_thanks, Toast.LENGTH_LONG);
    401     toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 50);
    402     toast.show();
    403   }
    404 
    405   @Override
    406   public void onRatingFailure(Throwable t) {
    407     LogUtil.e("PhoneCallDetailsHelper.onRatingFailure", "failed to send rating", t);
    408   }
    409 
    410   /**
    411    * Builds a string containing the call location and date. For voicemail logs only the call date is
    412    * returned because location information is displayed in the call action button
    413    *
    414    * @param details The call details.
    415    * @return The call location and date string.
    416    */
    417   public CharSequence getCallLocationAndDate(PhoneCallDetails details) {
    418     descriptionItems.clear();
    419 
    420     if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) {
    421       // Get type of call (ie mobile, home, etc) if known, or the caller's location.
    422       CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
    423 
    424       // Only add the call type or location if its not empty.  It will be empty for unknown
    425       // callers.
    426       if (!TextUtils.isEmpty(callTypeOrLocation)) {
    427         descriptionItems.add(callTypeOrLocation);
    428       }
    429     }
    430 
    431     // The date of this call
    432     descriptionItems.add(getCallDate(details));
    433 
    434     // Create a comma separated list from the call type or location, and call date.
    435     return DialerUtils.join(descriptionItems);
    436   }
    437 
    438   /**
    439    * For a call, if there is an associated contact for the caller, return the known call type (e.g.
    440    * mobile, home, work). If there is no associated contact, attempt to use the caller's location if
    441    * known.
    442    *
    443    * @param details Call details to use.
    444    * @return Type of call (mobile/home) if known, or the location of the caller (if known).
    445    */
    446   public CharSequence getCallTypeOrLocation(PhoneCallDetails details) {
    447     if (details.isSpam) {
    448       return resources.getString(R.string.spam_number_call_log_label);
    449     } else if (details.isBlocked) {
    450       return resources.getString(R.string.blocked_number_call_log_label);
    451     }
    452 
    453     CharSequence numberFormattedLabel = null;
    454     // Only show a label if the number is shown and it is not a SIP address.
    455     if (!TextUtils.isEmpty(details.number)
    456         && !PhoneNumberHelper.isUriNumber(details.number.toString())
    457         && !callLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
    458 
    459       if (shouldShowLocation(details)) {
    460         numberFormattedLabel = details.geocode;
    461       } else if (!(details.numberType == Phone.TYPE_CUSTOM
    462           && TextUtils.isEmpty(details.numberLabel))) {
    463         // Get type label only if it will not be "Custom" because of an empty number label.
    464         numberFormattedLabel =
    465             phoneTypeLabelForTest != null
    466                 ? phoneTypeLabelForTest
    467                 : Phone.getTypeLabel(resources, details.numberType, details.numberLabel);
    468       }
    469     }
    470 
    471     if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) {
    472       numberFormattedLabel = details.displayNumber;
    473     }
    474     return numberFormattedLabel;
    475   }
    476 
    477   /** Returns true if primary name is empty or the data is from Cequint Caller ID. */
    478   private static boolean shouldShowLocation(PhoneCallDetails details) {
    479     if (TextUtils.isEmpty(details.geocode)) {
    480       return false;
    481     }
    482     // For caller ID provided by Cequint we want to show the geo location.
    483     if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
    484       return true;
    485     }
    486     // Don't bother showing geo location for contacts.
    487     if (!TextUtils.isEmpty(details.namePrimary)) {
    488       return false;
    489     }
    490     return true;
    491   }
    492 
    493   public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) {
    494     this.phoneTypeLabelForTest = phoneTypeLabel;
    495   }
    496 
    497   /**
    498    * Get the call date/time of the call. For the call log this is relative to the current time. e.g.
    499    * 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)}
    500    *
    501    * @param details Call details to use.
    502    * @return String representing when the call occurred.
    503    */
    504   public CharSequence getCallDate(PhoneCallDetails details) {
    505     if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) {
    506       return getGranularDateTime(details);
    507     }
    508 
    509     return DateUtils.getRelativeTimeSpanString(
    510         details.date,
    511         getCurrentTimeMillis(),
    512         DateUtils.MINUTE_IN_MILLIS,
    513         DateUtils.FORMAT_ABBREV_RELATIVE);
    514   }
    515 
    516   /**
    517    * Get the granular version of the call date/time of the call. The result is always in the form
    518    * 'DATE at TIME'. The date value changes based on when the call was created.
    519    *
    520    * <p>If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is
    521    * 'MMM dd, yyyy'
    522    *
    523    * <p>TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm'
    524    *
    525    * @param details Call details to use
    526    * @return String representing when the call occurred
    527    */
    528   public CharSequence getGranularDateTime(PhoneCallDetails details) {
    529     return resources.getString(
    530         R.string.voicemailCallLogDateTimeFormat,
    531         getGranularDate(details.date),
    532         DateUtils.formatDateTime(context, details.date, DateUtils.FORMAT_SHOW_TIME));
    533   }
    534 
    535   /**
    536    * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)}
    537    */
    538   private String getGranularDate(long date) {
    539     if (DateUtils.isToday(date)) {
    540       return resources.getString(R.string.voicemailCallLogToday);
    541     }
    542     return DateUtils.formatDateTime(
    543         context,
    544         date,
    545         DateUtils.FORMAT_SHOW_DATE
    546             | DateUtils.FORMAT_ABBREV_MONTH
    547             | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
    548   }
    549 
    550   /**
    551    * Determines whether the year should be shown for the given date
    552    *
    553    * @return {@code true} if date is within the current year, {@code false} otherwise
    554    */
    555   private boolean shouldShowYear(long date) {
    556     calendar.setTimeInMillis(getCurrentTimeMillis());
    557     int currentYear = calendar.get(Calendar.YEAR);
    558     calendar.setTimeInMillis(date);
    559     return currentYear != calendar.get(Calendar.YEAR);
    560   }
    561 
    562   /** Sets the text of the header view for the details page of a phone call. */
    563   public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) {
    564     final CharSequence nameText;
    565     if (!TextUtils.isEmpty(details.namePrimary)) {
    566       nameText = details.namePrimary;
    567     } else if (!TextUtils.isEmpty(details.displayNumber)) {
    568       nameText = details.displayNumber;
    569     } else {
    570       nameText = resources.getString(R.string.unknown);
    571     }
    572 
    573     nameView.setText(nameText);
    574   }
    575 
    576   public void setCurrentTimeForTest(long currentTimeMillis) {
    577     currentTimeMillisForTest = currentTimeMillis;
    578   }
    579 
    580   /**
    581    * Returns the current time in milliseconds since the epoch.
    582    *
    583    * <p>It can be injected in tests using {@link #setCurrentTimeForTest(long)}.
    584    */
    585   private long getCurrentTimeMillis() {
    586     if (currentTimeMillisForTest == null) {
    587       return System.currentTimeMillis();
    588     } else {
    589       return currentTimeMillisForTest;
    590     }
    591   }
    592 
    593   /** Sets the call count, date, and if it is a voicemail, sets the duration. */
    594   private void setDetailText(
    595       PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) {
    596     // Combine the count (if present) and the date.
    597     CharSequence dateText = details.callLocationAndDate;
    598     final CharSequence text;
    599     if (callCount != null) {
    600       text = resources.getString(R.string.call_log_item_count_and_date, callCount, dateText);
    601     } else {
    602       text = dateText;
    603     }
    604 
    605     if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
    606       views.callLocationAndDate.setText(
    607           resources.getString(
    608               R.string.voicemailCallLogDateTimeFormatWithDuration,
    609               text,
    610               getVoicemailDuration(details)));
    611     } else {
    612       views.callLocationAndDate.setText(text);
    613     }
    614   }
    615 
    616   private String getVoicemailDuration(PhoneCallDetails details) {
    617     long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
    618     long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
    619     if (minutes > 99) {
    620       minutes = 99;
    621     }
    622     return resources.getString(R.string.voicemailDurationFormat, minutes, seconds);
    623   }
    624 }
    625