1 package com.android.incallui;
2 
3 import android.content.Context;
4 import android.content.Loader;
5 import android.content.Loader.OnLoadCompleteListener;
6 import android.net.Uri;
7 import android.telecom.PhoneAccount;
8 import android.telecom.TelecomManager;
9 import android.text.TextUtils;
10 import android.util.Log;
11 
12 import com.android.contacts.common.model.Contact;
13 import com.android.contacts.common.model.ContactLoader;
14 import com.android.dialer.R;
15 import com.android.dialer.calllog.ContactInfo;
16 import com.android.dialer.service.CachedNumberLookupService;
17 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
18 import com.android.dialer.util.TelecomUtil;
19 
20 import java.util.Arrays;
21 
22 /**
23  * Utility methods for contact and caller info related functionality
24  */
25 public class CallerInfoUtils {
26 
27     private static final String TAG = CallerInfoUtils.class.getSimpleName();
28 
29     /** Define for not a special CNAP string */
30     private static final int CNAP_SPECIAL_CASE_NO = -1;
31 
CallerInfoUtils()32     public CallerInfoUtils() {
33     }
34 
35     private static final int QUERY_TOKEN = -1;
36 
37     /**
38      * This is called to get caller info for a call. This will return a CallerInfo
39      * object immediately based off information in the call, but
40      * more information is returned to the OnQueryCompleteListener (which contains
41      * information about the phone number label, user's name, etc).
42      */
getCallerInfoForCall(Context context, Call call, CallerInfoAsyncQuery.OnQueryCompleteListener listener)43     public static CallerInfo getCallerInfoForCall(Context context, Call call,
44             CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
45         CallerInfo info = buildCallerInfo(context, call);
46 
47         // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
48 
49         if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
50             // Start the query with the number provided from the call.
51             Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
52             CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call);
53         }
54         return info;
55     }
56 
buildCallerInfo(Context context, Call call)57     public static CallerInfo buildCallerInfo(Context context, Call call) {
58         CallerInfo info = new CallerInfo();
59 
60         // Store CNAP information retrieved from the Connection (we want to do this
61         // here regardless of whether the number is empty or not).
62         info.cnapName = call.getCnapName();
63         info.name = info.cnapName;
64         info.numberPresentation = call.getNumberPresentation();
65         info.namePresentation = call.getCnapNamePresentation();
66         info.callSubject = call.getCallSubject();
67 
68         String number = call.getNumber();
69         if (!TextUtils.isEmpty(number)) {
70             final String[] numbers = number.split("&");
71             number = numbers[0];
72             if (numbers.length > 1) {
73                 info.forwardingNumber = numbers[1];
74             }
75 
76             number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
77             info.phoneNumber = number;
78         }
79 
80         // Because the InCallUI is immediately launched before the call is connected, occasionally
81         // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
82         // This call should still be handled as a voicemail call.
83         if ((call.getHandle() != null &&
84                 PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) ||
85                 isVoiceMailNumber(context, call)) {
86             info.markAsVoiceMail(context);
87         }
88 
89         ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call,
90                 info);
91 
92         return info;
93     }
94 
95     /**
96      * Creates a new {@link CachedContactInfo} from a {@link CallerInfo}
97      *
98      * @param lookupService the {@link CachedNumberLookupService} used to build a
99      *        new {@link CachedContactInfo}
100      * @param {@link CallerInfo} object
101      * @return a CachedContactInfo object created from this CallerInfo
102      * @throws NullPointerException if lookupService or ci are null
103      */
buildCachedContactInfo(CachedNumberLookupService lookupService, CallerInfo ci)104     public static CachedContactInfo buildCachedContactInfo(CachedNumberLookupService lookupService,
105             CallerInfo ci) {
106         ContactInfo info = new ContactInfo();
107         info.name = ci.name;
108         info.type = ci.numberType;
109         info.label = ci.phoneLabel;
110         info.number = ci.phoneNumber;
111         info.normalizedNumber = ci.normalizedNumber;
112         info.photoUri = ci.contactDisplayPhotoUri;
113         info.userType = ci.userType;
114 
115         CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info);
116         cacheInfo.setLookupKey(ci.lookupKeyOrNull);
117         return cacheInfo;
118     }
119 
isVoiceMailNumber(Context context, Call call)120     public static boolean isVoiceMailNumber(Context context, Call call) {
121          return TelecomUtil.isVoicemailNumber(context,
122                  call.getTelecomCall().getDetails().getAccountHandle(),
123                  call.getNumber());
124     }
125 
126     /**
127      * Handles certain "corner cases" for CNAP. When we receive weird phone numbers
128      * from the network to indicate different number presentations, convert them to
129      * expected number and presentation values within the CallerInfo object.
130      * @param number number we use to verify if we are in a corner case
131      * @param presentation presentation value used to verify if we are in a corner case
132      * @return the new String that should be used for the phone number
133      */
modifyForSpecialCnapCases(Context context, CallerInfo ci, String number, int presentation)134     /* package */static String modifyForSpecialCnapCases(Context context, CallerInfo ci,
135             String number, int presentation) {
136         // Obviously we return number if ci == null, but still return number if
137         // number == null, because in these cases the correct string will still be
138         // displayed/logged after this function returns based on the presentation value.
139         if (ci == null || number == null) return number;
140 
141         Log.d(TAG, "modifyForSpecialCnapCases: initially, number="
142                 + toLogSafePhoneNumber(number)
143                 + ", presentation=" + presentation + " ci " + ci);
144 
145         // "ABSENT NUMBER" is a possible value we could get from the network as the
146         // phone number, so if this happens, change it to "Unknown" in the CallerInfo
147         // and fix the presentation to be the same.
148         final String[] absentNumberValues =
149                 context.getResources().getStringArray(R.array.absent_num);
150         if (Arrays.asList(absentNumberValues).contains(number)
151                 && presentation == TelecomManager.PRESENTATION_ALLOWED) {
152             number = context.getString(R.string.unknown);
153             ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
154         }
155 
156         // Check for other special "corner cases" for CNAP and fix them similarly. Corner
157         // cases only apply if we received an allowed presentation from the network, so check
158         // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
159         // match the presentation passed in for verification (meaning we changed it previously
160         // because it's a corner case and we're being called from a different entry point).
161         if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED
162                 || (ci.numberPresentation != presentation
163                         && presentation == TelecomManager.PRESENTATION_ALLOWED)) {
164             // For all special strings, change number & numberPrentation.
165             if (isCnapSpecialCaseRestricted(number)) {
166                 number = context.getString(R.string.private_num);
167                 ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED;
168             } else if (isCnapSpecialCaseUnknown(number)) {
169                 number = context.getString(R.string.unknown);
170                 ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
171             }
172             Log.d(TAG, "SpecialCnap: number=" + toLogSafePhoneNumber(number)
173                     + "; presentation now=" + ci.numberPresentation);
174         }
175         Log.d(TAG, "modifyForSpecialCnapCases: returning number string="
176                 + toLogSafePhoneNumber(number));
177         return number;
178     }
179 
isCnapSpecialCaseRestricted(String n)180     private static boolean isCnapSpecialCaseRestricted(String n) {
181         return n.equals("PRIVATE") || n.equals("P") || n.equals("RES");
182     }
183 
isCnapSpecialCaseUnknown(String n)184     private static boolean isCnapSpecialCaseUnknown(String n) {
185         return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
186     }
187 
toLogSafePhoneNumber(String number)188     /* package */static String toLogSafePhoneNumber(String number) {
189         // For unknown number, log empty string.
190         if (number == null) {
191             return "";
192         }
193 
194         // Todo: Figure out an equivalent for VDBG
195         if (false) {
196             // When VDBG is true we emit PII.
197             return number;
198         }
199 
200         // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
201         // sanitized phone numbers.
202         StringBuilder builder = new StringBuilder();
203         for (int i = 0; i < number.length(); i++) {
204             char c = number.charAt(i);
205             if (c == '-' || c == '@' || c == '.' || c == '&') {
206                 builder.append(c);
207             } else {
208                 builder.append('x');
209             }
210         }
211         return builder.toString();
212     }
213 
214     /**
215      * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are
216      * viewing a particular contact, so that it can download the high-res photo.
217      */
sendViewNotification(Context context, Uri contactUri)218     public static void sendViewNotification(Context context, Uri contactUri) {
219         final ContactLoader loader = new ContactLoader(context, contactUri,
220                 true /* postViewNotification */);
221         loader.registerListener(0, new OnLoadCompleteListener<Contact>() {
222             @Override
223             public void onLoadComplete(
224                     Loader<Contact> loader, Contact contact) {
225                 try {
226                     loader.reset();
227                 } catch (RuntimeException e) {
228                     Log.e(TAG, "Error resetting loader", e);
229                 }
230             }
231         });
232         loader.startLoading();
233     }
234 }
235