summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads/Helpers.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r--src/com/android/providers/downloads/Helpers.java492
1 files changed, 388 insertions, 104 deletions
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index f966a7f5..89a57313 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -27,7 +28,9 @@ import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Environment;
import android.os.StatFs;
+import android.os.SystemClock;
import android.provider.Downloads;
+import android.telephony.TelephonyManager;
import android.util.Config;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -39,14 +42,15 @@ import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.Set;
/**
* Some helper functions for the download manager
*/
public class Helpers {
- /** Tag used for debugging/logging */
- private static final String TAG = Constants.TAG;
-
+
+ public static Random rnd = new Random(SystemClock.uptimeMillis());
+
/** Regex used to parse content-disposition headers */
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
@@ -83,8 +87,6 @@ public class Helpers {
String contentLocation,
String mimeType,
int destination,
- boolean otaUpdate,
- boolean noSystem,
int contentLength) throws FileNotFoundException {
/*
@@ -94,13 +96,7 @@ public class Helpers {
|| destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE) {
if (mimeType == null) {
if (Config.LOGD) {
- Log.d(TAG, "external download with no mime type not allowed");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
- }
- if (noSystem && mimeType.equalsIgnoreCase(Constants.MIMETYPE_APK)) {
- if (Config.LOGD) {
- Log.d(TAG, "system files not allowed by initiating application");
+ Log.d(Constants.TAG, "external download with no mime type not allowed");
}
return new DownloadFileInfo(null, null, Downloads.STATUS_NOT_ACCEPTABLE);
}
@@ -121,7 +117,7 @@ public class Helpers {
intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
List<ResolveInfo> list = pm.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
- //Log.i(TAG, "*** FILENAME QUERY " + intent + ": " + list);
+ //Log.i(Constants.TAG, "*** FILENAME QUERY " + intent + ": " + list);
if (list.size() == 0) {
if (Config.LOGD) {
@@ -132,7 +128,7 @@ public class Helpers {
}
}
String filename = chooseFilename(
- url, hint, contentDisposition, contentLocation, destination, otaUpdate);
+ url, hint, contentDisposition, contentLocation, destination);
// Split filename between base and extension
// Add an extension if filename does not have one
@@ -142,7 +138,7 @@ public class Helpers {
extension = chooseExtensionFromMimeType(mimeType, true);
} else {
extension = chooseExtensionFromFilename(
- mimeType, destination, otaUpdate, filename, dotIndex);
+ mimeType, destination, filename, dotIndex);
filename = filename.substring(0, dotIndex);
}
@@ -156,6 +152,7 @@ public class Helpers {
// the DRM content provider
if (destination == Downloads.DESTINATION_CACHE_PARTITION
|| destination == Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE
+ || destination == Downloads.DESTINATION_CACHE_PARTITION_NOROAMING
|| DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
base = Environment.getDownloadCacheDirectory();
stat = new StatFs(base.getPath());
@@ -173,7 +170,8 @@ public class Helpers {
if (!discardPurgeableFiles(context,
contentLength - blockSize * ((long) availableBlocks - 4))) {
if (Config.LOGD) {
- Log.d(TAG, "download aborted - not enough free space in internal storage");
+ Log.d(Constants.TAG,
+ "download aborted - not enough free space in internal storage");
}
return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
@@ -181,34 +179,22 @@ public class Helpers {
}
} else {
- if (destination == Downloads.DESTINATION_DATA_CACHE) {
- base = context.getCacheDir();
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ String root = Environment.getExternalStorageDirectory().getPath();
+ base = new File(root + Constants.DEFAULT_DL_SUBDIR);
if (!base.isDirectory() && !base.mkdir()) {
- if (Config.LOGD) {
- Log.d(TAG, "download aborted - can't create base directory "
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "download aborted - can't create base directory "
+ base.getPath());
}
return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
stat = new StatFs(base.getPath());
} else {
- if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- String root = Environment.getExternalStorageDirectory().getPath();
- base = new File(root + Constants.DEFAULT_DL_SUBDIR);
- if (!base.isDirectory() && !base.mkdir()) {
- if (Config.LOGD) {
- Log.d(TAG, "download aborted - can't create base directory "
- + base.getPath());
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
- }
- stat = new StatFs(base.getPath());
- } else {
- if (Config.LOGD) {
- Log.d(TAG, "download aborted - no external storage");
- }
- return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
+ if (Config.LOGD) {
+ Log.d(Constants.TAG, "download aborted - no external storage");
}
+ return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
/*
@@ -217,14 +203,13 @@ public class Helpers {
*/
if (stat.getBlockSize() * ((long) stat.getAvailableBlocks() - 4) < contentLength) {
if (Config.LOGD) {
- Log.d(TAG, "download aborted - not enough free space");
+ Log.d(Constants.TAG, "download aborted - not enough free space");
}
return new DownloadFileInfo(null, null, Downloads.STATUS_FILE_ERROR);
}
}
- boolean otaFilename = Constants.OTA_UPDATE_FILENAME.equalsIgnoreCase(filename + extension);
boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
filename = base.getPath() + File.separator + filename;
@@ -233,11 +218,11 @@ public class Helpers {
* Generate a unique filename, create the file, return it.
*/
if (Constants.LOGVV) {
- Log.v(TAG, "target file: " + filename + extension);
+ Log.v(Constants.TAG, "target file: " + filename + extension);
}
String fullFilename = chooseUniqueFilename(
- destination, otaUpdate, filename, extension, otaFilename, recoveryDir);
+ destination, filename, extension, recoveryDir);
if (fullFilename != null) {
return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
} else {
@@ -246,18 +231,13 @@ public class Helpers {
}
private static String chooseFilename(String url, String hint, String contentDisposition,
- String contentLocation, int destination, boolean otaUpdate) {
+ String contentLocation, int destination) {
String filename = null;
- // Before we even start, special-case the OTA updates
- if (destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate) {
- filename = Constants.OTA_UPDATE_FILENAME;
- }
-
// First, try to use the hint from the application, if there's one
if (filename == null && hint != null && !hint.endsWith("/")) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from hint");
+ Log.v(Constants.TAG, "getting filename from hint");
}
int index = hint.lastIndexOf('/') + 1;
if (index > 0) {
@@ -272,7 +252,7 @@ public class Helpers {
filename = parseContentDisposition(contentDisposition);
if (filename != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from content-disposition");
+ Log.v(Constants.TAG, "getting filename from content-disposition");
}
int index = filename.lastIndexOf('/') + 1;
if (index > 0) {
@@ -288,7 +268,7 @@ public class Helpers {
&& !decodedContentLocation.endsWith("/")
&& decodedContentLocation.indexOf('?') < 0) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from content-location");
+ Log.v(Constants.TAG, "getting filename from content-location");
}
int index = decodedContentLocation.lastIndexOf('/') + 1;
if (index > 0) {
@@ -307,7 +287,7 @@ public class Helpers {
int index = decodedUrl.lastIndexOf('/') + 1;
if (index > 0) {
if (Constants.LOGVV) {
- Log.v(TAG, "getting filename from uri");
+ Log.v(Constants.TAG, "getting filename from uri");
}
filename = decodedUrl.substring(index);
}
@@ -317,10 +297,14 @@ public class Helpers {
// Finally, if couldn't get filename from URI, get a generic filename
if (filename == null) {
if (Constants.LOGVV) {
- Log.v(TAG, "using default filename");
+ Log.v(Constants.TAG, "using default filename");
}
filename = Constants.DEFAULT_DL_FILENAME;
}
+
+ filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
+
+
return filename;
}
@@ -330,12 +314,12 @@ public class Helpers {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (extension != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding extension from type");
+ Log.v(Constants.TAG, "adding extension from type");
}
extension = "." + extension;
} else {
if (Constants.LOGVV) {
- Log.v(TAG, "couldn't find extension for " + mimeType);
+ Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
}
}
}
@@ -343,18 +327,18 @@ public class Helpers {
if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
if (mimeType.equalsIgnoreCase("text/html")) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default html extension");
+ Log.v(Constants.TAG, "adding default html extension");
}
extension = Constants.DEFAULT_DL_HTML_EXTENSION;
} else if (useDefaults) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default text extension");
+ Log.v(Constants.TAG, "adding default text extension");
}
extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
}
} else if (useDefaults) {
if (Constants.LOGVV) {
- Log.v(TAG, "adding default binary extension");
+ Log.v(Constants.TAG, "adding default binary extension");
}
extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
}
@@ -363,10 +347,9 @@ public class Helpers {
}
private static String chooseExtensionFromFilename(String mimeType, int destination,
- boolean otaUpdate, String filename, int dotIndex) {
+ String filename, int dotIndex) {
String extension = null;
- if (mimeType != null
- && !(destination == Downloads.DESTINATION_CACHE_PARTITION && otaUpdate)) {
+ if (mimeType != null) {
// Compare the last segment of the extension against the mime type.
// If there's a mismatch, discard the entire extension.
int lastDotIndex = filename.lastIndexOf('.');
@@ -376,63 +359,60 @@ public class Helpers {
extension = chooseExtensionFromMimeType(mimeType, false);
if (extension != null) {
if (Constants.LOGVV) {
- Log.v(TAG, "substituting extension from type");
+ Log.v(Constants.TAG, "substituting extension from type");
}
} else {
if (Constants.LOGVV) {
- Log.v(TAG, "couldn't find extension for " + mimeType);
+ Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
}
}
}
}
if (extension == null) {
if (Constants.LOGVV) {
- Log.v(TAG, "keeping extension");
+ Log.v(Constants.TAG, "keeping extension");
}
extension = filename.substring(dotIndex);
}
return extension;
}
- private static String chooseUniqueFilename(int destination, boolean otaUpdate, String filename,
- String extension, boolean otaFilename, boolean recoveryDir) {
+ private static String chooseUniqueFilename(int destination, String filename,
+ String extension, boolean recoveryDir) {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
(destination != Downloads.DESTINATION_CACHE_PARTITION &&
- destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE))
- && (!otaFilename ||
- (otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION))) {
+ destination != Downloads.DESTINATION_CACHE_PARTITION_PURGEABLE &&
+ destination != Downloads.DESTINATION_CACHE_PARTITION_NOROAMING))) {
return fullFilename;
- } else if (!(otaUpdate && destination == Downloads.DESTINATION_CACHE_PARTITION)) {
- filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
- /*
- * This number is used to generate partially randomized filenames to avoid
- * collisions.
- * It starts at 1.
- * The next 9 iterations increment it by 1 at a time (up to 10).
- * The next 9 iterations increment it by 1 to 10 (random) at a time.
- * The next 9 iterations increment it by 1 to 100 (random) at a time.
- * ... Up to the point where it increases by 100000000 at a time.
- * (the maximum value that can be reached is 1000000000)
- * As soon as a number is reached that generates a filename that doesn't exist,
- * that filename is used.
- * If the filename coming in is [base].[ext], the generated filenames are
- * [base]-[sequence].[ext].
- */
- int sequence = 1;
- Random random = new Random();
- for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
- for (int iteration = 0; iteration < 9; ++iteration) {
- fullFilename = filename + sequence + extension;
- if (!new File(fullFilename).exists()) {
- return fullFilename;
- }
- if (Constants.LOGVV) {
- Log.v(TAG, "file with sequence number " + sequence + " exists");
- }
- sequence += random.nextInt(magnitude) + 1;
+ }
+ filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
+ /*
+ * This number is used to generate partially randomized filenames to avoid
+ * collisions.
+ * It starts at 1.
+ * The next 9 iterations increment it by 1 at a time (up to 10).
+ * The next 9 iterations increment it by 1 to 10 (random) at a time.
+ * The next 9 iterations increment it by 1 to 100 (random) at a time.
+ * ... Up to the point where it increases by 100000000 at a time.
+ * (the maximum value that can be reached is 1000000000)
+ * As soon as a number is reached that generates a filename that doesn't exist,
+ * that filename is used.
+ * If the filename coming in is [base].[ext], the generated filenames are
+ * [base]-[sequence].[ext].
+ */
+ int sequence = 1;
+ for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
+ for (int iteration = 0; iteration < 9; ++iteration) {
+ fullFilename = filename + sequence + extension;
+ if (!new File(fullFilename).exists()) {
+ return fullFilename;
+ }
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "file with sequence number " + sequence + " exists");
}
+ sequence += rnd.nextInt(magnitude) + 1;
}
}
return null;
@@ -460,15 +440,17 @@ public class Helpers {
try {
cursor.moveToFirst();
while (!cursor.isAfterLast() && totalFreed < targetBytes) {
- File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.FILENAME)));
+ File file = new File(cursor.getString(cursor.getColumnIndex(Downloads._DATA)));
if (Constants.LOGVV) {
Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
file.length() + " bytes");
}
totalFreed += file.length();
file.delete();
- cursor.deleteRow(); // This moves the cursor to the next entry,
- // no need to call next()
+ long id = cursor.getLong(cursor.getColumnIndex(Downloads._ID));
+ context.getContentResolver().delete(
+ ContentUris.withAppendedId(Downloads.CONTENT_URI, id), null, null);
+ cursor.moveToNext();
}
} finally {
cursor.close();
@@ -491,20 +473,322 @@ public class Helpers {
if (connectivity == null) {
Log.w(Constants.TAG, "couldn't get connectivity manager");
} else {
- NetworkInfo info = connectivity.getActiveNetworkInfo();
+ NetworkInfo[] info = connectivity.getAllNetworkInfo();
if (info != null) {
- if (info.getState() == NetworkInfo.State.CONNECTED) {
- if (Constants.LOGVV) {
- Log.v(TAG, "network is available");
+ for (int i = 0; i < info.length; i++) {
+ if (info[i].getState() == NetworkInfo.State.CONNECTED) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "network is available");
+ }
+ return true;
}
- return true;
}
}
}
if (Constants.LOGVV) {
- Log.v(TAG, "network is not available");
+ Log.v(Constants.TAG, "network is not available");
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the network is roaming
+ */
+ public static boolean isNetworkRoaming(Context context) {
+ ConnectivityManager connectivity =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivity == null) {
+ Log.w(Constants.TAG, "couldn't get connectivity manager");
+ } else {
+ NetworkInfo info = connectivity.getActiveNetworkInfo();
+ if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) {
+ if (TelephonyManager.getDefault().isNetworkRoaming()) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "network is roaming");
+ }
+ return true;
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "network is not roaming");
+ }
+ }
+ } else {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "not using mobile network");
+ }
+ }
}
return false;
}
+ /**
+ * Checks whether the filename looks legitimate
+ */
+ public static boolean isFilenameValid(String filename) {
+ File dir = new File(filename).getParentFile();
+ return dir.equals(Environment.getDownloadCacheDirectory())
+ || dir.equals(new File(Environment.getExternalStorageDirectory()
+ + Constants.DEFAULT_DL_SUBDIR));
+ }
+
+ /**
+ * Checks whether this looks like a legitimate selection parameter
+ */
+ public static void validateSelection(String selection, Set<String> allowedColumns) {
+ try {
+ if (selection == null) {
+ return;
+ }
+ Lexer lexer = new Lexer(selection, allowedColumns);
+ parseExpression(lexer);
+ if (lexer.currentToken() != Lexer.TOKEN_END) {
+ throw new IllegalArgumentException("syntax error");
+ }
+ } catch (RuntimeException ex) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "invalid selection triggered " + ex);
+ }
+ throw ex;
+ }
+
+ }
+
+ // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
+ // | statement [AND_OR expression]*
+ private static void parseExpression(Lexer lexer) {
+ for (;;) {
+ // ( expression )
+ if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
+ lexer.advance();
+ parseExpression(lexer);
+ if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
+ throw new IllegalArgumentException("syntax error, unmatched parenthese");
+ }
+ lexer.advance();
+ } else {
+ // statement
+ parseStatement(lexer);
+ }
+ if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
+ break;
+ }
+ lexer.advance();
+ }
+ }
+
+ // statement <- COLUMN COMPARE VALUE
+ // | COLUMN IS NULL
+ private static void parseStatement(Lexer lexer) {
+ // both possibilities start with COLUMN
+ if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
+ throw new IllegalArgumentException("syntax error, expected column name");
+ }
+ lexer.advance();
+
+ // statement <- COLUMN COMPARE VALUE
+ if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
+ lexer.advance();
+ if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
+ throw new IllegalArgumentException("syntax error, expected quoted string");
+ }
+ lexer.advance();
+ return;
+ }
+
+ // statement <- COLUMN IS NULL
+ if (lexer.currentToken() == Lexer.TOKEN_IS) {
+ lexer.advance();
+ if (lexer.currentToken() != Lexer.TOKEN_NULL) {
+ throw new IllegalArgumentException("syntax error, expected NULL");
+ }
+ lexer.advance();
+ return;
+ }
+
+ // didn't get anything good after COLUMN
+ throw new IllegalArgumentException("syntax error after column name");
+ }
+
+ /**
+ * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
+ */
+ private static class Lexer {
+ public static final int TOKEN_START = 0;
+ public static final int TOKEN_OPEN_PAREN = 1;
+ public static final int TOKEN_CLOSE_PAREN = 2;
+ public static final int TOKEN_AND_OR = 3;
+ public static final int TOKEN_COLUMN = 4;
+ public static final int TOKEN_COMPARE = 5;
+ public static final int TOKEN_VALUE = 6;
+ public static final int TOKEN_IS = 7;
+ public static final int TOKEN_NULL = 8;
+ public static final int TOKEN_END = 9;
+
+ private final String mSelection;
+ private final Set<String> mAllowedColumns;
+ private int mOffset = 0;
+ private int mCurrentToken = TOKEN_START;
+ private final char[] mChars;
+
+ public Lexer(String selection, Set<String> allowedColumns) {
+ mSelection = selection;
+ mAllowedColumns = allowedColumns;
+ mChars = new char[mSelection.length()];
+ mSelection.getChars(0, mChars.length, mChars, 0);
+ advance();
+ }
+
+ public int currentToken() {
+ return mCurrentToken;
+ }
+
+ public void advance() {
+ char[] chars = mChars;
+
+ // consume whitespace
+ while (mOffset < chars.length && chars[mOffset] == ' ') {
+ ++mOffset;
+ }
+
+ // end of input
+ if (mOffset == chars.length) {
+ mCurrentToken = TOKEN_END;
+ return;
+ }
+
+ // "("
+ if (chars[mOffset] == '(') {
+ ++mOffset;
+ mCurrentToken = TOKEN_OPEN_PAREN;
+ return;
+ }
+
+ // ")"
+ if (chars[mOffset] == ')') {
+ ++mOffset;
+ mCurrentToken = TOKEN_CLOSE_PAREN;
+ return;
+ }
+
+ // "?"
+ if (chars[mOffset] == '?') {
+ ++mOffset;
+ mCurrentToken = TOKEN_VALUE;
+ return;
+ }
+
+ // "=" and "=="
+ if (chars[mOffset] == '=') {
+ ++mOffset;
+ mCurrentToken = TOKEN_COMPARE;
+ if (mOffset < chars.length && chars[mOffset] == '=') {
+ ++mOffset;
+ }
+ return;
+ }
+
+ // ">" and ">="
+ if (chars[mOffset] == '>') {
+ ++mOffset;
+ mCurrentToken = TOKEN_COMPARE;
+ if (mOffset < chars.length && chars[mOffset] == '=') {
+ ++mOffset;
+ }
+ return;
+ }
+
+ // "<", "<=" and "<>"
+ if (chars[mOffset] == '<') {
+ ++mOffset;
+ mCurrentToken = TOKEN_COMPARE;
+ if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
+ ++mOffset;
+ }
+ return;
+ }
+
+ // "!="
+ if (chars[mOffset] == '!') {
+ ++mOffset;
+ mCurrentToken = TOKEN_COMPARE;
+ if (mOffset < chars.length && chars[mOffset] == '=') {
+ ++mOffset;
+ return;
+ }
+ throw new IllegalArgumentException("Unexpected character after !");
+ }
+
+ // columns and keywords
+ // first look for anything that looks like an identifier or a keyword
+ // and then recognize the individual words.
+ // no attempt is made at discarding sequences of underscores with no alphanumeric
+ // characters, even though it's not clear that they'd be legal column names.
+ if (isIdentifierStart(chars[mOffset])) {
+ int startOffset = mOffset;
+ ++mOffset;
+ while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
+ ++mOffset;
+ }
+ String word = mSelection.substring(startOffset, mOffset);
+ if (mOffset - startOffset <= 4) {
+ if (word.equals("IS")) {
+ mCurrentToken = TOKEN_IS;
+ return;
+ }
+ if (word.equals("OR") || word.equals("AND")) {
+ mCurrentToken = TOKEN_AND_OR;
+ return;
+ }
+ if (word.equals("NULL")) {
+ mCurrentToken = TOKEN_NULL;
+ return;
+ }
+ }
+ if (mAllowedColumns.contains(word)) {
+ mCurrentToken = TOKEN_COLUMN;
+ return;
+ }
+ throw new IllegalArgumentException("unrecognized column or keyword");
+ }
+
+ // quoted strings
+ if (chars[mOffset] == '\'') {
+ ++mOffset;
+ while(mOffset < chars.length) {
+ if (chars[mOffset] == '\'') {
+ if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
+ ++mOffset;
+ } else {
+ break;
+ }
+ }
+ ++mOffset;
+ }
+ if (mOffset == chars.length) {
+ throw new IllegalArgumentException("unterminated string");
+ }
+ ++mOffset;
+ mCurrentToken = TOKEN_VALUE;
+ return;
+ }
+
+ // anything we don't recognize
+ throw new IllegalArgumentException("illegal character");
+ }
+
+ private static final boolean isIdentifierStart(char c) {
+ return c == '_' ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z');
+ }
+
+ private static final boolean isIdentifierChar(char c) {
+ return c == '_' ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c >= '0' && c <= '9');
+ }
+ }
}