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
17package com.android.dialer.app.calllog;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Typeface;
22import android.provider.CallLog.Calls;
23import android.provider.ContactsContract.CommonDataKinds.Phone;
24import android.support.v4.content.ContextCompat;
25import android.telecom.PhoneAccount;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.text.util.Linkify;
29import android.view.View;
30import android.widget.TextView;
31import com.android.dialer.app.R;
32import com.android.dialer.app.calllog.calllogcache.CallLogCache;
33import com.android.dialer.calllogutils.PhoneCallDetails;
34import com.android.dialer.logging.ContactSource;
35import com.android.dialer.oem.MotorolaUtils;
36import com.android.dialer.phonenumberutil.PhoneNumberHelper;
37import com.android.dialer.util.DialerUtils;
38import java.util.ArrayList;
39import java.util.Calendar;
40import java.util.concurrent.TimeUnit;
41
42/** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */
43public class PhoneCallDetailsHelper {
44
45  /** The maximum number of icons will be shown to represent the call types in a group. */
46  private static final int MAX_CALL_TYPE_ICONS = 3;
47
48  private final Context mContext;
49  private final Resources mResources;
50  private final CallLogCache mCallLogCache;
51  /** Calendar used to construct dates */
52  private final Calendar mCalendar;
53  /** The injected current time in milliseconds since the epoch. Used only by tests. */
54  private Long mCurrentTimeMillisForTest;
55
56  private CharSequence mPhoneTypeLabelForTest;
57  /** List of items to be concatenated together for accessibility descriptions */
58  private ArrayList<CharSequence> mDescriptionItems = new ArrayList<>();
59
60  /**
61   * Creates a new instance of the helper.
62   *
63   * <p>Generally you should have a single instance of this helper in any context.
64   *
65   * @param resources used to look up strings
66   */
67  public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) {
68    mContext = context;
69    mResources = resources;
70    mCallLogCache = callLogCache;
71    mCalendar = Calendar.getInstance();
72  }
73
74  /** Fills the call details views with content. */
75  public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) {
76    // Display up to a given number of icons.
77    views.callTypeIcons.clear();
78    int count = details.callTypes.length;
79    boolean isVoicemail = false;
80    for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) {
81      views.callTypeIcons.add(details.callTypes[index]);
82      if (index == 0) {
83        isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE;
84      }
85    }
86
87    // Show the video icon if the call had video enabled.
88    views.callTypeIcons.setShowVideo(
89        (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
90    views.callTypeIcons.setShowHd(
91        MotorolaUtils.shouldShowHdIconInCallLog(mContext, details.features));
92    views.callTypeIcons.setShowWifi(
93        MotorolaUtils.shouldShowWifiIconInCallLog(mContext, details.features));
94    views.callTypeIcons.requestLayout();
95    views.callTypeIcons.setVisibility(View.VISIBLE);
96
97    // Show the total call count only if there are more than the maximum number of icons.
98    final Integer callCount;
99    if (count > MAX_CALL_TYPE_ICONS) {
100      callCount = count;
101    } else {
102      callCount = null;
103    }
104
105    // Set the call count, location, date and if voicemail, set the duration.
106    setDetailText(views, callCount, details);
107
108    // Set the account label if it exists.
109    String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
110    if (!TextUtils.isEmpty(details.viaNumber)) {
111      if (!TextUtils.isEmpty(accountLabel)) {
112        accountLabel =
113            mResources.getString(
114                R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber);
115      } else {
116        accountLabel = mResources.getString(R.string.call_log_via_number, details.viaNumber);
117      }
118    }
119    if (!TextUtils.isEmpty(accountLabel)) {
120      views.callAccountLabel.setVisibility(View.VISIBLE);
121      views.callAccountLabel.setText(accountLabel);
122      int color = mCallLogCache.getAccountColor(details.accountHandle);
123      if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
124        int defaultColor = R.color.dialer_secondary_text_color;
125        views.callAccountLabel.setTextColor(mContext.getResources().getColor(defaultColor));
126      } else {
127        views.callAccountLabel.setTextColor(color);
128      }
129    } else {
130      views.callAccountLabel.setVisibility(View.GONE);
131    }
132
133    final CharSequence nameText;
134    final CharSequence displayNumber = details.displayNumber;
135    if (TextUtils.isEmpty(details.getPreferredName())) {
136      nameText = displayNumber;
137      // We have a real phone number as "nameView" so make it always LTR
138      views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR);
139    } else {
140      nameText = details.getPreferredName();
141    }
142
143    views.nameView.setText(nameText);
144
145    if (isVoicemail) {
146      int relevantLinkTypes = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS;
147      views.voicemailTranscriptionView.setAutoLinkMask(relevantLinkTypes);
148      views.voicemailTranscriptionView.setText(
149          TextUtils.isEmpty(details.transcription) ? null : details.transcription);
150    }
151
152    // Bold if not read
153    Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD;
154    views.nameView.setTypeface(typeface);
155    views.voicemailTranscriptionView.setTypeface(typeface);
156    views.callLocationAndDate.setTypeface(typeface);
157    views.callLocationAndDate.setTextColor(
158        ContextCompat.getColor(
159            mContext,
160            details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color));
161  }
162
163  /**
164   * Builds a string containing the call location and date. For voicemail logs only the call date is
165   * returned because location information is displayed in the call action button
166   *
167   * @param details The call details.
168   * @return The call location and date string.
169   */
170  public CharSequence getCallLocationAndDate(PhoneCallDetails details) {
171    mDescriptionItems.clear();
172
173    if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) {
174      // Get type of call (ie mobile, home, etc) if known, or the caller's location.
175      CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
176
177      // Only add the call type or location if its not empty.  It will be empty for unknown
178      // callers.
179      if (!TextUtils.isEmpty(callTypeOrLocation)) {
180        mDescriptionItems.add(callTypeOrLocation);
181      }
182    }
183
184    // The date of this call
185    mDescriptionItems.add(getCallDate(details));
186
187    // Create a comma separated list from the call type or location, and call date.
188    return DialerUtils.join(mDescriptionItems);
189  }
190
191  /**
192   * For a call, if there is an associated contact for the caller, return the known call type (e.g.
193   * mobile, home, work). If there is no associated contact, attempt to use the caller's location if
194   * known.
195   *
196   * @param details Call details to use.
197   * @return Type of call (mobile/home) if known, or the location of the caller (if known).
198   */
199  public CharSequence getCallTypeOrLocation(PhoneCallDetails details) {
200    if (details.isSpam) {
201      return mResources.getString(R.string.spam_number_call_log_label);
202    } else if (details.isBlocked) {
203      return mResources.getString(R.string.blocked_number_call_log_label);
204    }
205
206    CharSequence numberFormattedLabel = null;
207    // Only show a label if the number is shown and it is not a SIP address.
208    if (!TextUtils.isEmpty(details.number)
209        && !PhoneNumberHelper.isUriNumber(details.number.toString())
210        && !mCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
211
212      if (shouldShowLocation(details)) {
213        numberFormattedLabel = details.geocode;
214      } else if (!(details.numberType == Phone.TYPE_CUSTOM
215          && TextUtils.isEmpty(details.numberLabel))) {
216        // Get type label only if it will not be "Custom" because of an empty number label.
217        numberFormattedLabel =
218            mPhoneTypeLabelForTest != null
219                ? mPhoneTypeLabelForTest
220                : Phone.getTypeLabel(mResources, details.numberType, details.numberLabel);
221      }
222    }
223
224    if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) {
225      numberFormattedLabel = details.displayNumber;
226    }
227    return numberFormattedLabel;
228  }
229
230  /** Returns true if primary name is empty or the data is from Cequint Caller ID. */
231  private static boolean shouldShowLocation(PhoneCallDetails details) {
232    if (TextUtils.isEmpty(details.geocode)) {
233      return false;
234    }
235    // For caller ID provided by Cequint we want to show the geo location.
236    if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
237      return true;
238    }
239    // Don't bother showing geo location for contacts.
240    if (!TextUtils.isEmpty(details.namePrimary)) {
241      return false;
242    }
243    return true;
244  }
245
246  public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) {
247    this.mPhoneTypeLabelForTest = phoneTypeLabel;
248  }
249
250  /**
251   * Get the call date/time of the call. For the call log this is relative to the current time. e.g.
252   * 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)}
253   *
254   * @param details Call details to use.
255   * @return String representing when the call occurred.
256   */
257  public CharSequence getCallDate(PhoneCallDetails details) {
258    if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) {
259      return getGranularDateTime(details);
260    }
261
262    return DateUtils.getRelativeTimeSpanString(
263        details.date,
264        getCurrentTimeMillis(),
265        DateUtils.MINUTE_IN_MILLIS,
266        DateUtils.FORMAT_ABBREV_RELATIVE);
267  }
268
269  /**
270   * Get the granular version of the call date/time of the call. The result is always in the form
271   * 'DATE at TIME'. The date value changes based on when the call was created.
272   *
273   * <p>If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is
274   * 'MMM dd, yyyy'
275   *
276   * <p>TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm'
277   *
278   * @param details Call details to use
279   * @return String representing when the call occurred
280   */
281  public CharSequence getGranularDateTime(PhoneCallDetails details) {
282    return mResources.getString(
283        R.string.voicemailCallLogDateTimeFormat,
284        getGranularDate(details.date),
285        DateUtils.formatDateTime(mContext, details.date, DateUtils.FORMAT_SHOW_TIME));
286  }
287
288  /**
289   * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)}
290   */
291  private String getGranularDate(long date) {
292    if (DateUtils.isToday(date)) {
293      return mResources.getString(R.string.voicemailCallLogToday);
294    }
295    return DateUtils.formatDateTime(
296        mContext,
297        date,
298        DateUtils.FORMAT_SHOW_DATE
299            | DateUtils.FORMAT_ABBREV_MONTH
300            | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
301  }
302
303  /**
304   * Determines whether the year should be shown for the given date
305   *
306   * @return {@code true} if date is within the current year, {@code false} otherwise
307   */
308  private boolean shouldShowYear(long date) {
309    mCalendar.setTimeInMillis(getCurrentTimeMillis());
310    int currentYear = mCalendar.get(Calendar.YEAR);
311    mCalendar.setTimeInMillis(date);
312    return currentYear != mCalendar.get(Calendar.YEAR);
313  }
314
315  /** Sets the text of the header view for the details page of a phone call. */
316  public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) {
317    final CharSequence nameText;
318    if (!TextUtils.isEmpty(details.namePrimary)) {
319      nameText = details.namePrimary;
320    } else if (!TextUtils.isEmpty(details.displayNumber)) {
321      nameText = details.displayNumber;
322    } else {
323      nameText = mResources.getString(R.string.unknown);
324    }
325
326    nameView.setText(nameText);
327  }
328
329  public void setCurrentTimeForTest(long currentTimeMillis) {
330    mCurrentTimeMillisForTest = currentTimeMillis;
331  }
332
333  /**
334   * Returns the current time in milliseconds since the epoch.
335   *
336   * <p>It can be injected in tests using {@link #setCurrentTimeForTest(long)}.
337   */
338  private long getCurrentTimeMillis() {
339    if (mCurrentTimeMillisForTest == null) {
340      return System.currentTimeMillis();
341    } else {
342      return mCurrentTimeMillisForTest;
343    }
344  }
345
346  /** Sets the call count, date, and if it is a voicemail, sets the duration. */
347  private void setDetailText(
348      PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) {
349    // Combine the count (if present) and the date.
350    CharSequence dateText = details.callLocationAndDate;
351    final CharSequence text;
352    if (callCount != null) {
353      text = mResources.getString(R.string.call_log_item_count_and_date, callCount, dateText);
354    } else {
355      text = dateText;
356    }
357
358    if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
359      views.callLocationAndDate.setText(
360          mResources.getString(
361              R.string.voicemailCallLogDateTimeFormatWithDuration,
362              text,
363              getVoicemailDuration(details)));
364    } else {
365      views.callLocationAndDate.setText(text);
366    }
367  }
368
369  private String getVoicemailDuration(PhoneCallDetails details) {
370    long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
371    long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
372    if (minutes > 99) {
373      minutes = 99;
374    }
375    return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds);
376  }
377}
378