diff options
Diffstat (limited to 'src/com/android/providers/downloads/Helpers.java')
-rw-r--r-- | src/com/android/providers/downloads/Helpers.java | 492 |
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'); + } + } } |