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