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 android.net.Uri;
20 
21 import 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  */
27 public 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 
compareTo(LookupKeySegment another)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      */
getAccountHashCode(String accountTypeWithDataSet, String accountName)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 
appendToLookupKey(StringBuilder lookupKey, String accountTypeWithDataSet, String accountName, long rawContactId, String sourceId, String displayName)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 
appendEscapedSourceId(StringBuilder sb, String sourceId)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 
parse(String lookupKey)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