1package com.android.incallui;
2
3import android.content.Context;
4import android.content.Loader;
5import android.content.Loader.OnLoadCompleteListener;
6import android.net.Uri;
7import android.telecom.PhoneAccount;
8import android.telecom.TelecomManager;
9import android.text.TextUtils;
10import android.util.Log;
11
12import com.android.contacts.common.model.Contact;
13import com.android.contacts.common.model.ContactLoader;
14import com.android.dialer.R;
15import com.android.dialer.calllog.ContactInfo;
16import com.android.dialer.service.CachedNumberLookupService;
17import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
18import com.android.dialer.util.TelecomUtil;
19
20import java.util.Arrays;
21
22/**
23 * Utility methods for contact and caller info related functionality
24 */
25public 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
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     */
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
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     */
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
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     */
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
180    private static boolean isCnapSpecialCaseRestricted(String n) {
181        return n.equals("PRIVATE") || n.equals("P") || n.equals("RES");
182    }
183
184    private static boolean isCnapSpecialCaseUnknown(String n) {
185        return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
186    }
187
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     */
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