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