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 
17 package com.android.providers.contacts;
18 
19 import 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  */
25 public 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 
compareTo(LookupKeySegment another)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      */
getAccountHashCode(String accountTypeWithDataSet, String accountName)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 
appendToLookupKey(StringBuilder lookupKey, String accountTypeWithDataSet, String accountName, long rawContactId, String sourceId, String displayName)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 
appendEscapedSourceId(StringBuilder sb, String sourceId)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 
parse(String lookupKey)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