• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    */
PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache)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. */
setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details)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 
shouldShowTranscriptionRating( int transcriptionState, PhoneAccountHandle account)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 
recordTranscriptionRating( TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView)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 
shouldShowVoicemailDonationPromo(Context context)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 
hasSeenVoicemailDonationPromo(Context context)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 
showVoicemailDonationPromo( TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView)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 
dpsToPixels(Context context, int dps)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 
recordPromoShown(Context context)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 
getVoicemailDonationPromoContent()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
onRatingSuccess(Uri voicemailUri)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
onRatingFailure(Throwable t)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    */
getCallLocationAndDate(PhoneCallDetails details)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    */
getCallTypeOrLocation(PhoneCallDetails details)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. */
shouldShowLocation(PhoneCallDetails details)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 
setPhoneTypeLabelForTest(CharSequence phoneTypeLabel)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    */
getCallDate(PhoneCallDetails details)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    */
getGranularDateTime(PhoneCallDetails details)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    */
getGranularDate(long date)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    */
shouldShowYear(long date)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. */
setCallDetailsHeader(TextView nameView, PhoneCallDetails details)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 
setCurrentTimeForTest(long currentTimeMillis)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    */
getCurrentTimeMillis()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. */
setDetailText( PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details)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 
getVoicemailDuration(PhoneCallDetails details)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