17ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Leepackage com.android.incallui;
27ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
37ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Leeimport android.content.Context;
4bf41b978dcb99db47247db6c56821d8090131f6eYorke Leeimport android.content.Loader;
5bf41b978dcb99db47247db6c56821d8090131f6eYorke Leeimport android.content.Loader.OnLoadCompleteListener;
6fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Leeimport android.net.Uri;
76cddf46812634fadc194830774110780f14e9462Tyler Gunnimport android.telecom.PhoneAccount;
86cddf46812634fadc194830774110780f14e9462Tyler Gunnimport android.telecom.TelecomManager;
97ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Leeimport android.text.TextUtils;
10bf41b978dcb99db47247db6c56821d8090131f6eYorke Leeimport android.util.Log;
11bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee
12bf41b978dcb99db47247db6c56821d8090131f6eYorke Leeimport com.android.contacts.common.model.Contact;
13bf41b978dcb99db47247db6c56821d8090131f6eYorke Leeimport com.android.contacts.common.model.ContactLoader;
147ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
157ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Leeimport java.util.Arrays;
167ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
177ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee/**
18fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee * Utility methods for contact and caller info related functionality
197ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee */
207ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Leepublic class CallerInfoUtils {
217ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
227ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    private static final String TAG = CallerInfoUtils.class.getSimpleName();
237ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
247ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    /** Define for not a special CNAP string */
257ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    private static final int CNAP_SPECIAL_CASE_NO = -1;
267ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
277ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    public CallerInfoUtils() {
287ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    }
297ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
307ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    private static final int QUERY_TOKEN = -1;
317ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
327ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    /**
334bbe4c6900dfe3584f674161dbe15e248d5adbe7Santos Cordon     * This is called to get caller info for a call. This will return a CallerInfo
344bbe4c6900dfe3584f674161dbe15e248d5adbe7Santos Cordon     * object immediately based off information in the call, but
357ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * more information is returned to the OnQueryCompleteListener (which contains
367ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * information about the phone number label, user's name, etc).
377ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     */
38fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal    public static CallerInfo getCallerInfoForCall(Context context, Call call,
397ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
408b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng        CallerInfo info = buildCallerInfo(context, call);
417ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
428b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng        // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
438b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng
446cddf46812634fadc194830774110780f14e9462Tyler Gunn        if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
458b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng            // Start the query with the number provided from the call.
468b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng            Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()...");
475ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen            CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call);
488b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng        }
498b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng        return info;
508b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng    }
518b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng
52fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal    public static CallerInfo buildCallerInfo(Context context, Call call) {
538b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng        CallerInfo info = new CallerInfo();
548b6c8d0dbfba6eabe3d835f8cdcec8a13e253d0cChiao Cheng
557ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // Store CNAP information retrieved from the Connection (we want to do this
567ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // here regardless of whether the number is empty or not).
57fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal        info.cnapName = call.getCnapName();
587ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        info.name = info.cnapName;
59fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal        info.numberPresentation = call.getNumberPresentation();
60fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal        info.namePresentation = call.getCnapNamePresentation();
617ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
62fc22ba88566ef70e202128335231c367de6c52afSailesh Nepal        String number = call.getNumber();
634bbe4c6900dfe3584f674161dbe15e248d5adbe7Santos Cordon        if (!TextUtils.isEmpty(number)) {
64101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner            final String[] numbers = number.split("&");
65101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner            number = numbers[0];
66101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner            if (numbers.length > 1) {
67101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner                info.forwardingNumber = numbers[1];
68101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner            }
69101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner
704bbe4c6900dfe3584f674161dbe15e248d5adbe7Santos Cordon            number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
714bbe4c6900dfe3584f674161dbe15e248d5adbe7Santos Cordon            info.phoneNumber = number;
727ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
735ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen
745ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen        // Because the InCallUI is immediately launched before the call is connected, occasionally
755ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen        // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
765ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen        // This call should still be handled as a voicemail call.
7704318a393b5492fb72d6edafca588e6b25ad752cNancy Chen        if ((call.getHandle() != null &&
7804318a393b5492fb72d6edafca588e6b25ad752cNancy Chen                PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) ||
7904318a393b5492fb72d6edafca588e6b25ad752cNancy Chen                isVoiceMailNumber(context, call)) {
805ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen            info.markAsVoiceMail(context);
815ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen        }
825ea0dcd39ff1f29d09d7bb146d7eb20f24b32201Nancy Chen
83789d810ee67f42b9ce7062423552f317af59fa78Yorke Lee        ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call,
84789d810ee67f42b9ce7062423552f317af59fa78Yorke Lee                info);
85789d810ee67f42b9ce7062423552f317af59fa78Yorke Lee
867ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        return info;
877ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    }
887ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
8904318a393b5492fb72d6edafca588e6b25ad752cNancy Chen    public static boolean isVoiceMailNumber(Context context, Call call) {
9004318a393b5492fb72d6edafca588e6b25ad752cNancy Chen         TelecomManager telecomManager =
9104318a393b5492fb72d6edafca588e6b25ad752cNancy Chen                 (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
9204318a393b5492fb72d6edafca588e6b25ad752cNancy Chen         return telecomManager.isVoiceMailNumber(
9304318a393b5492fb72d6edafca588e6b25ad752cNancy Chen                 call.getTelecommCall().getDetails().getAccountHandle(), call.getNumber());
9404318a393b5492fb72d6edafca588e6b25ad752cNancy Chen    }
9504318a393b5492fb72d6edafca588e6b25ad752cNancy Chen
967ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    /**
977ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * Handles certain "corner cases" for CNAP. When we receive weird phone numbers
987ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * from the network to indicate different number presentations, convert them to
997ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * expected number and presentation values within the CallerInfo object.
1007ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * @param number number we use to verify if we are in a corner case
1017ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * @param presentation presentation value used to verify if we are in a corner case
1027ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     * @return the new String that should be used for the phone number
1037ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee     */
1047ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    /* package */static String modifyForSpecialCnapCases(Context context, CallerInfo ci,
1055461c6a3b5090e6fab47076c5c0aeb2d1091606dSailesh Nepal            String number, int presentation) {
1067ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // Obviously we return number if ci == null, but still return number if
1077ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // number == null, because in these cases the correct string will still be
1087ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // displayed/logged after this function returns based on the presentation value.
1097ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        if (ci == null || number == null) return number;
1107ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1111a7f2bcab2d2023f2ee4cfb0bc57bc265b5aab87Chiao Cheng        Log.d(TAG, "modifyForSpecialCnapCases: initially, number="
1127ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                + toLogSafePhoneNumber(number)
1137ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                + ", presentation=" + presentation + " ci " + ci);
1147ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1157ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // "ABSENT NUMBER" is a possible value we could get from the network as the
1167ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // phone number, so if this happens, change it to "Unknown" in the CallerInfo
1177ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // and fix the presentation to be the same.
1187ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        final String[] absentNumberValues =
1197ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                context.getResources().getStringArray(R.array.absent_num);
1207ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        if (Arrays.asList(absentNumberValues).contains(number)
1216cddf46812634fadc194830774110780f14e9462Tyler Gunn                && presentation == TelecomManager.PRESENTATION_ALLOWED) {
1227ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            number = context.getString(R.string.unknown);
1236cddf46812634fadc194830774110780f14e9462Tyler Gunn            ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
1247ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
1257ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1267ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // Check for other special "corner cases" for CNAP and fix them similarly. Corner
1277ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // cases only apply if we received an allowed presentation from the network, so check
1287ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
1297ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // match the presentation passed in for verification (meaning we changed it previously
1307ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // because it's a corner case and we're being called from a different entry point).
1316cddf46812634fadc194830774110780f14e9462Tyler Gunn        if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED
1327ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                || (ci.numberPresentation != presentation
1336cddf46812634fadc194830774110780f14e9462Tyler Gunn                        && presentation == TelecomManager.PRESENTATION_ALLOWED)) {
13407a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal            // For all special strings, change number & numberPrentation.
13507a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal            if (isCnapSpecialCaseRestricted(number)) {
13607a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal                number = context.getString(R.string.private_num);
1376cddf46812634fadc194830774110780f14e9462Tyler Gunn                ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED;
13807a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal            } else if (isCnapSpecialCaseUnknown(number)) {
13907a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal                number = context.getString(R.string.unknown);
1406cddf46812634fadc194830774110780f14e9462Tyler Gunn                ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
1417ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            }
14207a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal            Log.d(TAG, "SpecialCnap: number=" + toLogSafePhoneNumber(number)
14307a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal                    + "; presentation now=" + ci.numberPresentation);
1447ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
1451a7f2bcab2d2023f2ee4cfb0bc57bc265b5aab87Chiao Cheng        Log.d(TAG, "modifyForSpecialCnapCases: returning number string="
1467ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                + toLogSafePhoneNumber(number));
1477ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        return number;
1487ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    }
1497ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
15007a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal    private static boolean isCnapSpecialCaseRestricted(String n) {
15107a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal        return n.equals("PRIVATE") || n.equals("P") || n.equals("RES");
15207a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal    }
15307a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal
15407a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal    private static boolean isCnapSpecialCaseUnknown(String n) {
15507a822b2ea92dc5b68cd43139be7efe4c6640cd4Sailesh Nepal        return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
1567ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    }
1577ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1587ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    /* package */static String toLogSafePhoneNumber(String number) {
1597ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // For unknown number, log empty string.
1607ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        if (number == null) {
1617ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            return "";
1627ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
1637ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1642d84705bf4c4f0588ccf4a6d64832e7f99b532cfChristine Chen        // Todo: Figure out an equivalent for VDBG
1657ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        if (false) {
1667ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            // When VDBG is true we emit PII.
1677ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            return number;
1687ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
1697ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee
1707ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
1717ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        // sanitized phone numbers.
1727ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        StringBuilder builder = new StringBuilder();
1737ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        for (int i = 0; i < number.length(); i++) {
1747ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            char c = number.charAt(i);
175101bed44998ff35c3a56f431345fac5c8229ec0eJay Shrauner            if (c == '-' || c == '@' || c == '.' || c == '&') {
1767ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                builder.append(c);
1777ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            } else {
1787ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee                builder.append('x');
1797ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee            }
1807ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        }
1817ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee        return builder.toString();
1827ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee    }
183fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee
184fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee    /**
185bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee     * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are
186bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee     * viewing a particular contact, so that it can download the high-res photo.
187fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee     */
188fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee    public static void sendViewNotification(Context context, Uri contactUri) {
189bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee        final ContactLoader loader = new ContactLoader(context, contactUri,
190bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                true /* postViewNotification */);
191bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee        loader.registerListener(0, new OnLoadCompleteListener<Contact>() {
192bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee            @Override
193bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee            public void onLoadComplete(
194bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                    Loader<Contact> loader, Contact contact) {
195bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                try {
196bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                    loader.reset();
197bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                } catch (RuntimeException e) {
198bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                    Log.e(TAG, "Error resetting loader", e);
199bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee                }
200bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee            }
201bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee        });
202bf41b978dcb99db47247db6c56821d8090131f6eYorke Lee        loader.startLoading();
203fd76bfcd5bd3ee6cc1eb53019a23738192972180Yorke Lee    }
2047ef707319c0e3563ff92a772ba53cb5dca0e3ae1Yorke Lee}
205