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