1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.providers.contacts;
18
19import java.util.ArrayList;
20
21/**
22 * Contacts lookup key. Used for generation and parsing of contact lookup keys as well
23 * as doing the actual lookup.
24 */
25public class ContactLookupKey {
26
27    public static final int LOOKUP_TYPE_SOURCE_ID = 0;
28    public static final int LOOKUP_TYPE_DISPLAY_NAME = 1;
29    public static final int LOOKUP_TYPE_RAW_CONTACT_ID = 2;
30    public static final int LOOKUP_TYPE_PROFILE = 3;
31
32    // The Profile contact will always have a lookup key of "profile".
33    public static final String PROFILE_LOOKUP_KEY = "profile";
34
35    public static class LookupKeySegment implements Comparable<LookupKeySegment> {
36        public int accountHashCode;
37        public int lookupType;
38        public String rawContactId;
39        public String key;
40        public long contactId;
41
42        public int compareTo(LookupKeySegment another) {
43            if (contactId > another.contactId) {
44                return -1;
45            }
46            if (contactId < another.contactId) {
47                return 1;
48            }
49            return 0;
50        }
51    }
52
53    /**
54     * Returns a short hash code that functions as an additional precaution against the exceedingly
55     * improbable collision between sync IDs in different accounts.
56     */
57    public static int getAccountHashCode(String accountTypeWithDataSet, String accountName) {
58        if (accountTypeWithDataSet == null || accountName == null) {
59            return 0;
60        }
61
62        return (accountTypeWithDataSet.hashCode() ^ accountName.hashCode()) & 0xFFF;
63    }
64
65    public static void appendToLookupKey(StringBuilder lookupKey, String accountTypeWithDataSet,
66            String accountName, long rawContactId, String sourceId,
67            String displayName) {
68        if (displayName == null) {
69            displayName = "";
70        }
71
72        if (lookupKey.length() != 0) {
73            lookupKey.append(".");
74        }
75
76        lookupKey.append(getAccountHashCode(accountTypeWithDataSet, accountName));
77        if (sourceId == null) {
78            lookupKey.append('r').append(rawContactId).append('-').append(
79                    NameNormalizer.normalize(displayName));
80        } else {
81            int pos = lookupKey.length();
82            lookupKey.append('i');
83            if (appendEscapedSourceId(lookupKey, sourceId)) {
84                lookupKey.setCharAt(pos, 'e');
85            }
86        }
87    }
88
89    private static boolean appendEscapedSourceId(StringBuilder sb, String sourceId) {
90        boolean escaped = false;
91        int start = 0;
92        while (true) {
93            int index = sourceId.indexOf('.', start);
94            if (index == -1) {
95                sb.append(sourceId, start, sourceId.length());
96                break;
97            }
98
99            escaped = true;
100            sb.append(sourceId, start, index);
101            sb.append("..");
102            start = index + 1;
103        }
104        return escaped;
105    }
106
107    public ArrayList<LookupKeySegment> parse(String lookupKey) {
108        ArrayList<LookupKeySegment> list = new ArrayList<LookupKeySegment>();
109
110        // If the lookup key is for the profile, just return a segment list indicating that.  The
111        // caller should already be in a context in which the only contact in the database is the
112        // user's profile.
113        if (PROFILE_LOOKUP_KEY.equals(lookupKey)) {
114            LookupKeySegment profileSegment = new LookupKeySegment();
115            profileSegment.lookupType = LOOKUP_TYPE_PROFILE;
116            list.add(profileSegment);
117            return list;
118        }
119
120        String string = lookupKey;
121        int offset = 0;
122        int length = string.length();
123        int hashCode = 0;
124        int lookupType = -1;
125        boolean escaped = false;
126        String rawContactId = null;
127        String key;
128
129        while (offset < length) {
130            char c = 0;
131
132            // Parse account hash code
133            hashCode = 0;
134            while (offset < length) {
135                c = string.charAt(offset++);
136                if (c < '0' || c > '9') {
137                    break;
138                }
139                hashCode = hashCode * 10 + (c - '0');
140            }
141
142            // Parse segment type
143            if (c == 'i') {
144                lookupType = LOOKUP_TYPE_SOURCE_ID;
145                escaped = false;
146            } else if (c == 'e') {
147                lookupType = LOOKUP_TYPE_SOURCE_ID;
148                escaped = true;
149            } else if (c == 'n') {
150                lookupType = LOOKUP_TYPE_DISPLAY_NAME;
151            } else if (c == 'r') {
152                lookupType = LOOKUP_TYPE_RAW_CONTACT_ID;
153            } else if (c == 'c') {
154                    throw new IllegalArgumentException(
155                            "Work contact lookup key is not accepted here: " + lookupKey);
156            } else {
157                throw new IllegalArgumentException("Invalid lookup id: " + lookupKey);
158            }
159
160            // Parse the source ID or normalized display name
161            switch (lookupType) {
162                case LOOKUP_TYPE_SOURCE_ID: {
163                    if (escaped) {
164                        StringBuffer sb = new StringBuffer();
165                        while (offset < length) {
166                            c = string.charAt(offset++);
167
168                            if (c == '.') {
169                                if (offset == length) {
170                                    throw new IllegalArgumentException("Invalid lookup id: " +
171                                            lookupKey);
172                                }
173                                c = string.charAt(offset);
174
175                                if (c == '.') {
176                                    sb.append('.');
177                                    offset++;
178                                } else {
179                                    break;
180                                }
181                            } else {
182                                sb.append(c);
183                            }
184                        }
185                        key = sb.toString();
186                    } else {
187                        int start = offset;
188                        while (offset < length) {
189                            c = string.charAt(offset++);
190                            if (c == '.') {
191                                break;
192                            }
193                        }
194                        if (offset == length) {
195                            key = string.substring(start);
196                        } else {
197                            key = string.substring(start, offset - 1);
198                        }
199                    }
200                    break;
201                }
202                case LOOKUP_TYPE_DISPLAY_NAME: {
203                    int start = offset;
204                    while (offset < length) {
205                        c = string.charAt(offset++);
206                        if (c == '.') {
207                            break;
208                        }
209                    }
210                    if (offset == length) {
211                        key = string.substring(start);
212                    } else {
213                        key = string.substring(start, offset - 1);
214                    }
215                    break;
216                }
217                case LOOKUP_TYPE_RAW_CONTACT_ID: {
218                    int dash = -1;
219                    int start = offset;
220                    while (offset < length) {
221                        c = string.charAt(offset);
222                        if (c == '-' && dash == -1) {
223                            dash = offset;
224                        }
225                        offset++;
226                        if (c == '.') {
227                            break;
228                        }
229                    }
230                    if (dash != -1) {
231                        rawContactId = string.substring(start, dash);
232                        start = dash + 1;
233                    }
234                    if (offset == length) {
235                        key = string.substring(start);
236                    } else {
237                        key = string.substring(start, offset - 1);
238                    }
239                    break;
240                }
241                default:
242                    // Will never happen
243                    throw new IllegalStateException();
244            }
245
246            LookupKeySegment segment = new LookupKeySegment();
247            segment.accountHashCode = hashCode;
248            segment.lookupType = lookupType;
249            segment.rawContactId = rawContactId;
250            segment.key = key;
251            segment.contactId = -1;
252            list.add(segment);
253        }
254
255        return list;
256    }
257}
258