1 /*
2  * Copyright (C) 2017 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.calllogutils;
18 
19 import android.content.Context;
20 import android.provider.CallLog.Calls;
21 import android.telephony.PhoneNumberUtils;
22 import android.text.TextUtils;
23 import com.android.dialer.calllog.model.CoalescedRow;
24 import com.android.dialer.duo.DuoComponent;
25 import com.android.dialer.spam.Spam;
26 import com.android.dialer.time.Clock;
27 import com.google.common.base.Optional;
28 import com.google.common.collect.Collections2;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 
33 /**
34  * Computes the primary text and secondary text for call log entries.
35  *
36  * <p>These text values are shown in the main call log list or in the top item of the bottom sheet
37  * menu.
38  */
39 public final class CallLogEntryText {
40 
41   /**
42    * The primary text for bottom sheets is the same as shown in the entry list.
43    *
44    * <p>(In the entry list, the number of calls and additional icons are displayed as images
45    * following the primary text.)
46    */
buildPrimaryText(Context context, CoalescedRow row)47   public static CharSequence buildPrimaryText(Context context, CoalescedRow row) {
48     // Calls to emergency services should be shown as "Emergency number".
49     if (row.getNumberAttributes().getIsEmergencyNumber()) {
50       return context.getText(R.string.emergency_number);
51     }
52 
53     // Otherwise, follow the following order of preferences.
54     // 1st preference: the presentation name, like "Restricted".
55     Optional<String> presentationName =
56         PhoneNumberDisplayUtil.getNameForPresentation(context, row.getNumberPresentation());
57     if (presentationName.isPresent()) {
58       return presentationName.get();
59     }
60 
61     // 2nd preference: the voicemail tag if the call is one made to a voicemail box.
62     if (row.getIsVoicemailCall() && !TextUtils.isEmpty(row.getVoicemailCallTag())) {
63       return row.getVoicemailCallTag();
64     }
65 
66     // 3rd preference: the name associated with the number.
67     if (!TextUtils.isEmpty(row.getNumberAttributes().getName())) {
68       return row.getNumberAttributes().getName();
69     }
70 
71     // 4th preference: the formatted number.
72     if (!TextUtils.isEmpty(row.getFormattedNumber())) {
73       return PhoneNumberUtils.createTtsSpannable(row.getFormattedNumber());
74     }
75 
76     // Last resort: show "Unknown".
77     return context.getText(R.string.new_call_log_unknown);
78   }
79 
80   /**
81    * The secondary text to be shown in the main call log entry list.
82    *
83    * <p>This method first obtains a list of strings to be shown in order and then concatenates them
84    * with " • ".
85    *
86    * <p>Examples:
87    *
88    * <ul>
89    *   <li>Mobile, Duo video • 10 min ago
90    *   <li>Spam • Mobile • Now
91    *   <li>Blocked • Spam • Mobile • Now
92    * </ul>
93    *
94    * @see #buildSecondaryTextListForEntries(Context, Clock, CoalescedRow, boolean) for details.
95    */
buildSecondaryTextForEntries( Context context, Clock clock, CoalescedRow row)96   public static CharSequence buildSecondaryTextForEntries(
97       Context context, Clock clock, CoalescedRow row) {
98     return joinSecondaryTextComponents(
99         buildSecondaryTextListForEntries(context, clock, row, /* abbreviateDateTime = */ true));
100   }
101 
102   /**
103    * Returns a list of strings to be shown in order as the main call log entry's secondary text.
104    *
105    * <p>Rules:
106    *
107    * <ul>
108    *   <li>An emergency number: [{Date}]
109    *   <li>Number - not blocked, call - not spam:
110    *       <p>[{$Label(, Duo video|Carrier video)?|$Location}, {Date}]
111    *   <li>Number - blocked, call - not spam:
112    *       <p>["Blocked", {$Label(, Duo video|Carrier video)?|$Location}, {Date}]
113    *   <li>Number - not blocked, call - spam:
114    *       <p>["Spam", {$Label(, Duo video|Carrier video)?}, {Date}]
115    *   <li>Number - blocked, call - spam:
116    *       <p>["Blocked, Spam", {$Label(, Duo video|Carrier video)?}, {Date}]
117    * </ul>
118    *
119    * <p>Examples:
120    *
121    * <ul>
122    *   <li>["Mobile, Duo video", "Now"]
123    *   <li>["Duo video", "10 min ago"]
124    *   <li>["Mobile", "11:45 PM"]
125    *   <li>["Mobile", "Sun"]
126    *   <li>["Blocked", "Mobile, Duo video", "Now"]
127    *   <li>["Blocked", "Brooklyn, NJ", "10 min ago"]
128    *   <li>["Spam", "Mobile", "Now"]
129    *   <li>["Spam", "Now"]
130    *   <li>["Blocked", "Spam", "Mobile", "Now"]
131    *   <li>["Brooklyn, NJ", "Jan 15"]
132    * </ul>
133    *
134    * <p>See {@link CallLogDates#newCallLogTimestampLabel(Context, long, long, boolean)} for date
135    * rules.
136    */
buildSecondaryTextListForEntries( Context context, Clock clock, CoalescedRow row, boolean abbreviateDateTime)137   static List<CharSequence> buildSecondaryTextListForEntries(
138       Context context, Clock clock, CoalescedRow row, boolean abbreviateDateTime) {
139     // For emergency numbers, the secondary text should contain only the timestamp.
140     if (row.getNumberAttributes().getIsEmergencyNumber()) {
141       return Collections.singletonList(
142           CallLogDates.newCallLogTimestampLabel(
143               context, clock.currentTimeMillis(), row.getTimestamp(), abbreviateDateTime));
144     }
145 
146     List<CharSequence> components = new ArrayList<>();
147 
148     if (row.getNumberAttributes().getIsBlocked()) {
149       components.add(context.getText(R.string.new_call_log_secondary_blocked));
150     }
151     if (Spam.shouldShowAsSpam(row.getNumberAttributes().getIsSpam(), row.getCallType())) {
152       components.add(context.getText(R.string.new_call_log_secondary_spam));
153     }
154 
155     components.add(getNumberTypeLabel(context, row));
156 
157     components.add(
158         CallLogDates.newCallLogTimestampLabel(
159             context, clock.currentTimeMillis(), row.getTimestamp(), abbreviateDateTime));
160     return components;
161   }
162 
163   /**
164    * The secondary text to show in the top item of the bottom sheet.
165    *
166    * <p>This is basically the same as {@link #buildSecondaryTextForEntries(Context, Clock,
167    * CoalescedRow)} except that instead of suffixing with the time of the call, we suffix with the
168    * formatted number.
169    */
buildSecondaryTextForBottomSheet(Context context, CoalescedRow row)170   public static CharSequence buildSecondaryTextForBottomSheet(Context context, CoalescedRow row) {
171     /*
172      * Rules:
173      *   For an emergency number:
174      *     Number
175      *   Number - not blocked, call - not spam:
176      *     $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]?
177      *   Number - blocked, call - not spam:
178      *     Blocked • $Label(, Duo video|Carrier video)?|$Location [• NumberIfNoName]?
179      *   Number - not blocked, call - spam:
180      *     Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]?
181      *   Number - blocked, call - spam:
182      *     Blocked • Spam • $Label(, Duo video|Carrier video)? [• NumberIfNoName]?
183      *
184      * The number is shown at the end if there is no name for the entry. (It is shown in primary
185      * text otherwise.)
186      *
187      * Examples:
188      *   Mobile, Duo video • 555-1234
189      *   Duo video • 555-1234
190      *   Mobile • 555-1234
191      *   Blocked • Mobile • 555-1234
192      *   Blocked • Brooklyn, NJ • 555-1234
193      *   Spam • Mobile • 555-1234
194      *   Mobile • 555-1234
195      *   Brooklyn, NJ
196      */
197 
198     // For emergency numbers, the secondary text should contain only the number.
199     if (row.getNumberAttributes().getIsEmergencyNumber()) {
200       return !row.getFormattedNumber().isEmpty()
201           ? row.getFormattedNumber()
202           : row.getNumber().getNormalizedNumber();
203     }
204 
205     List<CharSequence> components = new ArrayList<>();
206 
207     if (row.getNumberAttributes().getIsBlocked()) {
208       components.add(context.getText(R.string.new_call_log_secondary_blocked));
209     }
210     if (Spam.shouldShowAsSpam(row.getNumberAttributes().getIsSpam(), row.getCallType())) {
211       components.add(context.getText(R.string.new_call_log_secondary_spam));
212     }
213 
214     components.add(getNumberTypeLabel(context, row));
215 
216     // If there's a presentation name, we showed it in the primary text and shouldn't show any name
217     // or number here.
218     Optional<String> presentationName =
219         PhoneNumberDisplayUtil.getNameForPresentation(context, row.getNumberPresentation());
220     if (presentationName.isPresent()) {
221       return joinSecondaryTextComponents(components);
222     }
223 
224     if (TextUtils.isEmpty(row.getNumberAttributes().getName())) {
225       // If the name is empty the number is shown as the primary text and there's nothing to add.
226       return joinSecondaryTextComponents(components);
227     }
228     if (TextUtils.isEmpty(row.getFormattedNumber())) {
229       // If there's no number, don't append anything.
230       return joinSecondaryTextComponents(components);
231     }
232     components.add(row.getFormattedNumber());
233     return joinSecondaryTextComponents(components);
234   }
235 
236   /**
237    * Returns a value such as "Mobile, Duo video" without the time of the call or formatted number
238    * appended.
239    *
240    * <p>When the secondary text is shown in call log entry list, this prefix is suffixed with the
241    * time of the call, and when it is shown in a bottom sheet, it is suffixed with the formatted
242    * number.
243    */
getNumberTypeLabel(Context context, CoalescedRow row)244   private static CharSequence getNumberTypeLabel(Context context, CoalescedRow row) {
245     StringBuilder secondaryText = new StringBuilder();
246 
247     // The number type label comes first (e.g., "Mobile", "Work", "Home", etc).
248     String numberTypeLabel = row.getNumberAttributes().getNumberTypeLabel();
249     secondaryText.append(numberTypeLabel);
250 
251     // Add video call info if applicable.
252     if ((row.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
253       if (secondaryText.length() > 0) {
254         secondaryText.append(", ");
255       }
256 
257       boolean isDuoCall =
258           DuoComponent.get(context).getDuo().isDuoAccount(row.getPhoneAccountComponentName());
259       secondaryText.append(
260           context.getText(
261               isDuoCall ? R.string.new_call_log_duo_video : R.string.new_call_log_carrier_video));
262     }
263 
264     // Show the location if
265     // (1) there is no number type label, and
266     // (2) the call should not be shown as spam.
267     if (TextUtils.isEmpty(numberTypeLabel)
268         && !Spam.shouldShowAsSpam(row.getNumberAttributes().getIsSpam(), row.getCallType())) {
269       // If number attributes contain a location (obtained from a PhoneLookup), use it instead
270       // of the one from the annotated call log.
271       String location =
272           !TextUtils.isEmpty(row.getNumberAttributes().getGeolocation())
273               ? row.getNumberAttributes().getGeolocation()
274               : row.getGeocodedLocation();
275       if (!TextUtils.isEmpty(location)) {
276         if (secondaryText.length() > 0) {
277           secondaryText.append(", ");
278         }
279         secondaryText.append(location);
280       }
281     }
282 
283     return secondaryText;
284   }
285 
joinSecondaryTextComponents(List<CharSequence> components)286   private static CharSequence joinSecondaryTextComponents(List<CharSequence> components) {
287     return TextUtils.join(
288         " • ", Collections2.filter(components, (text) -> !TextUtils.isEmpty(text)));
289   }
290 }
291