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