• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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