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