1 /* 2 * Copyright (C) 2008 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 android.text.util; 18 19 import android.widget.MultiAutoCompleteTextView; 20 21 import java.util.ArrayList; 22 import java.util.Collection; 23 24 /** 25 * This class works as a Tokenizer for MultiAutoCompleteTextView for 26 * address list fields, and also provides a method for converting 27 * a string of addresses (such as might be typed into such a field) 28 * into a series of Rfc822Tokens. 29 */ 30 @android.ravenwood.annotation.RavenwoodKeepWholeClass 31 public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { 32 33 /** 34 * This constructor will try to take a string like 35 * "Foo Bar (something) <foo\@google.com>, 36 * blah\@google.com (something)" 37 * and convert it into one or more Rfc822Tokens, output into the supplied 38 * collection. 39 * 40 * It does *not* decode MIME encoded-words; charset conversion 41 * must already have taken place if necessary. 42 * It will try to be tolerant of broken syntax instead of 43 * returning an error. 44 * 45 */ tokenize(CharSequence text, Collection<Rfc822Token> out)46 public static void tokenize(CharSequence text, Collection<Rfc822Token> out) { 47 StringBuilder name = new StringBuilder(); 48 StringBuilder address = new StringBuilder(); 49 StringBuilder comment = new StringBuilder(); 50 51 int i = 0; 52 int cursor = text.length(); 53 54 while (i < cursor) { 55 char c = text.charAt(i); 56 57 if (c == ',' || c == ';') { 58 i++; 59 60 while (i < cursor && text.charAt(i) == ' ') { 61 i++; 62 } 63 64 crunch(name); 65 66 if (address.length() > 0) { 67 out.add(new Rfc822Token(name.toString(), 68 address.toString(), 69 comment.toString())); 70 } else if (name.length() > 0) { 71 out.add(new Rfc822Token(null, 72 name.toString(), 73 comment.toString())); 74 } 75 76 name.setLength(0); 77 address.setLength(0); 78 comment.setLength(0); 79 } else if (c == '"') { 80 i++; 81 82 while (i < cursor) { 83 c = text.charAt(i); 84 85 if (c == '"') { 86 i++; 87 break; 88 } else if (c == '\\') { 89 if (i + 1 < cursor) { 90 name.append(text.charAt(i + 1)); 91 } 92 i += 2; 93 } else { 94 name.append(c); 95 i++; 96 } 97 } 98 } else if (c == '(') { 99 int level = 1; 100 i++; 101 102 while (i < cursor && level > 0) { 103 c = text.charAt(i); 104 105 if (c == ')') { 106 if (level > 1) { 107 comment.append(c); 108 } 109 110 level--; 111 i++; 112 } else if (c == '(') { 113 comment.append(c); 114 level++; 115 i++; 116 } else if (c == '\\') { 117 if (i + 1 < cursor) { 118 comment.append(text.charAt(i + 1)); 119 } 120 i += 2; 121 } else { 122 comment.append(c); 123 i++; 124 } 125 } 126 } else if (c == '<') { 127 i++; 128 129 while (i < cursor) { 130 c = text.charAt(i); 131 132 if (c == '>') { 133 i++; 134 break; 135 } else { 136 address.append(c); 137 i++; 138 } 139 } 140 } else if (c == ' ') { 141 name.append('\0'); 142 i++; 143 } else { 144 name.append(c); 145 i++; 146 } 147 } 148 149 crunch(name); 150 151 if (address.length() > 0) { 152 out.add(new Rfc822Token(name.toString(), 153 address.toString(), 154 comment.toString())); 155 } else if (name.length() > 0) { 156 out.add(new Rfc822Token(null, 157 name.toString(), 158 comment.toString())); 159 } 160 } 161 162 /** 163 * This method will try to take a string like 164 * "Foo Bar (something) <foo\@google.com>, 165 * blah\@google.com (something)" 166 * and convert it into one or more Rfc822Tokens. 167 * It does *not* decode MIME encoded-words; charset conversion 168 * must already have taken place if necessary. 169 * It will try to be tolerant of broken syntax instead of 170 * returning an error. 171 */ tokenize(CharSequence text)172 public static Rfc822Token[] tokenize(CharSequence text) { 173 ArrayList<Rfc822Token> out = new ArrayList<Rfc822Token>(); 174 tokenize(text, out); 175 return out.toArray(new Rfc822Token[out.size()]); 176 } 177 crunch(StringBuilder sb)178 private static void crunch(StringBuilder sb) { 179 int i = 0; 180 int len = sb.length(); 181 182 while (i < len) { 183 char c = sb.charAt(i); 184 185 if (c == '\0') { 186 if (i == 0 || i == len - 1 || 187 sb.charAt(i - 1) == ' ' || 188 sb.charAt(i - 1) == '\0' || 189 sb.charAt(i + 1) == ' ' || 190 sb.charAt(i + 1) == '\0') { 191 sb.deleteCharAt(i); 192 len--; 193 } else { 194 i++; 195 } 196 } else { 197 i++; 198 } 199 } 200 201 for (i = 0; i < len; i++) { 202 if (sb.charAt(i) == '\0') { 203 sb.setCharAt(i, ' '); 204 } 205 } 206 } 207 208 /** 209 * {@inheritDoc} 210 */ findTokenStart(CharSequence text, int cursor)211 public int findTokenStart(CharSequence text, int cursor) { 212 /* 213 * It's hard to search backward, so search forward until 214 * we reach the cursor. 215 */ 216 217 int best = 0; 218 int i = 0; 219 220 while (i < cursor) { 221 i = findTokenEnd(text, i); 222 223 if (i < cursor) { 224 i++; // Skip terminating punctuation 225 226 while (i < cursor && text.charAt(i) == ' ') { 227 i++; 228 } 229 230 if (i < cursor) { 231 best = i; 232 } 233 } 234 } 235 236 return best; 237 } 238 239 /** 240 * {@inheritDoc} 241 */ findTokenEnd(CharSequence text, int cursor)242 public int findTokenEnd(CharSequence text, int cursor) { 243 int len = text.length(); 244 int i = cursor; 245 246 while (i < len) { 247 char c = text.charAt(i); 248 249 if (c == ',' || c == ';') { 250 return i; 251 } else if (c == '"') { 252 i++; 253 254 while (i < len) { 255 c = text.charAt(i); 256 257 if (c == '"') { 258 i++; 259 break; 260 } else if (c == '\\' && i + 1 < len) { 261 i += 2; 262 } else { 263 i++; 264 } 265 } 266 } else if (c == '(') { 267 int level = 1; 268 i++; 269 270 while (i < len && level > 0) { 271 c = text.charAt(i); 272 273 if (c == ')') { 274 level--; 275 i++; 276 } else if (c == '(') { 277 level++; 278 i++; 279 } else if (c == '\\' && i + 1 < len) { 280 i += 2; 281 } else { 282 i++; 283 } 284 } 285 } else if (c == '<') { 286 i++; 287 288 while (i < len) { 289 c = text.charAt(i); 290 291 if (c == '>') { 292 i++; 293 break; 294 } else { 295 i++; 296 } 297 } 298 } else { 299 i++; 300 } 301 } 302 303 return i; 304 } 305 306 /** 307 * Terminates the specified address with a comma and space. 308 * This assumes that the specified text already has valid syntax. 309 * The Adapter subclass's convertToString() method must make that 310 * guarantee. 311 */ terminateToken(CharSequence text)312 public CharSequence terminateToken(CharSequence text) { 313 return text + ", "; 314 } 315 } 316