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 com.android.providers.downloads; 18 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.content.pm.ResolveInfo; 24 import android.database.Cursor; 25 import android.drm.mobile1.DrmRawContent; 26 import android.net.ConnectivityManager; 27 import android.net.NetworkInfo; 28 import android.net.Uri; 29 import android.os.Environment; 30 import android.os.StatFs; 31 import android.os.SystemClock; 32 import android.provider.Downloads; 33 import android.telephony.TelephonyManager; 34 import android.util.Config; 35 import android.util.Log; 36 import android.webkit.MimeTypeMap; 37 38 import java.io.File; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.util.Random; 42 import java.util.Set; 43 import java.util.regex.Matcher; 44 import java.util.regex.Pattern; 45 46 /** 47 * Some helper functions for the download manager 48 */ 49 public class Helpers { 50 51 public static Random sRandom = new Random(SystemClock.uptimeMillis()); 52 53 /** Regex used to parse content-disposition headers */ 54 private static final Pattern CONTENT_DISPOSITION_PATTERN = 55 Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); 56 Helpers()57 private Helpers() { 58 } 59 60 /* 61 * Parse the Content-Disposition HTTP Header. The format of the header 62 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 63 * This header provides a filename for content that is going to be 64 * downloaded to the file system. We only support the attachment type. 65 */ parseContentDisposition(String contentDisposition)66 private static String parseContentDisposition(String contentDisposition) { 67 try { 68 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 69 if (m.find()) { 70 return m.group(1); 71 } 72 } catch (IllegalStateException ex) { 73 // This function is defined as returning null when it can't parse the header 74 } 75 return null; 76 } 77 78 /** 79 * Creates a filename (where the file should be saved) from a uri. 80 */ generateSaveFile( Context context, String url, String hint, String contentDisposition, String contentLocation, String mimeType, int destination, int contentLength)81 public static DownloadFileInfo generateSaveFile( 82 Context context, 83 String url, 84 String hint, 85 String contentDisposition, 86 String contentLocation, 87 String mimeType, 88 int destination, 89 int contentLength) throws FileNotFoundException { 90 91 /* 92 * Don't download files that we won't be able to handle 93 */ 94 if (destination == Downloads.DESTINATION_EXTERNAL 95 || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) { 96 if (mimeType == null) { 97 if (Config.LOGD) { 98 Log.d(Constants.TAG, "external download with no mime type not allowed"); 99 } 100 return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE); 101 } 102 if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) { 103 // Check to see if we are allowed to download this file. Only files 104 // that can be handled by the platform can be downloaded. 105 // special case DRM files, which we should always allow downloading. 106 Intent intent = new Intent(Intent.ACTION_VIEW); 107 108 // We can provide data as either content: or file: URIs, 109 // so allow both. (I think it would be nice if we just did 110 // everything as content: URIs) 111 // Actually, right now the download manager's UId restrictions 112 // prevent use from using content: so it's got to be file: or 113 // nothing 114 115 PackageManager pm = context.getPackageManager(); 116 intent.setDataAndType(Uri.fromParts("file", "", null), mimeType); 117 ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); 118 //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list); 119 120 if (ri == null) { 121 if (Config.LOGD) { 122 Log.d(Constants.TAG, "no handler found for type " + mimeType); 123 } 124 return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE); 125 } 126 } 127 } 128 String filename = chooseFilename( 129 url, hint, contentDisposition, contentLocation, destination); 130 131 // Split filename between base and extension 132 // Add an extension if filename does not have one 133 String extension = null; 134 int dotIndex = filename.indexOf('.'); 135 if (dotIndex < 0) { 136 extension = chooseExtensionFromMimeType(mimeType, true); 137 } else { 138 extension = chooseExtensionFromFilename( 139 mimeType, destination, filename, dotIndex); 140 filename = filename.substring(0, dotIndex); 141 } 142 143 /* 144 * Locate the directory where the file will be saved 145 */ 146 147 File base = null; 148 StatFs stat = null; 149 // DRM messages should be temporarily stored internally and then passed to 150 // the DRM content provider 151 if (destination == Downloads.DESTINATION_CACHE_PARTITION 152 || destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE 153 || destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING 154 || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) { 155 base = Environment.getDownloadCacheDirectory(); 156 stat = new StatFs(base.getPath()); 157 158 /* 159 * Check whether there's enough space on the target filesystem to save the file. 160 * Put a bit of margin (in case creating the file grows the system by a few blocks). 161 */ 162 int blockSize = stat.getBlockSize(); 163 for (;;) { 164 int availableBlocks = stat.getAvailableBlocks(); 165 if (blockSize * ((long) availableBlocks - 4) >= contentLength) { 166 break; 167 } 168 if (!discardPurgeableFiles(context, 169 contentLength - blockSize * ((long) availableBlocks - 4))) { 170 if (Config.LOGD) { 171 Log.d(Constants.TAG, 172 "download aborted - not enough free space in internal storage"); 173 } 174 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR); 175 } 176 stat.restat(base.getPath()); 177 } 178 179 } else { 180 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 181 String root = Environment.getExternalStorageDirectory().getPath(); 182 base = new File(root + Constants.DEFAULT_DL_SUBDIR); 183 if (!base.isDirectory() && !base.mkdir()) { 184 if (Config.LOGD) { 185 Log.d(Constants.TAG, "download aborted - can't create base directory " 186 + base.getPath()); 187 } 188 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR); 189 } 190 stat = new StatFs(base.getPath()); 191 } else { 192 if (Config.LOGD) { 193 Log.d(Constants.TAG, "download aborted - no external storage"); 194 } 195 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR); 196 } 197 198 /* 199 * Check whether there's enough space on the target filesystem to save the file. 200 * Put a bit of margin (in case creating the file grows the system by a few blocks). 201 */ 202 if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) { 203 if (Config.LOGD) { 204 Log.d(Constants.TAG, "download aborted - not enough free space"); 205 } 206 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR); 207 } 208 209 } 210 211 boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension); 212 213 filename = base.getPath() + File.separator + filename; 214 215 /* 216 * Generate a unique filename, create the file, return it. 217 */ 218 if (Constants.LOGVV) { 219 Log.v(Constants.TAG, "target file: " + filename + extension); 220 } 221 222 String fullFilename = chooseUniqueFilename( 223 destination, filename, extension, recoveryDir); 224 if (fullFilename != null) { 225 return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0); 226 } else { 227 return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR); 228 } 229 } 230 chooseFilename(String url, String hint, String contentDisposition, String contentLocation, int destination)231 private static String chooseFilename(String url, String hint, String contentDisposition, 232 String contentLocation, int destination) { 233 String filename = null; 234 235 // First, try to use the hint from the application, if there's one 236 if (filename == null && hint != null && !hint.endsWith("/")) { 237 if (Constants.LOGVV) { 238 Log.v(Constants.TAG, "getting filename from hint"); 239 } 240 int index = hint.lastIndexOf('/') + 1; 241 if (index > 0) { 242 filename = hint.substring(index); 243 } else { 244 filename = hint; 245 } 246 } 247 248 // If we couldn't do anything with the hint, move toward the content disposition 249 if (filename == null && contentDisposition != null) { 250 filename = parseContentDisposition(contentDisposition); 251 if (filename != null) { 252 if (Constants.LOGVV) { 253 Log.v(Constants.TAG, "getting filename from content-disposition"); 254 } 255 int index = filename.lastIndexOf('/') + 1; 256 if (index > 0) { 257 filename = filename.substring(index); 258 } 259 } 260 } 261 262 // If we still have nothing at this point, try the content location 263 if (filename == null && contentLocation != null) { 264 String decodedContentLocation = Uri.decode(contentLocation); 265 if (decodedContentLocation != null 266 && !decodedContentLocation.endsWith("/") 267 && decodedContentLocation.indexOf('?') < 0) { 268 if (Constants.LOGVV) { 269 Log.v(Constants.TAG, "getting filename from content-location"); 270 } 271 int index = decodedContentLocation.lastIndexOf('/') + 1; 272 if (index > 0) { 273 filename = decodedContentLocation.substring(index); 274 } else { 275 filename = decodedContentLocation; 276 } 277 } 278 } 279 280 // If all the other http-related approaches failed, use the plain uri 281 if (filename == null) { 282 String decodedUrl = Uri.decode(url); 283 if (decodedUrl != null 284 && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { 285 int index = decodedUrl.lastIndexOf('/') + 1; 286 if (index > 0) { 287 if (Constants.LOGVV) { 288 Log.v(Constants.TAG, "getting filename from uri"); 289 } 290 filename = decodedUrl.substring(index); 291 } 292 } 293 } 294 295 // Finally, if couldn't get filename from URI, get a generic filename 296 if (filename == null) { 297 if (Constants.LOGVV) { 298 Log.v(Constants.TAG, "using default filename"); 299 } 300 filename = Constants.DEFAULT_DL_FILENAME; 301 } 302 303 filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_"); 304 305 306 return filename; 307 } 308 chooseExtensionFromMimeType(String mimeType, boolean useDefaults)309 private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { 310 String extension = null; 311 if (mimeType != null) { 312 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 313 if (extension != null) { 314 if (Constants.LOGVV) { 315 Log.v(Constants.TAG, "adding extension from type"); 316 } 317 extension = "." + extension; 318 } else { 319 if (Constants.LOGVV) { 320 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 321 } 322 } 323 } 324 if (extension == null) { 325 if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 326 if (mimeType.equalsIgnoreCase("text/html")) { 327 if (Constants.LOGVV) { 328 Log.v(Constants.TAG, "adding default html extension"); 329 } 330 extension = Constants.DEFAULT_DL_HTML_EXTENSION; 331 } else if (useDefaults) { 332 if (Constants.LOGVV) { 333 Log.v(Constants.TAG, "adding default text extension"); 334 } 335 extension = Constants.DEFAULT_DL_TEXT_EXTENSION; 336 } 337 } else if (useDefaults) { 338 if (Constants.LOGVV) { 339 Log.v(Constants.TAG, "adding default binary extension"); 340 } 341 extension = Constants.DEFAULT_DL_BINARY_EXTENSION; 342 } 343 } 344 return extension; 345 } 346 chooseExtensionFromFilename(String mimeType, int destination, String filename, int dotIndex)347 private static String chooseExtensionFromFilename(String mimeType, int destination, 348 String filename, int dotIndex) { 349 String extension = null; 350 if (mimeType != null) { 351 // Compare the last segment of the extension against the mime type. 352 // If there's a mismatch, discard the entire extension. 353 int lastDotIndex = filename.lastIndexOf('.'); 354 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 355 filename.substring(lastDotIndex + 1)); 356 if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { 357 extension = chooseExtensionFromMimeType(mimeType, false); 358 if (extension != null) { 359 if (Constants.LOGVV) { 360 Log.v(Constants.TAG, "substituting extension from type"); 361 } 362 } else { 363 if (Constants.LOGVV) { 364 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 365 } 366 } 367 } 368 } 369 if (extension == null) { 370 if (Constants.LOGVV) { 371 Log.v(Constants.TAG, "keeping extension"); 372 } 373 extension = filename.substring(dotIndex); 374 } 375 return extension; 376 } 377 chooseUniqueFilename(int destination, String filename, String extension, boolean recoveryDir)378 private static String chooseUniqueFilename(int destination, String filename, 379 String extension, boolean recoveryDir) { 380 String fullFilename = filename + extension; 381 if (!new File(fullFilename).exists() 382 && (!recoveryDir || 383 (destination != Downloads.DESTINATION_CACHE_PARTITION && 384 destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE && 385 destination != Downloads.DESTINATION_CACHE_PARTITION_NOROAMING))) { 386 return fullFilename; 387 } 388 filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR; 389 /* 390 * This number is used to generate partially randomized filenames to avoid 391 * collisions. 392 * It starts at 1. 393 * The next 9 iterations increment it by 1 at a time (up to 10). 394 * The next 9 iterations increment it by 1 to 10 (random) at a time. 395 * The next 9 iterations increment it by 1 to 100 (random) at a time. 396 * ... Up to the point where it increases by 100000000 at a time. 397 * (the maximum value that can be reached is 1000000000) 398 * As soon as a number is reached that generates a filename that doesn't exist, 399 * that filename is used. 400 * If the filename coming in is [base].[ext], the generated filenames are 401 * [base]-[sequence].[ext]. 402 */ 403 int sequence = 1; 404 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 405 for (int iteration = 0; iteration < 9; ++iteration) { 406 fullFilename = filename + sequence + extension; 407 if (!new File(fullFilename).exists()) { 408 return fullFilename; 409 } 410 if (Constants.LOGVV) { 411 Log.v(Constants.TAG, "file with sequence number " + sequence + " exists"); 412 } 413 sequence += sRandom.nextInt(magnitude) + 1; 414 } 415 } 416 return null; 417 } 418 419 /** 420 * Deletes purgeable files from the cache partition. This also deletes 421 * the matching database entries. Files are deleted in LRU order until 422 * the total byte size is greater than targetBytes. 423 */ discardPurgeableFiles(Context context, long targetBytes)424 public static final boolean discardPurgeableFiles(Context context, long targetBytes) { 425 Cursor cursor = context.getContentResolver().query( 426 Downloads.CONTENT_URI, 427 null, 428 "( " + 429 Downloads.COLUMN_STATUS + " = '" + Downloads.STATUS_SUCCESS + "' AND " + 430 Downloads.COLUMN_DESTINATION + 431 " = '" + Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )", 432 null, 433 Downloads.COLUMN_LAST_MODIFICATION); 434 if (cursor == null) { 435 return false; 436 } 437 long totalFreed = 0; 438 try { 439 cursor.moveToFirst(); 440 while (!cursor.isAfterLast() && totalFreed < targetBytes) { 441 File file = new File(cursor.getString(cursor.getColumnIndex(Downloads._DATA))); 442 if (Constants.LOGVV) { 443 Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " + 444 file.length() + " bytes"); 445 } 446 totalFreed += file.length(); 447 file.delete(); 448 long id = cursor.getLong(cursor.getColumnIndex(Downloads._ID)); 449 context.getContentResolver().delete( 450 ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null); 451 cursor.moveToNext(); 452 } 453 } finally { 454 cursor.close(); 455 } 456 if (Constants.LOGV) { 457 if (totalFreed > 0) { 458 Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " + 459 targetBytes + " requested"); 460 } 461 } 462 return totalFreed > 0; 463 } 464 465 /** 466 * Returns whether the network is available 467 */ isNetworkAvailable(Context context)468 public static boolean isNetworkAvailable(Context context) { 469 ConnectivityManager connectivity = 470 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 471 if (connectivity == null) { 472 Log.w(Constants.TAG, "couldn't get connectivity manager"); 473 } else { 474 NetworkInfo[] info = connectivity.getAllNetworkInfo(); 475 if (info != null) { 476 for (int i = 0; i < info.length; i++) { 477 if (info[i].getState() == NetworkInfo.State.CONNECTED) { 478 if (Constants.LOGVV) { 479 Log.v(Constants.TAG, "network is available"); 480 } 481 return true; 482 } 483 } 484 } 485 } 486 if (Constants.LOGVV) { 487 Log.v(Constants.TAG, "network is not available"); 488 } 489 return false; 490 } 491 492 /** 493 * Returns whether the network is roaming 494 */ isNetworkRoaming(Context context)495 public static boolean isNetworkRoaming(Context context) { 496 ConnectivityManager connectivity = 497 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 498 if (connectivity == null) { 499 Log.w(Constants.TAG, "couldn't get connectivity manager"); 500 } else { 501 NetworkInfo info = connectivity.getActiveNetworkInfo(); 502 if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) { 503 if (TelephonyManager.getDefault().isNetworkRoaming()) { 504 if (Constants.LOGVV) { 505 Log.v(Constants.TAG, "network is roaming"); 506 } 507 return true; 508 } else { 509 if (Constants.LOGVV) { 510 Log.v(Constants.TAG, "network is not roaming"); 511 } 512 } 513 } else { 514 if (Constants.LOGVV) { 515 Log.v(Constants.TAG, "not using mobile network"); 516 } 517 } 518 } 519 return false; 520 } 521 522 /** 523 * Checks whether the filename looks legitimate 524 */ isFilenameValid(String filename)525 public static boolean isFilenameValid(String filename) { 526 File dir = new File(filename).getParentFile(); 527 return dir.equals(Environment.getDownloadCacheDirectory()) 528 || dir.equals(new File(Environment.getExternalStorageDirectory() 529 + Constants.DEFAULT_DL_SUBDIR)); 530 } 531 532 /** 533 * Checks whether this looks like a legitimate selection parameter 534 */ validateSelection(String selection, Set<String> allowedColumns)535 public static void validateSelection(String selection, Set<String> allowedColumns) { 536 try { 537 if (selection == null) { 538 return; 539 } 540 Lexer lexer = new Lexer(selection, allowedColumns); 541 parseExpression(lexer); 542 if (lexer.currentToken() != Lexer.TOKEN_END) { 543 throw new IllegalArgumentException("syntax error"); 544 } 545 } catch (RuntimeException ex) { 546 if (Constants.LOGV) { 547 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex); 548 } else if (Config.LOGD) { 549 Log.d(Constants.TAG, "invalid selection triggered " + ex); 550 } 551 throw ex; 552 } 553 554 } 555 556 // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] * 557 // | statement [AND_OR expression]* parseExpression(Lexer lexer)558 private static void parseExpression(Lexer lexer) { 559 for (;;) { 560 // ( expression ) 561 if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) { 562 lexer.advance(); 563 parseExpression(lexer); 564 if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) { 565 throw new IllegalArgumentException("syntax error, unmatched parenthese"); 566 } 567 lexer.advance(); 568 } else { 569 // statement 570 parseStatement(lexer); 571 } 572 if (lexer.currentToken() != Lexer.TOKEN_AND_OR) { 573 break; 574 } 575 lexer.advance(); 576 } 577 } 578 579 // statement <- COLUMN COMPARE VALUE 580 // | COLUMN IS NULL parseStatement(Lexer lexer)581 private static void parseStatement(Lexer lexer) { 582 // both possibilities start with COLUMN 583 if (lexer.currentToken() != Lexer.TOKEN_COLUMN) { 584 throw new IllegalArgumentException("syntax error, expected column name"); 585 } 586 lexer.advance(); 587 588 // statement <- COLUMN COMPARE VALUE 589 if (lexer.currentToken() == Lexer.TOKEN_COMPARE) { 590 lexer.advance(); 591 if (lexer.currentToken() != Lexer.TOKEN_VALUE) { 592 throw new IllegalArgumentException("syntax error, expected quoted string"); 593 } 594 lexer.advance(); 595 return; 596 } 597 598 // statement <- COLUMN IS NULL 599 if (lexer.currentToken() == Lexer.TOKEN_IS) { 600 lexer.advance(); 601 if (lexer.currentToken() != Lexer.TOKEN_NULL) { 602 throw new IllegalArgumentException("syntax error, expected NULL"); 603 } 604 lexer.advance(); 605 return; 606 } 607 608 // didn't get anything good after COLUMN 609 throw new IllegalArgumentException("syntax error after column name"); 610 } 611 612 /** 613 * A simple lexer that recognizes the words of our restricted subset of SQL where clauses 614 */ 615 private static class Lexer { 616 public static final int TOKEN_START = 0; 617 public static final int TOKEN_OPEN_PAREN = 1; 618 public static final int TOKEN_CLOSE_PAREN = 2; 619 public static final int TOKEN_AND_OR = 3; 620 public static final int TOKEN_COLUMN = 4; 621 public static final int TOKEN_COMPARE = 5; 622 public static final int TOKEN_VALUE = 6; 623 public static final int TOKEN_IS = 7; 624 public static final int TOKEN_NULL = 8; 625 public static final int TOKEN_END = 9; 626 627 private final String mSelection; 628 private final Set<String> mAllowedColumns; 629 private int mOffset = 0; 630 private int mCurrentToken = TOKEN_START; 631 private final char[] mChars; 632 Lexer(String selection, Set<String> allowedColumns)633 public Lexer(String selection, Set<String> allowedColumns) { 634 mSelection = selection; 635 mAllowedColumns = allowedColumns; 636 mChars = new char[mSelection.length()]; 637 mSelection.getChars(0, mChars.length, mChars, 0); 638 advance(); 639 } 640 currentToken()641 public int currentToken() { 642 return mCurrentToken; 643 } 644 advance()645 public void advance() { 646 char[] chars = mChars; 647 648 // consume whitespace 649 while (mOffset < chars.length && chars[mOffset] == ' ') { 650 ++mOffset; 651 } 652 653 // end of input 654 if (mOffset == chars.length) { 655 mCurrentToken = TOKEN_END; 656 return; 657 } 658 659 // "(" 660 if (chars[mOffset] == '(') { 661 ++mOffset; 662 mCurrentToken = TOKEN_OPEN_PAREN; 663 return; 664 } 665 666 // ")" 667 if (chars[mOffset] == ')') { 668 ++mOffset; 669 mCurrentToken = TOKEN_CLOSE_PAREN; 670 return; 671 } 672 673 // "?" 674 if (chars[mOffset] == '?') { 675 ++mOffset; 676 mCurrentToken = TOKEN_VALUE; 677 return; 678 } 679 680 // "=" and "==" 681 if (chars[mOffset] == '=') { 682 ++mOffset; 683 mCurrentToken = TOKEN_COMPARE; 684 if (mOffset < chars.length && chars[mOffset] == '=') { 685 ++mOffset; 686 } 687 return; 688 } 689 690 // ">" and ">=" 691 if (chars[mOffset] == '>') { 692 ++mOffset; 693 mCurrentToken = TOKEN_COMPARE; 694 if (mOffset < chars.length && chars[mOffset] == '=') { 695 ++mOffset; 696 } 697 return; 698 } 699 700 // "<", "<=" and "<>" 701 if (chars[mOffset] == '<') { 702 ++mOffset; 703 mCurrentToken = TOKEN_COMPARE; 704 if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) { 705 ++mOffset; 706 } 707 return; 708 } 709 710 // "!=" 711 if (chars[mOffset] == '!') { 712 ++mOffset; 713 mCurrentToken = TOKEN_COMPARE; 714 if (mOffset < chars.length && chars[mOffset] == '=') { 715 ++mOffset; 716 return; 717 } 718 throw new IllegalArgumentException("Unexpected character after !"); 719 } 720 721 // columns and keywords 722 // first look for anything that looks like an identifier or a keyword 723 // and then recognize the individual words. 724 // no attempt is made at discarding sequences of underscores with no alphanumeric 725 // characters, even though it's not clear that they'd be legal column names. 726 if (isIdentifierStart(chars[mOffset])) { 727 int startOffset = mOffset; 728 ++mOffset; 729 while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) { 730 ++mOffset; 731 } 732 String word = mSelection.substring(startOffset, mOffset); 733 if (mOffset - startOffset <= 4) { 734 if (word.equals("IS")) { 735 mCurrentToken = TOKEN_IS; 736 return; 737 } 738 if (word.equals("OR") || word.equals("AND")) { 739 mCurrentToken = TOKEN_AND_OR; 740 return; 741 } 742 if (word.equals("NULL")) { 743 mCurrentToken = TOKEN_NULL; 744 return; 745 } 746 } 747 if (mAllowedColumns.contains(word)) { 748 mCurrentToken = TOKEN_COLUMN; 749 return; 750 } 751 throw new IllegalArgumentException("unrecognized column or keyword"); 752 } 753 754 // quoted strings 755 if (chars[mOffset] == '\'') { 756 ++mOffset; 757 while (mOffset < chars.length) { 758 if (chars[mOffset] == '\'') { 759 if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') { 760 ++mOffset; 761 } else { 762 break; 763 } 764 } 765 ++mOffset; 766 } 767 if (mOffset == chars.length) { 768 throw new IllegalArgumentException("unterminated string"); 769 } 770 ++mOffset; 771 mCurrentToken = TOKEN_VALUE; 772 return; 773 } 774 775 // anything we don't recognize 776 throw new IllegalArgumentException("illegal character"); 777 } 778 isIdentifierStart(char c)779 private static final boolean isIdentifierStart(char c) { 780 return c == '_' || 781 (c >= 'A' && c <= 'Z') || 782 (c >= 'a' && c <= 'z'); 783 } 784 isIdentifierChar(char c)785 private static final boolean isIdentifierChar(char c) { 786 return c == '_' || 787 (c >= 'A' && c <= 'Z') || 788 (c >= 'a' && c <= 'z') || 789 (c >= '0' && c <= '9'); 790 } 791 } 792 } 793