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 static android.os.Environment.buildExternalStorageAppCacheDirs; 20 import static android.os.Environment.buildExternalStorageAppFilesDirs; 21 import static android.os.Environment.buildExternalStorageAppMediaDirs; 22 import static android.os.Environment.buildExternalStorageAppObbDirs; 23 import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING; 24 import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE; 25 26 import static com.android.providers.downloads.Constants.TAG; 27 28 import android.app.job.JobInfo; 29 import android.app.job.JobScheduler; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.database.Cursor; 33 import android.net.Uri; 34 import android.os.Environment; 35 import android.os.FileUtils; 36 import android.os.Handler; 37 import android.os.HandlerThread; 38 import android.os.Process; 39 import android.os.SystemClock; 40 import android.os.UserHandle; 41 import android.os.storage.StorageManager; 42 import android.os.storage.StorageVolume; 43 import android.provider.Downloads; 44 import android.util.Log; 45 import android.webkit.MimeTypeMap; 46 47 import com.google.common.annotations.VisibleForTesting; 48 49 import java.io.File; 50 import java.io.IOException; 51 import java.util.Random; 52 import java.util.Set; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 /** 57 * Some helper functions for the download manager 58 */ 59 public class Helpers { 60 public static Random sRandom = new Random(SystemClock.uptimeMillis()); 61 62 /** Regex used to parse content-disposition headers */ 63 private static final Pattern CONTENT_DISPOSITION_PATTERN = 64 Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); 65 66 private static final Object sUniqueLock = new Object(); 67 68 private static HandlerThread sAsyncHandlerThread; 69 private static Handler sAsyncHandler; 70 71 private static SystemFacade sSystemFacade; 72 private static DownloadNotifier sNotifier; 73 74 private Helpers() { 75 } 76 77 public synchronized static Handler getAsyncHandler() { 78 if (sAsyncHandlerThread == null) { 79 sAsyncHandlerThread = new HandlerThread("sAsyncHandlerThread", 80 Process.THREAD_PRIORITY_BACKGROUND); 81 sAsyncHandlerThread.start(); 82 sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper()); 83 } 84 return sAsyncHandler; 85 } 86 87 @VisibleForTesting 88 public synchronized static void setSystemFacade(SystemFacade systemFacade) { 89 sSystemFacade = systemFacade; 90 } 91 92 public synchronized static SystemFacade getSystemFacade(Context context) { 93 if (sSystemFacade == null) { 94 sSystemFacade = new RealSystemFacade(context); 95 } 96 return sSystemFacade; 97 } 98 99 public synchronized static DownloadNotifier getDownloadNotifier(Context context) { 100 if (sNotifier == null) { 101 sNotifier = new DownloadNotifier(context); 102 } 103 return sNotifier; 104 } 105 106 public static String getString(Cursor cursor, String col) { 107 return cursor.getString(cursor.getColumnIndexOrThrow(col)); 108 } 109 110 public static int getInt(Cursor cursor, String col) { 111 return cursor.getInt(cursor.getColumnIndexOrThrow(col)); 112 } 113 114 public static void scheduleJob(Context context, long downloadId) { 115 final boolean scheduled = scheduleJob(context, 116 DownloadInfo.queryDownloadInfo(context, downloadId)); 117 if (!scheduled) { 118 // If we didn't schedule a future job, kick off a notification 119 // update pass immediately 120 getDownloadNotifier(context).update(); 121 } 122 } 123 124 /** 125 * Schedule (or reschedule) a job for the given {@link DownloadInfo} using 126 * its current state to define job constraints. 127 */ 128 public static boolean scheduleJob(Context context, DownloadInfo info) { 129 if (info == null) return false; 130 131 final JobScheduler scheduler = context.getSystemService(JobScheduler.class); 132 133 // Tear down any existing job for this download 134 final int jobId = (int) info.mId; 135 scheduler.cancel(jobId); 136 137 // Skip scheduling if download is paused or finished 138 if (!info.isReadyToSchedule()) return false; 139 140 final JobInfo.Builder builder = new JobInfo.Builder(jobId, 141 new ComponentName(context, DownloadJobService.class)); 142 143 // When this download will show a notification, run with a higher 144 // priority, since it's effectively a foreground service 145 if (info.isVisible()) { 146 builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP); 147 builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND); 148 } 149 150 // We might have a backoff constraint due to errors 151 final long latency = info.getMinimumLatency(); 152 if (latency > 0) { 153 builder.setMinimumLatency(latency); 154 } 155 156 // We always require a network, but the type of network might be further 157 // restricted based on download request or user override 158 builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes)); 159 160 if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) { 161 builder.setRequiresCharging(true); 162 } 163 if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) { 164 builder.setRequiresDeviceIdle(true); 165 } 166 167 // If package name was filtered during insert (probably due to being 168 // invalid), blame based on the requesting UID instead 169 String packageName = info.mPackage; 170 if (packageName == null) { 171 packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0]; 172 } 173 174 scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG); 175 return true; 176 } 177 178 /* 179 * Parse the Content-Disposition HTTP Header. The format of the header 180 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 181 * This header provides a filename for content that is going to be 182 * downloaded to the file system. We only support the attachment type. 183 */ 184 private static String parseContentDisposition(String contentDisposition) { 185 try { 186 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 187 if (m.find()) { 188 return m.group(1); 189 } 190 } catch (IllegalStateException ex) { 191 // This function is defined as returning null when it can't parse the header 192 } 193 return null; 194 } 195 196 /** 197 * Creates a filename (where the file should be saved) from info about a download. 198 * This file will be touched to reserve it. 199 */ 200 static String generateSaveFile(Context context, String url, String hint, 201 String contentDisposition, String contentLocation, String mimeType, int destination) 202 throws IOException { 203 204 final File parent; 205 final File[] parentTest; 206 String name = null; 207 208 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 209 final File file = new File(Uri.parse(hint).getPath()); 210 parent = file.getParentFile().getAbsoluteFile(); 211 parentTest = new File[] { parent }; 212 name = file.getName(); 213 } else { 214 parent = getRunningDestinationDirectory(context, destination); 215 parentTest = new File[] { 216 parent, 217 getSuccessDestinationDirectory(context, destination) 218 }; 219 name = chooseFilename(url, hint, contentDisposition, contentLocation); 220 } 221 222 // Ensure target directories are ready 223 for (File test : parentTest) { 224 if (!(test.isDirectory() || test.mkdirs())) { 225 throw new IOException("Failed to create parent for " + test); 226 } 227 } 228 229 if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) { 230 name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name); 231 } 232 233 final String prefix; 234 final String suffix; 235 final int dotIndex = name.lastIndexOf('.'); 236 final boolean missingExtension = dotIndex < 0; 237 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 238 // Destination is explicitly set - do not change the extension 239 if (missingExtension) { 240 prefix = name; 241 suffix = ""; 242 } else { 243 prefix = name.substring(0, dotIndex); 244 suffix = name.substring(dotIndex); 245 } 246 } else { 247 // Split filename between base and extension 248 // Add an extension if filename does not have one 249 if (missingExtension) { 250 prefix = name; 251 suffix = chooseExtensionFromMimeType(mimeType, true); 252 } else { 253 prefix = name.substring(0, dotIndex); 254 suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex); 255 } 256 } 257 258 synchronized (sUniqueLock) { 259 name = generateAvailableFilenameLocked(parentTest, prefix, suffix); 260 261 // Claim this filename inside lock to prevent other threads from 262 // clobbering us. We're not paranoid enough to use O_EXCL. 263 final File file = new File(parent, name); 264 file.createNewFile(); 265 return file.getAbsolutePath(); 266 } 267 } 268 269 private static String chooseFilename(String url, String hint, String contentDisposition, 270 String contentLocation) { 271 String filename = null; 272 273 // First, try to use the hint from the application, if there's one 274 if (filename == null && hint != null && !hint.endsWith("/")) { 275 if (Constants.LOGVV) { 276 Log.v(Constants.TAG, "getting filename from hint"); 277 } 278 int index = hint.lastIndexOf('/') + 1; 279 if (index > 0) { 280 filename = hint.substring(index); 281 } else { 282 filename = hint; 283 } 284 } 285 286 // If we couldn't do anything with the hint, move toward the content disposition 287 if (filename == null && contentDisposition != null) { 288 filename = parseContentDisposition(contentDisposition); 289 if (filename != null) { 290 if (Constants.LOGVV) { 291 Log.v(Constants.TAG, "getting filename from content-disposition"); 292 } 293 int index = filename.lastIndexOf('/') + 1; 294 if (index > 0) { 295 filename = filename.substring(index); 296 } 297 } 298 } 299 300 // If we still have nothing at this point, try the content location 301 if (filename == null && contentLocation != null) { 302 String decodedContentLocation = Uri.decode(contentLocation); 303 if (decodedContentLocation != null 304 && !decodedContentLocation.endsWith("/") 305 && decodedContentLocation.indexOf('?') < 0) { 306 if (Constants.LOGVV) { 307 Log.v(Constants.TAG, "getting filename from content-location"); 308 } 309 int index = decodedContentLocation.lastIndexOf('/') + 1; 310 if (index > 0) { 311 filename = decodedContentLocation.substring(index); 312 } else { 313 filename = decodedContentLocation; 314 } 315 } 316 } 317 318 // If all the other http-related approaches failed, use the plain uri 319 if (filename == null) { 320 String decodedUrl = Uri.decode(url); 321 if (decodedUrl != null 322 && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { 323 int index = decodedUrl.lastIndexOf('/') + 1; 324 if (index > 0) { 325 if (Constants.LOGVV) { 326 Log.v(Constants.TAG, "getting filename from uri"); 327 } 328 filename = decodedUrl.substring(index); 329 } 330 } 331 } 332 333 // Finally, if couldn't get filename from URI, get a generic filename 334 if (filename == null) { 335 if (Constants.LOGVV) { 336 Log.v(Constants.TAG, "using default filename"); 337 } 338 filename = Constants.DEFAULT_DL_FILENAME; 339 } 340 341 // The VFAT file system is assumed as target for downloads. 342 // Replace invalid characters according to the specifications of VFAT. 343 filename = FileUtils.buildValidFatFilename(filename); 344 345 return filename; 346 } 347 348 private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { 349 String extension = null; 350 if (mimeType != null) { 351 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 352 if (extension != null) { 353 if (Constants.LOGVV) { 354 Log.v(Constants.TAG, "adding extension from type"); 355 } 356 extension = "." + extension; 357 } else { 358 if (Constants.LOGVV) { 359 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 360 } 361 } 362 } 363 if (extension == null) { 364 if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 365 if (mimeType.equalsIgnoreCase("text/html")) { 366 if (Constants.LOGVV) { 367 Log.v(Constants.TAG, "adding default html extension"); 368 } 369 extension = Constants.DEFAULT_DL_HTML_EXTENSION; 370 } else if (useDefaults) { 371 if (Constants.LOGVV) { 372 Log.v(Constants.TAG, "adding default text extension"); 373 } 374 extension = Constants.DEFAULT_DL_TEXT_EXTENSION; 375 } 376 } else if (useDefaults) { 377 if (Constants.LOGVV) { 378 Log.v(Constants.TAG, "adding default binary extension"); 379 } 380 extension = Constants.DEFAULT_DL_BINARY_EXTENSION; 381 } 382 } 383 return extension; 384 } 385 386 private static String chooseExtensionFromFilename(String mimeType, int destination, 387 String filename, int lastDotIndex) { 388 String extension = null; 389 if (mimeType != null) { 390 // Compare the last segment of the extension against the mime type. 391 // If there's a mismatch, discard the entire extension. 392 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 393 filename.substring(lastDotIndex + 1)); 394 if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { 395 extension = chooseExtensionFromMimeType(mimeType, false); 396 if (extension != null) { 397 if (Constants.LOGVV) { 398 Log.v(Constants.TAG, "substituting extension from type"); 399 } 400 } else { 401 if (Constants.LOGVV) { 402 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 403 } 404 } 405 } 406 } 407 if (extension == null) { 408 if (Constants.LOGVV) { 409 Log.v(Constants.TAG, "keeping extension"); 410 } 411 extension = filename.substring(lastDotIndex); 412 } 413 return extension; 414 } 415 416 private static boolean isFilenameAvailableLocked(File[] parents, String name) { 417 if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false; 418 419 for (File parent : parents) { 420 if (new File(parent, name).exists()) { 421 return false; 422 } 423 } 424 425 return true; 426 } 427 428 private static String generateAvailableFilenameLocked( 429 File[] parents, String prefix, String suffix) throws IOException { 430 String name = prefix + suffix; 431 if (isFilenameAvailableLocked(parents, name)) { 432 return name; 433 } 434 435 /* 436 * This number is used to generate partially randomized filenames to avoid 437 * collisions. 438 * It starts at 1. 439 * The next 9 iterations increment it by 1 at a time (up to 10). 440 * The next 9 iterations increment it by 1 to 10 (random) at a time. 441 * The next 9 iterations increment it by 1 to 100 (random) at a time. 442 * ... Up to the point where it increases by 100000000 at a time. 443 * (the maximum value that can be reached is 1000000000) 444 * As soon as a number is reached that generates a filename that doesn't exist, 445 * that filename is used. 446 * If the filename coming in is [base].[ext], the generated filenames are 447 * [base]-[sequence].[ext]. 448 */ 449 int sequence = 1; 450 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 451 for (int iteration = 0; iteration < 9; ++iteration) { 452 name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix; 453 if (isFilenameAvailableLocked(parents, name)) { 454 return name; 455 } 456 sequence += sRandom.nextInt(magnitude) + 1; 457 } 458 } 459 460 throw new IOException("Failed to generate an available filename"); 461 } 462 463 static boolean isFilenameValid(Context context, File file) { 464 return isFilenameValid(context, file, true); 465 } 466 467 static boolean isFilenameValidInExternal(Context context, File file) { 468 return isFilenameValid(context, file, false); 469 } 470 471 /** 472 * Test if given file exists in one of the package-specific external storage 473 * directories that are always writable to apps, regardless of storage 474 * permission. 475 */ 476 static boolean isFilenameValidInExternalPackage(Context context, File file, 477 String packageName) { 478 try { 479 if (containsCanonical(buildExternalStorageAppFilesDirs(packageName), file) || 480 containsCanonical(buildExternalStorageAppObbDirs(packageName), file) || 481 containsCanonical(buildExternalStorageAppCacheDirs(packageName), file) || 482 containsCanonical(buildExternalStorageAppMediaDirs(packageName), file)) { 483 return true; 484 } 485 } catch (IOException e) { 486 Log.w(TAG, "Failed to resolve canonical path: " + e); 487 return false; 488 } 489 490 Log.w(TAG, "Path appears to be invalid: " + file); 491 return false; 492 } 493 494 /** 495 * Checks whether the filename looks legitimate for security purposes. This 496 * prevents us from opening files that aren't actually downloads. 497 */ 498 static boolean isFilenameValid(Context context, File file, boolean allowInternal) { 499 try { 500 if (allowInternal) { 501 if (containsCanonical(context.getFilesDir(), file) 502 || containsCanonical(context.getCacheDir(), file) 503 || containsCanonical(Environment.getDownloadCacheDirectory(), file)) { 504 return true; 505 } 506 } 507 508 final StorageVolume[] volumes = StorageManager.getVolumeList(UserHandle.myUserId(), 509 StorageManager.FLAG_FOR_WRITE); 510 for (StorageVolume volume : volumes) { 511 if (containsCanonical(volume.getPathFile(), file)) { 512 return true; 513 } 514 } 515 } catch (IOException e) { 516 Log.w(TAG, "Failed to resolve canonical path: " + e); 517 return false; 518 } 519 520 Log.w(TAG, "Path appears to be invalid: " + file); 521 return false; 522 } 523 524 private static boolean containsCanonical(File dir, File file) throws IOException { 525 return FileUtils.contains(dir.getCanonicalFile(), file); 526 } 527 528 private static boolean containsCanonical(File[] dirs, File file) throws IOException { 529 for (File dir : dirs) { 530 if (containsCanonical(dir, file)) { 531 return true; 532 } 533 } 534 return false; 535 } 536 537 public static File getRunningDestinationDirectory(Context context, int destination) 538 throws IOException { 539 return getDestinationDirectory(context, destination, true); 540 } 541 542 public static File getSuccessDestinationDirectory(Context context, int destination) 543 throws IOException { 544 return getDestinationDirectory(context, destination, false); 545 } 546 547 private static File getDestinationDirectory(Context context, int destination, boolean running) 548 throws IOException { 549 switch (destination) { 550 case Downloads.Impl.DESTINATION_CACHE_PARTITION: 551 case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 552 case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 553 if (running) { 554 return context.getFilesDir(); 555 } else { 556 return context.getCacheDir(); 557 } 558 559 case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: 560 if (running) { 561 return new File(Environment.getDownloadCacheDirectory(), 562 Constants.DIRECTORY_CACHE_RUNNING); 563 } else { 564 return Environment.getDownloadCacheDirectory(); 565 } 566 567 case Downloads.Impl.DESTINATION_EXTERNAL: 568 final File target = new File( 569 Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); 570 if (!target.isDirectory() && target.mkdirs()) { 571 throw new IOException("unable to create external downloads directory"); 572 } 573 return target; 574 575 default: 576 throw new IllegalStateException("unexpected destination: " + destination); 577 } 578 } 579 580 /** 581 * Checks whether this looks like a legitimate selection parameter 582 */ 583 public static void validateSelection(String selection, Set<String> allowedColumns) { 584 try { 585 if (selection == null || selection.isEmpty()) { 586 return; 587 } 588 Lexer lexer = new Lexer(selection, allowedColumns); 589 parseExpression(lexer); 590 if (lexer.currentToken() != Lexer.TOKEN_END) { 591 throw new IllegalArgumentException("syntax error"); 592 } 593 } catch (RuntimeException ex) { 594 if (Constants.LOGV) { 595 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex); 596 } else if (false) { 597 Log.d(Constants.TAG, "invalid selection triggered " + ex); 598 } 599 throw ex; 600 } 601 602 } 603 604 // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] * 605 // | statement [AND_OR expression]* 606 private static void parseExpression(Lexer lexer) { 607 for (;;) { 608 // ( expression ) 609 if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) { 610 lexer.advance(); 611 parseExpression(lexer); 612 if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) { 613 throw new IllegalArgumentException("syntax error, unmatched parenthese"); 614 } 615 lexer.advance(); 616 } else { 617 // statement 618 parseStatement(lexer); 619 } 620 if (lexer.currentToken() != Lexer.TOKEN_AND_OR) { 621 break; 622 } 623 lexer.advance(); 624 } 625 } 626 627 // statement <- COLUMN COMPARE VALUE 628 // | COLUMN IS NULL 629 private static void parseStatement(Lexer lexer) { 630 // both possibilities start with COLUMN 631 if (lexer.currentToken() != Lexer.TOKEN_COLUMN) { 632 throw new IllegalArgumentException("syntax error, expected column name"); 633 } 634 lexer.advance(); 635 636 // statement <- COLUMN COMPARE VALUE 637 if (lexer.currentToken() == Lexer.TOKEN_COMPARE) { 638 lexer.advance(); 639 if (lexer.currentToken() != Lexer.TOKEN_VALUE) { 640 throw new IllegalArgumentException("syntax error, expected quoted string"); 641 } 642 lexer.advance(); 643 return; 644 } 645 646 // statement <- COLUMN IS NULL 647 if (lexer.currentToken() == Lexer.TOKEN_IS) { 648 lexer.advance(); 649 if (lexer.currentToken() != Lexer.TOKEN_NULL) { 650 throw new IllegalArgumentException("syntax error, expected NULL"); 651 } 652 lexer.advance(); 653 return; 654 } 655 656 // didn't get anything good after COLUMN 657 throw new IllegalArgumentException("syntax error after column name"); 658 } 659 660 /** 661 * A simple lexer that recognizes the words of our restricted subset of SQL where clauses 662 */ 663 private static class Lexer { 664 public static final int TOKEN_START = 0; 665 public static final int TOKEN_OPEN_PAREN = 1; 666 public static final int TOKEN_CLOSE_PAREN = 2; 667 public static final int TOKEN_AND_OR = 3; 668 public static final int TOKEN_COLUMN = 4; 669 public static final int TOKEN_COMPARE = 5; 670 public static final int TOKEN_VALUE = 6; 671 public static final int TOKEN_IS = 7; 672 public static final int TOKEN_NULL = 8; 673 public static final int TOKEN_END = 9; 674 675 private final String mSelection; 676 private final Set<String> mAllowedColumns; 677 private int mOffset = 0; 678 private int mCurrentToken = TOKEN_START; 679 private final char[] mChars; 680 681 public Lexer(String selection, Set<String> allowedColumns) { 682 mSelection = selection; 683 mAllowedColumns = allowedColumns; 684 mChars = new char[mSelection.length()]; 685 mSelection.getChars(0, mChars.length, mChars, 0); 686 advance(); 687 } 688 689 public int currentToken() { 690 return mCurrentToken; 691 } 692 693 public void advance() { 694 char[] chars = mChars; 695 696 // consume whitespace 697 while (mOffset < chars.length && chars[mOffset] == ' ') { 698 ++mOffset; 699 } 700 701 // end of input 702 if (mOffset == chars.length) { 703 mCurrentToken = TOKEN_END; 704 return; 705 } 706 707 // "(" 708 if (chars[mOffset] == '(') { 709 ++mOffset; 710 mCurrentToken = TOKEN_OPEN_PAREN; 711 return; 712 } 713 714 // ")" 715 if (chars[mOffset] == ')') { 716 ++mOffset; 717 mCurrentToken = TOKEN_CLOSE_PAREN; 718 return; 719 } 720 721 // "?" 722 if (chars[mOffset] == '?') { 723 ++mOffset; 724 mCurrentToken = TOKEN_VALUE; 725 return; 726 } 727 728 // "=" and "==" 729 if (chars[mOffset] == '=') { 730 ++mOffset; 731 mCurrentToken = TOKEN_COMPARE; 732 if (mOffset < chars.length && chars[mOffset] == '=') { 733 ++mOffset; 734 } 735 return; 736 } 737 738 // ">" and ">=" 739 if (chars[mOffset] == '>') { 740 ++mOffset; 741 mCurrentToken = TOKEN_COMPARE; 742 if (mOffset < chars.length && chars[mOffset] == '=') { 743 ++mOffset; 744 } 745 return; 746 } 747 748 // "<", "<=" and "<>" 749 if (chars[mOffset] == '<') { 750 ++mOffset; 751 mCurrentToken = TOKEN_COMPARE; 752 if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) { 753 ++mOffset; 754 } 755 return; 756 } 757 758 // "!=" 759 if (chars[mOffset] == '!') { 760 ++mOffset; 761 mCurrentToken = TOKEN_COMPARE; 762 if (mOffset < chars.length && chars[mOffset] == '=') { 763 ++mOffset; 764 return; 765 } 766 throw new IllegalArgumentException("Unexpected character after !"); 767 } 768 769 // columns and keywords 770 // first look for anything that looks like an identifier or a keyword 771 // and then recognize the individual words. 772 // no attempt is made at discarding sequences of underscores with no alphanumeric 773 // characters, even though it's not clear that they'd be legal column names. 774 if (isIdentifierStart(chars[mOffset])) { 775 int startOffset = mOffset; 776 ++mOffset; 777 while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) { 778 ++mOffset; 779 } 780 String word = mSelection.substring(startOffset, mOffset); 781 if (mOffset - startOffset <= 4) { 782 if (word.equals("IS")) { 783 mCurrentToken = TOKEN_IS; 784 return; 785 } 786 if (word.equals("OR") || word.equals("AND")) { 787 mCurrentToken = TOKEN_AND_OR; 788 return; 789 } 790 if (word.equals("NULL")) { 791 mCurrentToken = TOKEN_NULL; 792 return; 793 } 794 } 795 if (mAllowedColumns.contains(word)) { 796 mCurrentToken = TOKEN_COLUMN; 797 return; 798 } 799 throw new IllegalArgumentException("unrecognized column or keyword"); 800 } 801 802 // quoted strings 803 if (chars[mOffset] == '\'') { 804 ++mOffset; 805 while (mOffset < chars.length) { 806 if (chars[mOffset] == '\'') { 807 if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') { 808 ++mOffset; 809 } else { 810 break; 811 } 812 } 813 ++mOffset; 814 } 815 if (mOffset == chars.length) { 816 throw new IllegalArgumentException("unterminated string"); 817 } 818 ++mOffset; 819 mCurrentToken = TOKEN_VALUE; 820 return; 821 } 822 823 // anything we don't recognize 824 throw new IllegalArgumentException("illegal character: " + chars[mOffset]); 825 } 826 827 private static final boolean isIdentifierStart(char c) { 828 return c == '_' || 829 (c >= 'A' && c <= 'Z') || 830 (c >= 'a' && c <= 'z'); 831 } 832 833 private static final boolean isIdentifierChar(char c) { 834 return c == '_' || 835 (c >= 'A' && c <= 'Z') || 836 (c >= 'a' && c <= 'z') || 837 (c >= '0' && c <= '9'); 838 } 839 } 840 } 841