packages/apps/Dialer/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java

615 lines
24 KiB
Java

/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.app.calllog;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v4.content.ContextCompat;
import android.support.v4.os.BuildCompat;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
import com.android.dialer.calllogutils.PhoneCallDetails;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.android.provider.VoicemailCompat;
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
import com.android.dialer.logging.ContactSource;
import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.phonenumbercache.CachedNumberLookupService;
import com.android.dialer.phonenumbercache.PhoneNumberCache;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.spannable.ContentWithLearnMoreSpanner;
import com.android.dialer.storage.StorageComponent;
import com.android.dialer.theme.base.ThemeComponent;
import com.android.dialer.util.DialerUtils;
import com.android.voicemail.VoicemailClient;
import com.android.voicemail.VoicemailComponent;
import com.android.voicemail.impl.transcribe.TranscriptionRatingHelper;
import com.google.internal.communications.voicemailtranscription.v1.TranscriptionRatingValue;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
/** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */
public class PhoneCallDetailsHelper
implements TranscriptionRatingHelper.SuccessListener,
TranscriptionRatingHelper.FailureListener {
/** The maximum number of icons will be shown to represent the call types in a group. */
private static final int MAX_CALL_TYPE_ICONS = 3;
private static final String PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY =
"pref_voicemail_donation_promo_shown_key";
private final Context context;
private final Resources resources;
private final CallLogCache callLogCache;
/** Calendar used to construct dates */
private final Calendar calendar;
private final CachedNumberLookupService cachedNumberLookupService;
/** The injected current time in milliseconds since the epoch. Used only by tests. */
private Long currentTimeMillisForTest;
private CharSequence phoneTypeLabelForTest;
/** List of items to be concatenated together for accessibility descriptions */
private ArrayList<CharSequence> descriptionItems = new ArrayList<>();
/**
* Creates a new instance of the helper.
*
* <p>Generally you should have a single instance of this helper in any context.
*
* @param resources used to look up strings
*/
public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) {
this.context = context;
this.resources = resources;
this.callLogCache = callLogCache;
calendar = Calendar.getInstance();
cachedNumberLookupService = PhoneNumberCache.get(context).getCachedNumberLookupService();
}
static boolean shouldShowVoicemailDonationPromo(
Context context, PhoneAccountHandle accountHandle) {
VoicemailClient client = VoicemailComponent.get(context).getVoicemailClient();
return client.isVoicemailDonationAvailable(context, accountHandle)
&& !hasSeenVoicemailDonationPromo(context);
}
static boolean hasSeenVoicemailDonationPromo(Context context) {
return StorageComponent.get(context.getApplicationContext())
.unencryptedSharedPrefs()
.getBoolean(PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY, false);
}
private static int dpsToPixels(Context context, int dps) {
return (int)
(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dps, context.getResources().getDisplayMetrics()));
}
private static void recordPromoShown(Context context) {
StorageComponent.get(context.getApplicationContext())
.unencryptedSharedPrefs()
.edit()
.putBoolean(PREF_VOICEMAIL_DONATION_PROMO_SHOWN_KEY, true)
.apply();
}
/** Returns true if primary name is empty or the data is from Cequint Caller ID. */
private boolean shouldShowLocation(PhoneCallDetails details) {
if (TextUtils.isEmpty(details.geocode)) {
return false;
}
// For caller ID provided by Cequint we want to show the geo location.
if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
return true;
}
if (cachedNumberLookupService != null
&& cachedNumberLookupService.isBusiness(details.sourceType)) {
return true;
}
// Don't bother showing geo location for contacts.
if (!TextUtils.isEmpty(details.namePrimary)) {
return false;
}
return true;
}
/** Fills the call details views with content. */
public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) {
// Display up to a given number of icons.
views.callTypeIcons.clear();
int count = details.callTypes.length;
boolean isVoicemail = false;
for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) {
views.callTypeIcons.add(details.callTypes[index]);
if (index == 0) {
isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE;
}
}
// Show the video icon if the call had video enabled.
views.callTypeIcons.setShowVideo(
(details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO);
views.callTypeIcons.setShowHd(
(details.features & Calls.FEATURES_HD_CALL) == Calls.FEATURES_HD_CALL);
views.callTypeIcons.setShowWifi(
MotorolaUtils.shouldShowWifiIconInCallLog(context, details.features));
views.callTypeIcons.setShowAssistedDialed(
(details.features & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
== TelephonyManagerCompat.FEATURES_ASSISTED_DIALING);
if (BuildCompat.isAtLeastP()) {
views.callTypeIcons.setShowRtt((details.features & Calls.FEATURES_RTT) == Calls.FEATURES_RTT);
}
views.callTypeIcons.requestLayout();
views.callTypeIcons.setVisibility(View.VISIBLE);
// Show the total call count only if there are more than the maximum number of icons.
final Integer callCount;
if (count > MAX_CALL_TYPE_ICONS) {
callCount = count;
} else {
callCount = null;
}
// Set the call count, location, date and if voicemail, set the duration.
setDetailText(views, callCount, details);
// Set the account label if it exists.
String accountLabel = callLogCache.getAccountLabel(details.accountHandle);
if (!TextUtils.isEmpty(details.viaNumber)) {
if (!TextUtils.isEmpty(accountLabel)) {
accountLabel =
resources.getString(
R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber);
} else {
accountLabel = resources.getString(R.string.call_log_via_number, details.viaNumber);
}
}
if (!TextUtils.isEmpty(accountLabel)) {
views.callAccountLabel.setVisibility(View.VISIBLE);
views.callAccountLabel.setText(accountLabel);
int color = callLogCache.getAccountColor(details.accountHandle);
if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
int defaultColor = R.color.dialer_secondary_text_color;
views.callAccountLabel.setTextColor(context.getResources().getColor(defaultColor));
} else {
views.callAccountLabel.setTextColor(color);
}
} else {
views.callAccountLabel.setVisibility(View.GONE);
}
setNameView(views, details);
if (isVoicemail) {
int relevantLinkTypes = Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS;
views.voicemailTranscriptionView.setAutoLinkMask(relevantLinkTypes);
String transcript = "";
String branding = "";
if (!TextUtils.isEmpty(details.transcription)) {
transcript = details.transcription;
if (details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE
|| details.transcriptionState == VoicemailCompat.TRANSCRIPTION_AVAILABLE_AND_RATED) {
branding = resources.getString(R.string.voicemail_transcription_branding_text);
}
} else {
switch (details.transcriptionState) {
case VoicemailCompat.TRANSCRIPTION_IN_PROGRESS:
branding = resources.getString(R.string.voicemail_transcription_in_progress);
break;
case VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED:
branding = resources.getString(R.string.voicemail_transcription_failed_no_speech);
break;
case VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED:
branding =
resources.getString(R.string.voicemail_transcription_failed_language_not_supported);
break;
case VoicemailCompat.TRANSCRIPTION_FAILED:
branding = resources.getString(R.string.voicemail_transcription_failed);
break;
default:
break; // Fall through
}
}
views.voicemailTranscriptionView.setText(transcript);
views.voicemailTranscriptionBrandingView.setText(branding);
View ratingView = views.voicemailTranscriptionRatingView;
if (shouldShowTranscriptionRating(details.transcriptionState, details.accountHandle)) {
ratingView.setVisibility(View.VISIBLE);
ratingView
.findViewById(R.id.voicemail_transcription_rating_good)
.setOnClickListener(
view ->
recordTranscriptionRating(
TranscriptionRatingValue.GOOD_TRANSCRIPTION, details, ratingView));
ratingView
.findViewById(R.id.voicemail_transcription_rating_bad)
.setOnClickListener(
view ->
recordTranscriptionRating(
TranscriptionRatingValue.BAD_TRANSCRIPTION, details, ratingView));
} else {
ratingView.setVisibility(View.GONE);
}
}
// Bold if not read
Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD;
views.nameView.setTypeface(typeface);
views.voicemailTranscriptionView.setTypeface(typeface);
views.voicemailTranscriptionBrandingView.setTypeface(typeface);
views.callLocationAndDate.setTypeface(typeface);
views.callLocationAndDate.setTextColor(
details.isRead
? ThemeComponent.get(context).theme().getTextColorSecondary()
: ThemeComponent.get(context).theme().getTextColorPrimary());
}
private void setNameView(PhoneCallDetailsViews views, PhoneCallDetails details) {
if (!TextUtils.isEmpty(details.getPreferredName())) {
views.nameView.setText(details.getPreferredName());
// "nameView" is updated from phone number to contact name after number matching.
// Since TextDirection remains at View.TEXT_DIRECTION_LTR, initialize it.
views.nameView.setTextDirection(View.TEXT_DIRECTION_INHERIT);
return;
}
if (PhoneNumberUtils.isEmergencyNumber(details.displayNumber)) {
views.nameView.setText(R.string.emergency_number);
views.nameView.setTextDirection(View.TEXT_DIRECTION_INHERIT);
return;
}
views.nameView.setText(details.displayNumber);
// We have a real phone number as "nameView" so make it always LTR
views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR);
}
private boolean shouldShowTranscriptionRating(
int transcriptionState, PhoneAccountHandle account) {
if (transcriptionState != VoicemailCompat.TRANSCRIPTION_AVAILABLE) {
return false;
}
VoicemailClient client = VoicemailComponent.get(context).getVoicemailClient();
if (client.isVoicemailDonationEnabled(context, account)) {
return true;
}
// Also show the rating option if voicemail donation is available (but not enabled)
// and the donation promo has not yet been shown.
if (client.isVoicemailDonationAvailable(context, account)
&& !hasSeenVoicemailDonationPromo(context)) {
return true;
}
return false;
}
private void recordTranscriptionRating(
TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView) {
LogUtil.enterBlock("PhoneCallDetailsHelper.recordTranscriptionRating");
if (shouldShowVoicemailDonationPromo(context, details.accountHandle)) {
showVoicemailDonationPromo(ratingValue, details, ratingView);
} else {
TranscriptionRatingHelper.sendRating(
context,
ratingValue,
Uri.parse(details.voicemailUri),
this::onRatingSuccess,
this::onRatingFailure);
}
}
private void showVoicemailDonationPromo(
TranscriptionRatingValue ratingValue, PhoneCallDetails details, View ratingView) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(getVoicemailDonationPromoContent());
builder.setPositiveButton(
R.string.voicemail_donation_promo_opt_in,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int button) {
LogUtil.i("PhoneCallDetailsHelper.showVoicemailDonationPromo", "onClick");
dialog.cancel();
recordPromoShown(context);
VoicemailComponent.get(context)
.getVoicemailClient()
.setVoicemailDonationEnabled(context, details.accountHandle, true);
TranscriptionRatingHelper.sendRating(
context,
ratingValue,
Uri.parse(details.voicemailUri),
PhoneCallDetailsHelper.this::onRatingSuccess,
PhoneCallDetailsHelper.this::onRatingFailure);
ratingView.setVisibility(View.GONE);
}
});
builder.setNegativeButton(
R.string.voicemail_donation_promo_opt_out,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int button) {
VoicemailComponent.get(context)
.getVoicemailClient()
.setVoicemailDonationEnabled(context, details.accountHandle, false);
dialog.cancel();
recordPromoShown(context);
ratingView.setVisibility(View.GONE);
}
});
builder.setCancelable(true);
AlertDialog dialog = builder.create();
TextView title = new TextView(context);
title.setText(R.string.voicemail_donation_promo_title);
title.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
title.setTextColor(ContextCompat.getColor(context, R.color.dialer_primary_text_color));
title.setPadding(
dpsToPixels(context, 24), /* left */
dpsToPixels(context, 10), /* top */
dpsToPixels(context, 24), /* right */
dpsToPixels(context, 0)); /* bottom */
dialog.setCustomTitle(title);
dialog.show();
// Make the message link clickable and adjust the appearance of the message and buttons
TextView textView = (TextView) dialog.findViewById(android.R.id.message);
textView.setLineSpacing(0, 1.2f);
textView.setMovementMethod(LinkMovementMethod.getInstance());
Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (positiveButton != null) {
positiveButton.setTextColor(ThemeComponent.get(context).theme().getColorPrimary());
}
Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
if (negativeButton != null) {
negativeButton.setTextColor(ThemeComponent.get(context).theme().getTextColorSecondary());
}
}
private SpannableString getVoicemailDonationPromoContent() {
return new ContentWithLearnMoreSpanner(context)
.create(
context.getString(R.string.voicemail_donation_promo_content),
context.getString(R.string.voicemail_donation_promo_learn_more_url));
}
@Override
public void onRatingSuccess(Uri voicemailUri) {
LogUtil.enterBlock("PhoneCallDetailsHelper.onRatingSuccess");
Toast toast =
Toast.makeText(context, R.string.voicemail_transcription_rating_thanks, Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 50);
toast.show();
}
@Override
public void onRatingFailure(Throwable t) {
LogUtil.e("PhoneCallDetailsHelper.onRatingFailure", "failed to send rating", t);
}
/**
* Builds a string containing the call location and date. For voicemail logs only the call date is
* returned because location information is displayed in the call action button
*
* @param details The call details.
* @return The call location and date string.
*/
public CharSequence getCallLocationAndDate(PhoneCallDetails details) {
descriptionItems.clear();
if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) {
// Get type of call (ie mobile, home, etc) if known, or the caller's location.
CharSequence callTypeOrLocation = getCallTypeOrLocation(details);
// Only add the call type or location if its not empty. It will be empty for unknown
// callers.
if (!TextUtils.isEmpty(callTypeOrLocation)) {
descriptionItems.add(callTypeOrLocation);
}
}
// The date of this call
descriptionItems.add(getCallDate(details));
// Create a comma separated list from the call type or location, and call date.
return DialerUtils.join(descriptionItems);
}
/**
* For a call, if there is an associated contact for the caller, return the known call type (e.g.
* mobile, home, work). If there is no associated contact, attempt to use the caller's location if
* known.
*
* @param details Call details to use.
* @return Type of call (mobile/home) if known, or the location of the caller (if known).
*/
public CharSequence getCallTypeOrLocation(PhoneCallDetails details) {
if (details.isSpam) {
return resources.getString(R.string.spam_number_call_log_label);
} else if (details.isBlocked) {
return resources.getString(R.string.blocked_number_call_log_label);
}
CharSequence numberFormattedLabel = null;
// Only show a label if the number is shown and it is not a SIP address.
if (!TextUtils.isEmpty(details.number)
&& !PhoneNumberHelper.isUriNumber(details.number.toString())
&& !callLogCache.isVoicemailNumber(details.accountHandle, details.number)) {
if (shouldShowLocation(details)) {
numberFormattedLabel = details.geocode;
} else if (!(details.numberType == Phone.TYPE_CUSTOM
&& TextUtils.isEmpty(details.numberLabel))) {
// Get type label only if it will not be "Custom" because of an empty number label.
numberFormattedLabel =
phoneTypeLabelForTest != null
? phoneTypeLabelForTest
: Phone.getTypeLabel(resources, details.numberType, details.numberLabel);
}
}
if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) {
numberFormattedLabel = details.displayNumber;
}
return numberFormattedLabel;
}
public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) {
this.phoneTypeLabelForTest = phoneTypeLabel;
}
/**
* Get the call date/time of the call. For the call log this is relative to the current time. e.g.
* 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)}
*
* @param details Call details to use.
* @return String representing when the call occurred.
*/
public CharSequence getCallDate(PhoneCallDetails details) {
if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) {
return getGranularDateTime(details);
}
return DateUtils.getRelativeTimeSpanString(
details.date,
getCurrentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE);
}
/**
* Get the granular version of the call date/time of the call. The result is always in the form
* 'DATE at TIME'. The date value changes based on when the call was created.
*
* <p>If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is
* 'MMM dd, yyyy'
*
* <p>TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm'
*
* @param details Call details to use
* @return String representing when the call occurred
*/
public CharSequence getGranularDateTime(PhoneCallDetails details) {
return resources.getString(
R.string.voicemailCallLogDateTimeFormat,
getGranularDate(details.date),
DateUtils.formatDateTime(context, details.date, DateUtils.FORMAT_SHOW_TIME));
}
/**
* Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)}
*/
private String getGranularDate(long date) {
if (DateUtils.isToday(date)) {
return resources.getString(R.string.voicemailCallLogToday);
}
return DateUtils.formatDateTime(
context,
date,
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_ABBREV_MONTH
| (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR));
}
/**
* Determines whether the year should be shown for the given date
*
* @return {@code true} if date is within the current year, {@code false} otherwise
*/
private boolean shouldShowYear(long date) {
calendar.setTimeInMillis(getCurrentTimeMillis());
int currentYear = calendar.get(Calendar.YEAR);
calendar.setTimeInMillis(date);
return currentYear != calendar.get(Calendar.YEAR);
}
/**
* Returns the current time in milliseconds since the epoch.
*
* <p>It can be injected in tests using {@link #setCurrentTimeForTest(long)}.
*/
private long getCurrentTimeMillis() {
if (currentTimeMillisForTest == null) {
return System.currentTimeMillis();
} else {
return currentTimeMillisForTest;
}
}
/** Sets the call count, date, and if it is a voicemail, sets the duration. */
private void setDetailText(
PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) {
// Combine the count (if present) and the date.
CharSequence dateText = details.callLocationAndDate;
final CharSequence text;
if (callCount != null) {
text = resources.getString(R.string.call_log_item_count_and_date, callCount, dateText);
} else {
text = dateText;
}
if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
views.callLocationAndDate.setText(
resources.getString(
R.string.voicemailCallLogDateTimeFormatWithDuration,
text,
getVoicemailDuration(details)));
} else {
views.callLocationAndDate.setText(text);
}
}
private String getVoicemailDuration(PhoneCallDetails details) {
long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
if (minutes > 99) {
minutes = 99;
}
return resources.getString(R.string.voicemailDurationFormat, minutes, seconds);
}
}