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