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