最近在Android R机型上面,很多应用查询通话记录数据库的时候,抛出了下面异常
DatabaseUtils: java.lang.IllegalArgumentException: Invalid token LIMIT,具体如下:
06-11 10:46:40.431 4034 9554 E DatabaseUtils: Writing exception to parcel
06-11 10:46:40.431 4034 9554 E DatabaseUtils: java.lang.IllegalArgumentException: Invalid token LIMIT
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.enforceStrictToken(SQLiteQueryBuilder.java:824)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.lambda$W2yQ6UjYGqGIu6HEomKgdgvGNKI(Unknown Source:0)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.-$$Lambda$SQLiteQueryBuilder$W2yQ6UjYGqGIu6HEomKgdgvGNKI.accept(Unknown Source:4)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteTokenizer.tokenize(SQLiteTokenizer.java:108)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.enforceStrictGrammar(SQLiteQueryBuilder.java:794)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:557)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:502)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at com.android.providers.contacts.CallLogProvider.queryInternal(CallLogProvider.java:427)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at com.android.providers.contacts.CallLogProvider.query(CallLogProvider.java:350)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider.query(ContentProvider.java:1447)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider.query(ContentProvider.java:1543)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProvider$Transport.query(ContentProvider.java:289)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:106)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.os.Binder.execTransactInternal(Binder.java:1165)
06-11 10:46:40.431 4034 9554 E DatabaseUtils: at android.os.Binder.execTransact(Binder.java:1129)
执行代码块如下:
/**
* Get if the query is marked as strict, as last configured by
* {@link #setStrictGrammar(boolean)}.
*/
public static String getLastOutgoingCall(Context context) {
final ContentResolver resolver = context.getContentResolver();
Cursor c = null;
try {
c = resolver.query(Calls.CONTENT_URI,
new String[] {
Calls.NUMBER
}, Calls.TYPE + " = "
+ Calls.OUTGOING_TYPE, null,
Calls.DEFAULT_SORT_ORDER + " LIMIT 1");
if (c == null || !c.moveToFirst()) {
return "";
}
return c.getString(0);
} finally {
if (c != null)
c.close();
}
}
就是一个简单的数据库查询操作,这一段代码在其他机型上面执行没有问题。
第一次看到这个异常就蒙圈了,百度谷歌查询,都没有找到有用的信息。
接着从limit这个关键字入手,limit是一个查询条件,意思就是获取数据库标准多少行数据,是一个标准的数据库关键字,写法没有任何问题。
没有头绪就只能查看源码,看看为什么抛出异常
首先看SQLiteQueryBuilder.java这个类,异常是这个类跑出来的,从异常堆栈信息中看出,实在
enforceStrictToken()这个方法里面执行出问题了,代码如下:
private void enforceStrictToken(@NonNull String token) {
Log.e("limeng","token:"+token);
if (TextUtils.isEmpty(token)) return;
if (isTableOrColumn(token)) return;
if (SQLiteTokenizer.isFunction(token)) return;
if (SQLiteTokenizer.isType(token)) return;
// Carefully block any tokens that are attempting to jump across query
// clauses or create subqueries, since they could leak data that should
// have been filtered by the trusted where clause
boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
switch (token.toUpperCase(Locale.US)) {
case "SELECT":
case "FROM":
case "WHERE":
case "GROUP":
case "HAVING":
case "WINDOW":
case "VALUES":
case "ORDER":
case "LIMIT":
isAllowedKeyword = false;
break;
}
if (!isAllowedKeyword) {
throw new IllegalArgumentException("Invalid token " + token);
}
}
这段代码可以看出,就是这个token与switch里面的字段相符的话,就会抛出异常,F3快捷键,是在enforceStrictGrammar()中调用到了,代码如下
private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
@Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
new Consumer<String>() {
@Override
public void accept(String token) {
SQLiteQueryBuilder.this.enforceStrictToken(token);
}
});
SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
this::enforceStrictToken);
SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
this::enforceStrictToken);
SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
this::enforceStrictToken);
SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
new Consumer<String>() {
@Override
public void accept(String token) {
SQLiteQueryBuilder.this.enforceStrictToken(token);
}
});
}
这里面也没啥特殊的,接着F3,来到了query()方法中,代码如下:
public Cursor query(SQLiteDatabase db, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
if (mTables == null) {
return null;
}
final String sql;
final String unwrappedSql = buildQuery(
projectionIn, selection, groupBy, having,
sortOrder, limit);
if (isStrictColumns()) {
enforceStrictColumns(projectionIn);
}
if (isStrictGrammar()) {
enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
}
if (isStrict()) {
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
// The idea is to ensure that the selection clause is a valid SQL expression
// by compiling it twice: once wrapped in parentheses and once as
// originally specified. An attacker cannot create an expression that
// would escape the SQL expression while maintaining balanced parentheses
// in both the wrapped and original forms.
// NOTE: The ordering of the below operations is important; we must
// execute the wrapped query to ensure the untrusted clause has been
// fully isolated.
// Validate the unwrapped query
db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
// Execute wrapped query for extra protection
final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
wrap(having), sortOrder, limit);
sql = wrappedSql;
} else {
// Execute unwrapped query
sql = unwrappedSql;
}
final String[] sqlArgs = selectionArgs;
if (Log.isLoggable(TAG, Log.DEBUG)) {
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
} else {
Log.d(TAG, sql);
}
}
return db.rawQueryWithFactory(
mFactory, sql, sqlArgs,
SQLiteDatabase.findEditTable(mTables),
cancellationSignal); // will throw if query is invalid
}
在query方法中的555行代码那调用了enforceStrictGrammar()方法,但是要执行这个方法有个判断条件,看看这个判断条件是啥,F3进去,代码如下:
就两个成员变量,下来看mStrictFlags这个,看看他是在哪个地方赋值的,F3快捷键可以知道,代码如下:
/**
* When set, the selection is verified against malicious arguments. When
* using this class to create a statement using
* {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
* non-numeric limits will raise an exception. If a projection map is
* specified, fields not in that map will be ignored. If this class is used
* to execute the statement directly using
* {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
* or
* {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
* additionally also parenthesis escaping selection are caught. To
* summarize: To get maximum protection against malicious third party apps
* (for example content provider consumers), make sure to do the following:
* <ul>
* <li>Set this value to true</li>
* <li>Use a projection map</li>
* <li>Use one of the query overloads instead of getting the statement as a
* sql string</li>
* </ul>
* <p>
* This feature is disabled by default on each newly constructed
* {@link SQLiteQueryBuilder} and needs to be manually enabled.
*/
public void setStrict(boolean strict) {
if (strict) {
mStrictFlags |= STRICT_PARENTHESES;
} else {
mStrictFlags &= ~STRICT_PARENTHESES;
}
}
就是一个set方法,这里重点看下注释,注释里面有一段是最新添加的
* additionally also parenthesis escaping selection are caught. To
* summarize: To get maximum protection against malicious third party apps
* (for example content provider consumers), make sure to do the following:
谷歌翻译下结果如下:
此外还捕获括号转义选择。总结:为了最大限度地防止恶意第三方应用程序(例如内容提供商消费者),请确保执行以下操作:将此值设置为 true,使用投影图使用查询重载之一,而不是将语句作为 sql 字符串获取
这段话的意思就是谷歌让咱们查询的时候,不要用sql语句字符串,如果用到了sql语句字符串,就会将里面的参数解析出来,执行到enforceStrictToken方式时,就会出现异常。上面的查询操作,limit就用到了sql语句字符串。到了这就知道为啥导致了查询异常,接下来就要修改查询语句,谷歌的官方文档推荐写法如下:
// Request 20 records starting at row index 30.
Bundle queryArgs = new Bundle();
queryArgs.putInt(ContentResolver.QUERY_ARG_OFFSET, 0);
queryArgs.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
Cursor cursor = getContentResolver().query(
contentUri, // Content Uri is specific to individual content providers.
projection, // String[] describing which columns to return.
queryArgs, // Query arguments.
null); // Cancellation signal.
修改成上面的写法,引入了一个新的问题,就是limit条件不生效,设置为1,当时还是会将所有数据都会查询出来。
这就引入了另外一个问题,就不在这个文章里面写了,我就直接给出解决方案把,limit条件使用如下写法:可以解决问题
Uri limitedCallLogUri = CallLog.Calls.CONTENT_URI.buildUpon()
.appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1").build();
对于query通话数据库,设置limit参数不生效的问题,还是得看CallLogProvider源码,首先来看CallLogProvider里面的query方法,代码如下:
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Note don't use mCallingUid here. That's only used by mutation functions.
final int callingUid = Binder.getCallingUid();
mStats.incrementQueryStats(callingUid);
try {
return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
} finally {
mStats.finishOperation(callingUid);
}
}
这里面代码比较简单,主要看queryInternal()这个方法,F3进去,代码如下:
private Cursor queryInternal(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
queryForTesting(uri);
waitForAccess(mReadAccessLatch);
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
final int match = sURIMatcher.match(uri);
switch (match) {
case CALLS:
mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
break;
case CALLS_ID: {
mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
selectionBuilder.addClause(getEqualityClause(Calls._ID,
parseCallIdFromUri(uri)));
break;
}
case CALLS_FILTER: {
mzSetCallTableQueryArgs(uri, qb, selectionBuilder);
List<String> pathSegments = uri.getPathSegments();
String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
if (!TextUtils.isEmpty(phoneNumber)) {
qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
qb.appendWhereEscapeString(phoneNumber);
qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
} else {
qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
+ Calls.PRESENTATION_ALLOWED);
}
break;
}
case MZ_CALLS_SYNC:
qb.setTables(Tables.MZ_CALLS_SYNC);
qb.setProjectionMap(sCallsSyncProjectionMap);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
String limitClause = null;
if (limit > 0) {
limitClause = offset + "," + limit;
}
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor c = null;
try {
c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
null, sortOrder, limitClause);
}catch (SQLiteException e){
if (c!=null){
c.close();
}
e.printStackTrace();
}
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
}
return c;
}
这个方法里面,书要是对数据库查询的一些参数设置,这会问题出在limit,咱们主要来看limit参数的设置,从上面的代码中可以看出,在53行代码出,通过getIntParam获取了limit,咱们在进到getIntParam()方法中,代码如下:
/**
* Gets an integer query parameter from a given uri.
*
* @param uri The uri to extract the query parameter from.
* @param key The query parameter key.
* @param defaultValue A default value to return if the query parameter does not exist.
* @return The value from the query parameter in the Uri. Or the default value if the parameter
* does not exist in the uri.
* @throws IllegalArgumentException when the value in the query parameter is not an integer.
*/
private int getIntParam(Uri uri, String key, int defaultValue) {
String valueString = uri.getQueryParameter(key);
if (valueString == null) {
return defaultValue;
}
try {
return Integer.parseInt(valueString);
} catch (NumberFormatException e) {
String msg = "Integer required for " + key + " parameter but value '" + valueString +
"' was found instead.";
throw new IllegalArgumentException(msg, e);
}
}
在12行代码出,通过URI的getQueryParameter方法获取value,但是咱们在查询的时候并没有设置这个值,所以valueString会返回默认值0,回到queryInternal方法中,在56行代码出,只有limit>0才会设置limit,这样就知道为什么通过下面方法设置limit有效,而其他方法设置无效了:
Uri limitedCallLogUri = CallLog.Calls.CONTENT_URI.buildUpon()
.appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1").build();
总结
第一,关于直接用sql语句字符串设置limit的问题,这是因为谷歌在Android R上面对其做了限制,不能直接使用ql语句字符串,不然就会抛出异常
第二,关于limit参数设置问题,因为在CallLogProvider中,是通过getQueryParameter方法获取limit值的,如果通过其他方法设置,而不是通过appendQueryParameter方法设置,就会导致limit设置无效
献丑了,谢谢观看!!!!!!!!!!!