diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 1166cb57cca79..77a777024a211 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -132,6 +132,9 @@ public class DownloadManager { */ public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS; + /** {@hide} */ + public static final String COLUMN_FILE_NAME_HINT = Downloads.Impl.COLUMN_FILE_NAME_HINT; + /** * Provides more detail on the status of the download. Its meaning depends on the value of * {@link #COLUMN_STATUS}. @@ -173,6 +176,9 @@ public class DownloadManager { @TestApi public static final String COLUMN_MEDIASTORE_URI = Downloads.Impl.COLUMN_MEDIASTORE_URI; + /** {@hide} */ + public static final String COLUMN_DESTINATION = Downloads.Impl.COLUMN_DESTINATION; + /** * @hide */ @@ -340,26 +346,22 @@ public class DownloadManager { */ @UnsupportedAppUsage public static final String[] UNDERLYING_COLUMNS = new String[] { - Downloads.Impl._ID, - Downloads.Impl._DATA + " AS " + COLUMN_LOCAL_FILENAME, - Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, - Downloads.Impl.COLUMN_DESTINATION, - Downloads.Impl.COLUMN_TITLE, - Downloads.Impl.COLUMN_DESCRIPTION, - Downloads.Impl.COLUMN_URI, - Downloads.Impl.COLUMN_STATUS, - Downloads.Impl.COLUMN_FILE_NAME_HINT, - Downloads.Impl.COLUMN_MIME_TYPE + " AS " + COLUMN_MEDIA_TYPE, - Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + COLUMN_TOTAL_SIZE_BYTES, - Downloads.Impl.COLUMN_LAST_MODIFICATION + " AS " + COLUMN_LAST_MODIFIED_TIMESTAMP, - Downloads.Impl.COLUMN_CURRENT_BYTES + " AS " + COLUMN_BYTES_DOWNLOADED_SO_FAR, - Downloads.Impl.COLUMN_ALLOW_WRITE, - /* add the following 'computed' columns to the cursor. - * they are not 'returned' by the database, but their inclusion - * eliminates need to have lot of methods in CursorTranslator - */ - "'placeholder' AS " + COLUMN_LOCAL_URI, - "'placeholder' AS " + COLUMN_REASON + DownloadManager.COLUMN_ID, + DownloadManager.COLUMN_LOCAL_FILENAME, + DownloadManager.COLUMN_MEDIAPROVIDER_URI, + DownloadManager.COLUMN_DESTINATION, + DownloadManager.COLUMN_TITLE, + DownloadManager.COLUMN_DESCRIPTION, + DownloadManager.COLUMN_URI, + DownloadManager.COLUMN_STATUS, + DownloadManager.COLUMN_FILE_NAME_HINT, + DownloadManager.COLUMN_MEDIA_TYPE, + DownloadManager.COLUMN_TOTAL_SIZE_BYTES, + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP, + DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR, + DownloadManager.COLUMN_ALLOW_WRITE, + DownloadManager.COLUMN_LOCAL_URI, + DownloadManager.COLUMN_REASON }; /** diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java index 0ccd49f2e0284..5e530eedd818f 100644 --- a/core/java/android/app/slice/SliceProvider.java +++ b/core/java/android/app/slice/SliceProvider.java @@ -355,7 +355,8 @@ public final String getType(Uri uri) { @Override public Bundle call(String method, String arg, Bundle extras) { if (method.equals(METHOD_SLICE)) { - Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI)); + Uri uri = getUriWithoutUserId(validateIncomingUriOrNull( + extras.getParcelable(EXTRA_BIND_URI))); List supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); String callingPackage = getCallingPackage(); @@ -369,7 +370,7 @@ public Bundle call(String method, String arg, Bundle extras) { } else if (method.equals(METHOD_MAP_INTENT)) { Intent intent = extras.getParcelable(EXTRA_INTENT); if (intent == null) return null; - Uri uri = onMapIntentToUri(intent); + Uri uri = validateIncomingUriOrNull(onMapIntentToUri(intent)); List supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); Bundle b = new Bundle(); if (uri != null) { @@ -383,24 +384,27 @@ public Bundle call(String method, String arg, Bundle extras) { } else if (method.equals(METHOD_MAP_ONLY_INTENT)) { Intent intent = extras.getParcelable(EXTRA_INTENT); if (intent == null) return null; - Uri uri = onMapIntentToUri(intent); + Uri uri = validateIncomingUriOrNull(onMapIntentToUri(intent)); Bundle b = new Bundle(); b.putParcelable(EXTRA_SLICE, uri); return b; } else if (method.equals(METHOD_PIN)) { - Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI)); + Uri uri = getUriWithoutUserId(validateIncomingUriOrNull( + extras.getParcelable(EXTRA_BIND_URI))); if (Binder.getCallingUid() != Process.SYSTEM_UID) { throw new SecurityException("Only the system can pin/unpin slices"); } handlePinSlice(uri); } else if (method.equals(METHOD_UNPIN)) { - Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI)); + Uri uri = getUriWithoutUserId(validateIncomingUriOrNull( + extras.getParcelable(EXTRA_BIND_URI))); if (Binder.getCallingUid() != Process.SYSTEM_UID) { throw new SecurityException("Only the system can pin/unpin slices"); } handleUnpinSlice(uri); } else if (method.equals(METHOD_GET_DESCENDANTS)) { - Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI)); + Uri uri = getUriWithoutUserId( + validateIncomingUriOrNull(extras.getParcelable(EXTRA_BIND_URI))); Bundle b = new Bundle(); b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS, new ArrayList<>(handleGetDescendants(uri))); @@ -416,6 +420,10 @@ public Bundle call(String method, String arg, Bundle extras) { return super.call(method, arg, extras); } + private Uri validateIncomingUriOrNull(Uri uri) { + return uri == null ? null : validateIncomingUri(uri); + } + private Collection handleGetDescendants(Uri uri) { mCallback = "onGetSliceDescendants"; return onGetSliceDescendants(uri); diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 3523e956656ac..58901798b5f79 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -30,11 +30,14 @@ import android.util.ArrayMap; import android.util.Log; +import com.android.internal.util.ArrayUtils; + import libcore.util.EmptyArray; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -49,14 +52,11 @@ public class SQLiteQueryBuilder { private static final String TAG = "SQLiteQueryBuilder"; - private static final Pattern sLimitPattern = - Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); private static final Pattern sAggregationPattern = Pattern.compile( "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); private Map mProjectionMap = null; private List mProjectionGreylist = null; - private boolean mProjectionAggregationAllowed = false; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private String mTables = ""; @@ -65,7 +65,12 @@ public class SQLiteQueryBuilder { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; - private boolean mStrict; + + private static final int STRICT_PARENTHESES = 1 << 0; + private static final int STRICT_COLUMNS = 1 << 1; + private static final int STRICT_GRAMMAR = 1 << 2; + + private int mStrictFlags; public SQLiteQueryBuilder() { mDistinct = false; @@ -208,14 +213,23 @@ public void setProjectionGreylist(@Nullable List projectionGreylist) { return mProjectionGreylist; } - /** {@hide} */ + /** + * @deprecated Projection aggregation is now always allowed + * + * @hide + */ + @Deprecated public void setProjectionAggregationAllowed(boolean projectionAggregationAllowed) { - mProjectionAggregationAllowed = projectionAggregationAllowed; } - /** {@hide} */ + /** + * @deprecated Projection aggregation is now always allowed + * + * @hide + */ + @Deprecated public boolean isProjectionAggregationAllowed() { - return mProjectionAggregationAllowed; + return true; } /** @@ -258,8 +272,12 @@ public void setCursorFactory(@Nullable SQLiteDatabase.CursorFactory factory) { * * By default, this value is false. */ - public void setStrict(boolean flag) { - mStrict = flag; + public void setStrict(boolean strict) { + if (strict) { + mStrictFlags |= STRICT_PARENTHESES; + } else { + mStrictFlags &= ~STRICT_PARENTHESES; + } } /** @@ -267,7 +285,75 @@ public void setStrict(boolean flag) { * {@link #setStrict(boolean)}. */ public boolean isStrict() { - return mStrict; + return (mStrictFlags & STRICT_PARENTHESES) != 0; + } + + /** + * When enabled, verify that all projections and {@link ContentValues} only + * contain valid columns as defined by {@link #setProjectionMap(Map)}. + *

+ * This enforcement applies to {@link #insert}, {@link #query}, and + * {@link #update} operations. Any enforcement failures will throw an + * {@link IllegalArgumentException}. + * + * {@hide} + */ + public void setStrictColumns(boolean strictColumns) { + if (strictColumns) { + mStrictFlags |= STRICT_COLUMNS; + } else { + mStrictFlags &= ~STRICT_COLUMNS; + } + } + + /** + * Get if the query is marked as strict, as last configured by + * {@link #setStrictColumns(boolean)}. + * + * {@hide} + */ + public boolean isStrictColumns() { + return (mStrictFlags & STRICT_COLUMNS) != 0; + } + + /** + * When enabled, verify that all untrusted SQL conforms to a restricted SQL + * grammar. Here are the restrictions applied: + *

    + *
  • In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and + * windowing terms are rejected. + *
  • In {@code GROUP BY} clauses: only valid columns are allowed. + *
  • In {@code ORDER BY} clauses: only valid columns, collation, and + * ordering terms are allowed. + *
  • In {@code LIMIT} clauses: only numerical values and offset terms are + * allowed. + *
+ * All column references must be valid as defined by + * {@link #setProjectionMap(Map)}. + *

+ * This enforcement applies to {@link #query}, {@link #update} and + * {@link #delete} operations. This enforcement does not apply to trusted + * inputs, such as those provided by {@link #appendWhere}. Any enforcement + * failures will throw an {@link IllegalArgumentException}. + * + * {@hide} + */ + public void setStrictGrammar(boolean strictGrammar) { + if (strictGrammar) { + mStrictFlags |= STRICT_GRAMMAR; + } else { + mStrictFlags &= ~STRICT_GRAMMAR; + } + } + + /** + * Get if the query is marked as strict, as last configured by + * {@link #setStrictGrammar(boolean)}. + * + * {@hide} + */ + public boolean isStrictGrammar() { + return (mStrictFlags & STRICT_GRAMMAR) != 0; } /** @@ -303,9 +389,6 @@ public static String buildQueryString( throw new IllegalArgumentException( "HAVING clauses are only permitted when using a groupBy clause"); } - if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { - throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); - } StringBuilder query = new StringBuilder(120); @@ -479,7 +562,13 @@ public Cursor query(SQLiteDatabase db, String[] projectionIn, projectionIn, selection, groupBy, having, sortOrder, limit); - if (mStrict && selection != null && selection.length() > 0) { + 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 @@ -497,7 +586,7 @@ public Cursor query(SQLiteDatabase db, String[] projectionIn, // Execute wrapped query for extra protection final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, - having, sortOrder, limit); + wrap(having), sortOrder, limit); sql = wrappedSql; } else { // Execute unwrapped query @@ -518,6 +607,42 @@ public Cursor query(SQLiteDatabase db, String[] projectionIn, cancellationSignal); // will throw if query is invalid } + /** + * Perform an insert by combining all current settings and the + * information passed into this method. + * + * @param db the database to insert on + * @return the row ID of the newly inserted row, or -1 if an error occurred + * + * {@hide} + */ + public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) { + Objects.requireNonNull(mTables, "No tables defined"); + Objects.requireNonNull(db, "No database defined"); + Objects.requireNonNull(values, "No values defined"); + + if (isStrictColumns()) { + enforceStrictColumns(values); + } + + final String sql = buildInsert(values); + + final ArrayMap rawValues = values.getValues(); + final int valuesLength = rawValues.size(); + final Object[] sqlArgs = new Object[valuesLength]; + for (int i = 0; i < sqlArgs.length; i++) { + sqlArgs[i] = rawValues.valueAt(i); + } + 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.executeSql(sql, sqlArgs); + } + /** * Perform an update by combining all current settings and the * information passed into this method. @@ -541,7 +666,13 @@ public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values, final String sql; final String unwrappedSql = buildUpdate(values, selection); - if (mStrict) { + if (isStrictColumns()) { + enforceStrictColumns(values); + } + if (isStrictGrammar()) { + enforceStrictGrammar(selection, null, null, null, null); + } + 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 @@ -610,7 +741,10 @@ public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, final String sql; final String unwrappedSql = buildDelete(selection); - if (mStrict) { + if (isStrictGrammar()) { + enforceStrictGrammar(selection, null, null, null, null); + } + 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 @@ -645,6 +779,81 @@ public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, return db.executeSql(sql, sqlArgs); } + private void enforceStrictColumns(@Nullable String[] projection) { + Objects.requireNonNull(mProjectionMap, "No projection map defined"); + + computeProjection(projection); + } + + private void enforceStrictColumns(@NonNull ContentValues values) { + Objects.requireNonNull(mProjectionMap, "No projection map defined"); + + final ArrayMap rawValues = values.getValues(); + for (int i = 0; i < rawValues.size(); i++) { + final String column = rawValues.keyAt(i); + if (!mProjectionMap.containsKey(column)) { + throw new IllegalArgumentException("Invalid column " + column); + } + } + } + + private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, + @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { + SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, + this::enforceStrictGrammarWhereHaving); + SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, + this::enforceStrictGrammarGroupBy); + SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, + this::enforceStrictGrammarWhereHaving); + SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, + this::enforceStrictGrammarOrderBy); + SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, + this::enforceStrictGrammarLimit); + } + + private void enforceStrictGrammarWhereHaving(@NonNull String token) { + if (isTableOrColumn(token)) return; + if (SQLiteTokenizer.isFunction(token)) return; + if (SQLiteTokenizer.isType(token)) return; + + // NOTE: we explicitly don't allow SELECT subqueries, since they could + // leak data that should have been filtered by the trusted where clause + switch (token.toUpperCase(Locale.US)) { + case "AND": case "AS": case "BETWEEN": case "BINARY": + case "CASE": case "CAST": case "COLLATE": case "DISTINCT": + case "ELSE": case "END": case "ESCAPE": case "EXISTS": + case "GLOB": case "IN": case "IS": case "ISNULL": + case "LIKE": case "MATCH": case "NOCASE": case "NOT": + case "NOTNULL": case "NULL": case "OR": case "REGEXP": + case "RTRIM": case "THEN": case "WHEN": + return; + } + throw new IllegalArgumentException("Invalid token " + token); + } + + private void enforceStrictGrammarGroupBy(@NonNull String token) { + if (isTableOrColumn(token)) return; + throw new IllegalArgumentException("Invalid token " + token); + } + + private void enforceStrictGrammarOrderBy(@NonNull String token) { + if (isTableOrColumn(token)) return; + switch (token.toUpperCase(Locale.US)) { + case "COLLATE": case "ASC": case "DESC": + case "BINARY": case "RTRIM": case "NOCASE": + return; + } + throw new IllegalArgumentException("Invalid token " + token); + } + + private void enforceStrictGrammarLimit(@NonNull String token) { + switch (token.toUpperCase(Locale.US)) { + case "OFFSET": + return; + } + throw new IllegalArgumentException("Invalid token " + token); + } + /** * Construct a {@code SELECT} statement suitable for use in a group of * {@code SELECT} statements that will be joined through {@code UNION} operators @@ -697,6 +906,35 @@ public String buildQuery( return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit); } + /** {@hide} */ + public String buildInsert(ContentValues values) { + if (values == null || values.isEmpty()) { + throw new IllegalArgumentException("Empty values"); + } + + StringBuilder sql = new StringBuilder(120); + sql.append("INSERT INTO "); + sql.append(SQLiteDatabase.findEditTable(mTables)); + sql.append(" ("); + + final ArrayMap rawValues = values.getValues(); + for (int i = 0; i < rawValues.size(); i++) { + if (i > 0) { + sql.append(','); + } + sql.append(rawValues.keyAt(i)); + } + sql.append(") VALUES ("); + for (int i = 0; i < rawValues.size(); i++) { + if (i > 0) { + sql.append(','); + } + sql.append('?'); + } + sql.append(")"); + return sql.toString(); + } + /** {@hide} */ public String buildUpdate(ContentValues values, String selection) { if (values == null || values.isEmpty()) { @@ -705,7 +943,7 @@ public String buildUpdate(ContentValues values, String selection) { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); - sql.append(mTables); + sql.append(SQLiteDatabase.findEditTable(mTables)); sql.append(" SET "); final ArrayMap rawValues = values.getValues(); @@ -726,7 +964,7 @@ public String buildUpdate(ContentValues values, String selection) { public String buildDelete(String selection) { StringBuilder sql = new StringBuilder(120); sql.append("DELETE FROM "); - sql.append(mTables); + sql.append(SQLiteDatabase.findEditTable(mTables)); final String where = computeWhere(selection); appendClause(sql, " WHERE ", where); @@ -868,65 +1106,13 @@ public String buildUnionQuery(String[] subQueries, String sortOrder, String limi /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public String[] computeProjection(String[] projectionIn) { - if (projectionIn != null && projectionIn.length > 0) { - if (mProjectionMap != null) { - String[] projection = new String[projectionIn.length]; - int length = projectionIn.length; - - for (int i = 0; i < length; i++) { - String operator = null; - String userColumn = projectionIn[i]; - String column = mProjectionMap.get(userColumn); - - // If aggregation is allowed, extract the underlying column - // that may be aggregated - if (mProjectionAggregationAllowed) { - final Matcher matcher = sAggregationPattern.matcher(userColumn); - if (matcher.matches()) { - operator = matcher.group(1); - userColumn = matcher.group(2); - column = mProjectionMap.get(userColumn); - } - } - - if (column != null) { - projection[i] = maybeWithOperator(operator, column); - continue; - } - - if (!mStrict && - ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { - /* A column alias already exist */ - projection[i] = maybeWithOperator(operator, userColumn); - continue; - } - - // If greylist is configured, we might be willing to let - // this custom column bypass our strict checks. - if (mProjectionGreylist != null) { - boolean match = false; - for (Pattern p : mProjectionGreylist) { - if (p.matcher(userColumn).matches()) { - match = true; - break; - } - } - - if (match) { - Log.w(TAG, "Allowing abusive custom column: " + userColumn); - projection[i] = maybeWithOperator(operator, userColumn); - continue; - } - } - - throw new IllegalArgumentException("Invalid column " - + projectionIn[i]); - } - return projection; - } else { - return projectionIn; + public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { + if (!ArrayUtils.isEmpty(projectionIn)) { + String[] projectionOut = new String[projectionIn.length]; + for (int i = 0; i < projectionIn.length; i++) { + projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); } + return projectionOut; } else if (mProjectionMap != null) { // Return all columns in projection map. Set> entrySet = mProjectionMap.entrySet(); @@ -948,6 +1134,69 @@ public String[] computeProjection(String[] projectionIn) { return null; } + private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { + final String column = computeSingleProjection(userColumn); + if (column != null) { + return column; + } else { + throw new IllegalArgumentException("Invalid column " + userColumn); + } + } + + private @Nullable String computeSingleProjection(@NonNull String userColumn) { + // When no mapping provided, anything goes + if (mProjectionMap == null) { + return userColumn; + } + + String operator = null; + String column = mProjectionMap.get(userColumn); + + // When no direct match found, look for aggregation + if (column == null) { + final Matcher matcher = sAggregationPattern.matcher(userColumn); + if (matcher.matches()) { + operator = matcher.group(1); + userColumn = matcher.group(2); + column = mProjectionMap.get(userColumn); + } + } + + if (column != null) { + return maybeWithOperator(operator, column); + } + + if (mStrictFlags == 0 + && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { + /* A column alias already exist */ + return maybeWithOperator(operator, userColumn); + } + + // If greylist is configured, we might be willing to let + // this custom column bypass our strict checks. + if (mProjectionGreylist != null) { + boolean match = false; + for (Pattern p : mProjectionGreylist) { + if (p.matcher(userColumn).matches()) { + match = true; + break; + } + } + + if (match) { + Log.w(TAG, "Allowing abusive custom column: " + userColumn); + return maybeWithOperator(operator, userColumn); + } + } + + return null; + } + + private boolean isTableOrColumn(String token) { + if (mTables.equals(token)) return true; + return computeSingleProjection(token) != null; + } + /** {@hide} */ public @Nullable String computeWhere(@Nullable String selection) { final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); diff --git a/core/java/android/database/sqlite/SQLiteTokenizer.java b/core/java/android/database/sqlite/SQLiteTokenizer.java new file mode 100644 index 0000000000000..7e7c3fb976c70 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteTokenizer.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +/** + * SQL Tokenizer specialized to extract tokens from SQL (snippets). + *

+ * Based on sqlite3GetToken() in tokenzie.c in SQLite. + *

+ * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7 + * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922) + *

+ * Also draft spec: http://www.sqlite.org/draft/tokenreq.html + * + * @hide + */ +public class SQLiteTokenizer { + private static boolean isAlpha(char ch) { + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_'); + } + + private static boolean isNum(char ch) { + return ('0' <= ch && ch <= '9'); + } + + private static boolean isAlNum(char ch) { + return isAlpha(ch) || isNum(ch); + } + + private static boolean isAnyOf(char ch, String set) { + return set.indexOf(ch) >= 0; + } + + private static IllegalArgumentException genException(String message, String sql) { + throw new IllegalArgumentException(message + " in '" + sql + "'"); + } + + private static char peek(String s, int index) { + return index < s.length() ? s.charAt(index) : '\0'; + } + + public static final int OPTION_NONE = 0; + + /** + * Require that SQL contains only tokens; any comments or values will result + * in an exception. + */ + public static final int OPTION_TOKEN_ONLY = 1 << 0; + + /** + * Tokenize the given SQL, returning the list of each encountered token. + * + * @throws IllegalArgumentException if invalid SQL is encountered. + */ + public static List tokenize(@Nullable String sql, int options) { + final ArrayList res = new ArrayList<>(); + tokenize(sql, options, res::add); + return res; + } + + /** + * Tokenize the given SQL, sending each encountered token to the given + * {@link Consumer}. + * + * @throws IllegalArgumentException if invalid SQL is encountered. + */ + public static void tokenize(@Nullable String sql, int options, Consumer checker) { + if (sql == null) { + return; + } + int pos = 0; + final int len = sql.length(); + while (pos < len) { + final char ch = peek(sql, pos); + + // Regular token. + if (isAlpha(ch)) { + final int start = pos; + pos++; + while (isAlNum(peek(sql, pos))) { + pos++; + } + final int end = pos; + + final String token = sql.substring(start, end); + checker.accept(token); + + continue; + } + + // Handle quoted tokens + if (isAnyOf(ch, "'\"`")) { + final int quoteStart = pos; + pos++; + + for (;;) { + pos = sql.indexOf(ch, pos); + if (pos < 0) { + throw genException("Unterminated quote", sql); + } + if (peek(sql, pos + 1) != ch) { + break; + } + // Quoted quote char -- e.g. "abc""def" is a single string. + pos += 2; + } + final int quoteEnd = pos; + pos++; + + if (ch != '\'') { + // Extract the token + final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd); + + final String token; + + // Unquote if needed. i.e. "aa""bb" -> aa"bb + if (tokenUnquoted.indexOf(ch) >= 0) { + token = tokenUnquoted.replaceAll( + String.valueOf(ch) + ch, String.valueOf(ch)); + } else { + token = tokenUnquoted; + } + checker.accept(token); + } else { + if ((options &= OPTION_TOKEN_ONLY) != 0) { + throw genException("Non-token detected", sql); + } + } + continue; + } + // Handle tokens enclosed in [...] + if (ch == '[') { + final int quoteStart = pos; + pos++; + + pos = sql.indexOf(']', pos); + if (pos < 0) { + throw genException("Unterminated quote", sql); + } + final int quoteEnd = pos; + pos++; + + final String token = sql.substring(quoteStart + 1, quoteEnd); + + checker.accept(token); + continue; + } + if ((options &= OPTION_TOKEN_ONLY) != 0) { + throw genException("Non-token detected", sql); + } + + // Detect comments. + if (ch == '-' && peek(sql, pos + 1) == '-') { + pos += 2; + pos = sql.indexOf('\n', pos); + if (pos < 0) { + // We disallow strings ending in an inline comment. + throw genException("Unterminated comment", sql); + } + pos++; + + continue; + } + if (ch == '/' && peek(sql, pos + 1) == '*') { + pos += 2; + pos = sql.indexOf("*/", pos); + if (pos < 0) { + throw genException("Unterminated comment", sql); + } + pos += 2; + + continue; + } + + // Semicolon is never allowed. + if (ch == ';') { + throw genException("Semicolon is not allowed", sql); + } + + // For this purpose, we can simply ignore other characters. + // (Note it doesn't handle the X'' literal properly and reports this X as a token, + // but that should be fine...) + pos++; + } + } + + /** + * Test if given token is a + * SQLite reserved + * keyword. + */ + public static boolean isKeyword(@NonNull String token) { + switch (token.toUpperCase(Locale.US)) { + case "ABORT": case "ACTION": case "ADD": case "AFTER": + case "ALL": case "ALTER": case "ANALYZE": case "AND": + case "AS": case "ASC": case "ATTACH": case "AUTOINCREMENT": + case "BEFORE": case "BEGIN": case "BETWEEN": case "BINARY": + case "BY": case "CASCADE": case "CASE": case "CAST": + case "CHECK": case "COLLATE": case "COLUMN": case "COMMIT": + case "CONFLICT": case "CONSTRAINT": case "CREATE": case "CROSS": + case "CURRENT": case "CURRENT_DATE": case "CURRENT_TIME": case "CURRENT_TIMESTAMP": + case "DATABASE": case "DEFAULT": case "DEFERRABLE": case "DEFERRED": + case "DELETE": case "DESC": case "DETACH": case "DISTINCT": + case "DO": case "DROP": case "EACH": case "ELSE": + case "END": case "ESCAPE": case "EXCEPT": case "EXCLUDE": + case "EXCLUSIVE": case "EXISTS": case "EXPLAIN": case "FAIL": + case "FILTER": case "FOLLOWING": case "FOR": case "FOREIGN": + case "FROM": case "FULL": case "GLOB": case "GROUP": + case "GROUPS": case "HAVING": case "IF": case "IGNORE": + case "IMMEDIATE": case "IN": case "INDEX": case "INDEXED": + case "INITIALLY": case "INNER": case "INSERT": case "INSTEAD": + case "INTERSECT": case "INTO": case "IS": case "ISNULL": + case "JOIN": case "KEY": case "LEFT": case "LIKE": + case "LIMIT": case "MATCH": case "NATURAL": case "NO": + case "NOCASE": case "NOT": case "NOTHING": case "NOTNULL": + case "NULL": case "OF": case "OFFSET": case "ON": + case "OR": case "ORDER": case "OTHERS": case "OUTER": + case "OVER": case "PARTITION": case "PLAN": case "PRAGMA": + case "PRECEDING": case "PRIMARY": case "QUERY": case "RAISE": + case "RANGE": case "RECURSIVE": case "REFERENCES": case "REGEXP": + case "REINDEX": case "RELEASE": case "RENAME": case "REPLACE": + case "RESTRICT": case "RIGHT": case "ROLLBACK": case "ROW": + case "ROWS": case "RTRIM": case "SAVEPOINT": case "SELECT": + case "SET": case "TABLE": case "TEMP": case "TEMPORARY": + case "THEN": case "TIES": case "TO": case "TRANSACTION": + case "TRIGGER": case "UNBOUNDED": case "UNION": case "UNIQUE": + case "UPDATE": case "USING": case "VACUUM": case "VALUES": + case "VIEW": case "VIRTUAL": case "WHEN": case "WHERE": + case "WINDOW": case "WITH": case "WITHOUT": + return true; + default: + return false; + } + } + + /** + * Test if given token is a + * SQLite reserved + * function. + */ + public static boolean isFunction(@NonNull String token) { + switch (token.toLowerCase(Locale.US)) { + case "abs": case "avg": case "char": case "coalesce": + case "count": case "glob": case "group_concat": case "hex": + case "ifnull": case "instr": case "length": case "like": + case "likelihood": case "likely": case "lower": case "ltrim": + case "max": case "min": case "nullif": case "random": + case "randomblob": case "replace": case "round": case "rtrim": + case "substr": case "sum": case "total": case "trim": + case "typeof": case "unicode": case "unlikely": case "upper": + case "zeroblob": + return true; + default: + return false; + } + } + + /** + * Test if given token is a + * SQLite reserved type. + */ + public static boolean isType(@NonNull String token) { + switch (token.toUpperCase(Locale.US)) { + case "INT": case "INTEGER": case "TINYINT": case "SMALLINT": + case "MEDIUMINT": case "BIGINT": case "INT2": case "INT8": + case "CHARACTER": case "VARCHAR": case "NCHAR": case "NVARCHAR": + case "TEXT": case "CLOB": case "BLOB": case "REAL": + case "DOUBLE": case "FLOAT": case "NUMERIC": case "DECIMAL": + case "BOOLEAN": case "DATE": case "DATETIME": + return true; + default: + return false; + } + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a86d63d31ad46..1d92644fcfda5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5951,6 +5951,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val @Deprecated public static final String DEVICE_PROVISIONED = Global.DEVICE_PROVISIONED; + /** + * Indicates whether a DPC has been downloaded during provisioning. + * + *

Type: int (0 for false, 1 for true) + * + *

If this is true, then any attempts to begin setup again should result in factory reset + * + * @hide + */ + public static final String MANAGED_PROVISIONING_DPC_DOWNLOADED = + "managed_provisioning_dpc_downloaded"; + /** * Indicates whether the current user has completed setup via the setup wizard. *

diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteTokenizerTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteTokenizerTest.java new file mode 100644 index 0000000000000..a9d148289262b --- /dev/null +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteTokenizerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class SQLiteTokenizerTest { + private List getTokens(String sql) { + return SQLiteTokenizer.tokenize(sql, SQLiteTokenizer.OPTION_NONE); + } + + private void checkTokens(String sql, String spaceSeparatedExpectedTokens) { + final List expected = spaceSeparatedExpectedTokens == null + ? new ArrayList<>() + : Arrays.asList(spaceSeparatedExpectedTokens.split(" +")); + + assertEquals(expected, getTokens(sql)); + } + + private void assertInvalidSql(String sql, String message) { + try { + getTokens(sql); + fail("Didn't throw InvalidSqlException"); + } catch (IllegalArgumentException e) { + assertTrue("Expected " + e.getMessage() + " to contain " + message, + e.getMessage().contains(message)); + } + } + + @Test + public void testWhitespaces() { + checkTokens(" select \t\r\n a\n\n ", "select a"); + checkTokens("a b", "a b"); + } + + @Test + public void testComment() { + checkTokens("--\n", null); + checkTokens("a--\n", "a"); + checkTokens("a--abcdef\n", "a"); + checkTokens("a--abcdef\nx", "a x"); + checkTokens("a--\nx", "a x"); + assertInvalidSql("a--abcdef", "Unterminated comment"); + assertInvalidSql("a--abcdef\ndef--", "Unterminated comment"); + + checkTokens("/**/", null); + assertInvalidSql("/*", "Unterminated comment"); + assertInvalidSql("/*/", "Unterminated comment"); + assertInvalidSql("/*\n* /*a", "Unterminated comment"); + checkTokens("a/**/", "a"); + checkTokens("/**/b", "b"); + checkTokens("a/**/b", "a b"); + checkTokens("a/* -- \n* /* **/b", "a b"); + } + + @Test + public void testStrings() { + assertInvalidSql("'", "Unterminated quote"); + assertInvalidSql("a'", "Unterminated quote"); + assertInvalidSql("a'''", "Unterminated quote"); + assertInvalidSql("a''' ", "Unterminated quote"); + checkTokens("''", null); + checkTokens("''''", null); + checkTokens("a''''b", "a b"); + checkTokens("a' '' 'b", "a b"); + checkTokens("'abc'", null); + checkTokens("'abc\ndef'", null); + checkTokens("a'abc\ndef'", "a"); + checkTokens("'abc\ndef'b", "b"); + checkTokens("a'abc\ndef'b", "a b"); + checkTokens("a'''abc\nd''ef'''b", "a b"); + } + + @Test + public void testDoubleQuotes() { + assertInvalidSql("\"", "Unterminated quote"); + assertInvalidSql("a\"", "Unterminated quote"); + assertInvalidSql("a\"\"\"", "Unterminated quote"); + assertInvalidSql("a\"\"\" ", "Unterminated quote"); + checkTokens("\"\"", ""); + checkTokens("\"\"\"\"", "\""); + checkTokens("a\"\"\"\"b", "a \" b"); + checkTokens("a\"\t\"\"\t\"b", "a \t\"\t b"); + checkTokens("\"abc\"", "abc"); + checkTokens("\"abc\ndef\"", "abc\ndef"); + checkTokens("a\"abc\ndef\"", "a abc\ndef"); + checkTokens("\"abc\ndef\"b", "abc\ndef b"); + checkTokens("a\"abc\ndef\"b", "a abc\ndef b"); + checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", "a \"abc\nd\"ef\" b"); + } + + @Test + public void testBackQuotes() { + assertInvalidSql("`", "Unterminated quote"); + assertInvalidSql("a`", "Unterminated quote"); + assertInvalidSql("a```", "Unterminated quote"); + assertInvalidSql("a``` ", "Unterminated quote"); + checkTokens("``", ""); + checkTokens("````", "`"); + checkTokens("a````b", "a ` b"); + checkTokens("a`\t``\t`b", "a \t`\t b"); + checkTokens("`abc`", "abc"); + checkTokens("`abc\ndef`", "abc\ndef"); + checkTokens("a`abc\ndef`", "a abc\ndef"); + checkTokens("`abc\ndef`b", "abc\ndef b"); + checkTokens("a`abc\ndef`b", "a abc\ndef b"); + checkTokens("a```abc\nd``ef```b", "a `abc\nd`ef` b"); + } + + @Test + public void testBrackets() { + assertInvalidSql("[", "Unterminated quote"); + assertInvalidSql("a[", "Unterminated quote"); + assertInvalidSql("a[ ", "Unterminated quote"); + assertInvalidSql("a[[ ", "Unterminated quote"); + checkTokens("[]", ""); + checkTokens("[[]", "["); + checkTokens("a[[]b", "a [ b"); + checkTokens("a[\t[\t]b", "a \t[\t b"); + checkTokens("[abc]", "abc"); + checkTokens("[abc\ndef]", "abc\ndef"); + checkTokens("a[abc\ndef]", "a abc\ndef"); + checkTokens("[abc\ndef]b", "abc\ndef b"); + checkTokens("a[abc\ndef]b", "a abc\ndef b"); + checkTokens("a[[abc\nd[ef[]b", "a [abc\nd[ef[ b"); + } + + @Test + public void testSemicolons() { + assertInvalidSql(";", "Semicolon is not allowed"); + assertInvalidSql(" ;", "Semicolon is not allowed"); + assertInvalidSql("; ", "Semicolon is not allowed"); + assertInvalidSql("-;-", "Semicolon is not allowed"); + checkTokens("--;\n", null); + checkTokens("/*;*/", null); + checkTokens("';'", null); + checkTokens("[;]", ";"); + checkTokens("`;`", ";"); + } + + @Test + public void testTokens() { + checkTokens("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef"); + checkTokens("a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f", + "a abc a00b _1 _123 abc d e f"); + } +} diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index cd36ba746a391..2bc150a564b21 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -714,6 +714,7 @@ public class SettingsBackupTest { Settings.Secure.PACKAGES_TO_CLEAR_DATA_BEFORE_FULL_RESTORE, Settings.Secure.FLASHLIGHT_AVAILABLE, Settings.Secure.FLASHLIGHT_ENABLED, + Settings.Secure.MANAGED_PROVISIONING_DPC_DOWNLOADED, Settings.Secure.CROSS_PROFILE_CALENDAR_ENABLED, Settings.Secure.LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS, diff --git a/data/keyboards/Vendor_045e_Product_02dd.kl b/data/keyboards/Vendor_045e_Product_02dd.kl new file mode 100644 index 0000000000000..3975cec24fcb9 --- /dev/null +++ b/data/keyboards/Vendor_045e_Product_02dd.kl @@ -0,0 +1,57 @@ +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# XBox One Controller - Model 1697 - USB +# + +# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html + +key 304 BUTTON_A +key 305 BUTTON_B +key 307 BUTTON_X +key 308 BUTTON_Y + +key 310 BUTTON_L1 +key 311 BUTTON_R1 + +# Triggers. +axis 0x02 LTRIGGER +axis 0x05 RTRIGGER + +# Left and right stick. +# The reported value for flat is 128 out of a range from -32767 to 32768, which is absurd. +# This confuses applications that rely on the flat value because the joystick actually +# settles in a flat range of +/- 4096 or so. +axis 0x00 X flat 4096 +axis 0x01 Y flat 4096 +axis 0x03 Z flat 4096 +axis 0x04 RZ flat 4096 + +key 317 BUTTON_THUMBL +key 318 BUTTON_THUMBR + +# Hat. +axis 0x10 HAT_X +axis 0x11 HAT_Y + + +# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt +# Two overlapping rectangles +key 314 BUTTON_SELECT +# Hamburger - 3 parallel lines +key 315 BUTTON_START + +# Xbox key +key 316 BUTTON_MODE diff --git a/data/keyboards/Vendor_045e_Product_02fd.kl b/data/keyboards/Vendor_045e_Product_02fd.kl index 512f7e1349780..1b03497ae3d12 100644 --- a/data/keyboards/Vendor_045e_Product_02fd.kl +++ b/data/keyboards/Vendor_045e_Product_02fd.kl @@ -53,5 +53,10 @@ key 158 BUTTON_SELECT # Hamburger - 3 parallel lines key 315 BUTTON_START -# Xbox key +# There are at least two versions of firmware out for this controller. +# They send different linux keys for the "Xbox" button. +# Xbox key (original firmware) key 172 BUTTON_MODE + +# Xbox key (newer firmware) +key 316 BUTTON_MODE diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 1789c5caf24ac..a01e6e8b9c75b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -27,6 +27,7 @@ import android.os.ParcelUuid; import android.os.SystemClock; import android.text.TextUtils; +import android.util.EventLog; import android.util.Log; import android.os.SystemProperties; @@ -852,10 +853,9 @@ private void processPhonebookAccess() { == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || mDevice.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) { - mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); - } else { - mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); + EventLog.writeEvent(0x534e4554, "138529441", -1, ""); } + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java index 01498e6bd54da..6fc265e6f983c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitor.java @@ -19,7 +19,6 @@ public interface KeyguardMonitor extends CallbackController { boolean isSecure(); - boolean canSkipBouncer(); boolean isShowing(); boolean isOccluded(); boolean isKeyguardFadingAway(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java index b53ff0e45cea7..2b08d68f1072e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardMonitorImpl.java @@ -17,13 +17,11 @@ package com.android.systemui.statusbar.policy; import android.annotation.NonNull; -import android.app.ActivityManager; import android.content.Context; import com.android.internal.util.Preconditions; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.systemui.settings.CurrentUserTracker; import java.util.ArrayList; @@ -39,14 +37,11 @@ public class KeyguardMonitorImpl extends KeyguardUpdateMonitorCallback private final ArrayList mCallbacks = new ArrayList<>(); private final Context mContext; - private final CurrentUserTracker mUserTracker; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private int mCurrentUser; private boolean mShowing; private boolean mSecure; private boolean mOccluded; - private boolean mCanSkipBouncer; private boolean mListening; private boolean mKeyguardFadingAway; @@ -61,13 +56,6 @@ public class KeyguardMonitorImpl extends KeyguardUpdateMonitorCallback public KeyguardMonitorImpl(Context context) { mContext = context; mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); - mUserTracker = new CurrentUserTracker(mContext) { - @Override - public void onUserSwitched(int newUserId) { - mCurrentUser = newUserId; - updateCanSkipBouncerState(); - } - }; } @Override @@ -76,10 +64,7 @@ public void addCallback(@NonNull Callback callback) { mCallbacks.add(callback); if (mCallbacks.size() != 0 && !mListening) { mListening = true; - mCurrentUser = ActivityManager.getCurrentUser(); - updateCanSkipBouncerState(); mKeyguardUpdateMonitor.registerCallback(this); - mUserTracker.startTracking(); } } @@ -89,7 +74,6 @@ public void removeCallback(@NonNull Callback callback) { if (mCallbacks.remove(callback) && mCallbacks.size() == 0 && mListening) { mListening = false; mKeyguardUpdateMonitor.removeCallback(this); - mUserTracker.stopTracking(); } } @@ -108,11 +92,6 @@ public boolean isOccluded() { return mOccluded; } - @Override - public boolean canSkipBouncer() { - return mCanSkipBouncer; - } - public void notifyKeyguardState(boolean showing, boolean secure, boolean occluded) { if (mShowing == showing && mSecure == secure && mOccluded == occluded) return; mShowing = showing; @@ -123,7 +102,6 @@ public void notifyKeyguardState(boolean showing, boolean secure, boolean occlude @Override public void onTrustChanged(int userId) { - updateCanSkipBouncerState(); notifyKeyguardChanged(); } @@ -131,10 +109,6 @@ public boolean isDeviceInteractive() { return mKeyguardUpdateMonitor.isDeviceInteractive(); } - private void updateCanSkipBouncerState() { - mCanSkipBouncer = mKeyguardUpdateMonitor.getUserCanSkipBouncer(mCurrentUser); - } - private void notifyKeyguardChanged() { // Copy the list to allow removal during callback. new ArrayList<>(mCallbacks).forEach(Callback::onKeyguardShowingChanged); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 395add76dda49..35e3923f285b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -61,6 +61,7 @@ import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.statusbar.phone.UnlockMethodCache; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -595,17 +596,19 @@ public static abstract class BaseUserAdapter extends BaseAdapter { final UserSwitcherController mController; private final KeyguardMonitor mKeyguardMonitor; + private final UnlockMethodCache mUnlockMethodCache; protected BaseUserAdapter(UserSwitcherController controller) { mController = controller; mKeyguardMonitor = controller.mKeyguardMonitor; + mUnlockMethodCache = UnlockMethodCache.getInstance(controller.mContext); controller.addAdapter(new WeakReference<>(this)); } public int getUserCount() { boolean secureKeyguardShowing = mKeyguardMonitor.isShowing() && mKeyguardMonitor.isSecure() - && !mKeyguardMonitor.canSkipBouncer(); + && !mUnlockMethodCache.canSkipBouncer(); if (!secureKeyguardShowing) { return mController.getUsers().size(); } @@ -627,7 +630,7 @@ public int getUserCount() { public int getCount() { boolean secureKeyguardShowing = mKeyguardMonitor.isShowing() && mKeyguardMonitor.isSecure() - && !mKeyguardMonitor.canSkipBouncer(); + && !mUnlockMethodCache.canSkipBouncer(); if (!secureKeyguardShowing) { return mController.getUsers().size(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardMonitor.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardMonitor.java index 95c7a4d09f928..2fb0e0e7caf8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardMonitor.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardMonitor.java @@ -80,9 +80,4 @@ public long getKeyguardFadingAwayDelay() { public long calculateGoingToFullShadeDelay() { return 0; } - - @Override - public boolean canSkipBouncer() { - return false; - } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 678bbe560a233..5cfdf6086bf0e 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -506,6 +506,11 @@ private int createSessionInternal(SessionParams params, String installerPackageN params.installFlags &= ~PackageManager.INSTALL_REQUEST_DOWNGRADE; } + if (callingUid != Process.SYSTEM_UID) { + // Only system_server can use INSTALL_DISABLE_VERIFICATION. + params.installFlags &= ~PackageManager.INSTALL_DISABLE_VERIFICATION; + } + boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0; if (params.isStaged || isApex) { mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, TAG); diff --git a/services/core/java/com/android/server/power/AttentionDetector.java b/services/core/java/com/android/server/power/AttentionDetector.java index ed11fd45ec39b..8004efb68ac55 100644 --- a/services/core/java/com/android/server/power/AttentionDetector.java +++ b/services/core/java/com/android/server/power/AttentionDetector.java @@ -156,7 +156,7 @@ public void systemReady(Context context) { context.getContentResolver().registerContentObserver(Settings.System.getUriFor( Settings.System.ADAPTIVE_SLEEP), - false, new ContentObserver(new Handler()) { + false, new ContentObserver(new Handler(context.getMainLooper())) { @Override public void onChange(boolean selfChange) { updateEnabledFromSettings(context); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index cfd3ae6ef5943..aa49ba62f48b1 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -2724,6 +2724,14 @@ private boolean needDisplaySuspendBlockerLocked() { return true; } } + + if (mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_DOZE + && mDisplayPowerRequest.dozeScreenState == Display.STATE_ON) { + // Although we are in DOZE and would normally allow the device to suspend, + // the doze service has explicitly requested the display to remain in the ON + // state which means we should hold the display suspend blocker. + return true; + } if (mScreenBrightnessBoostInProgress) { return true; } @@ -4858,7 +4866,8 @@ private PowerManager.WakeData getLastWakeupInternal() { } } - private final class LocalService extends PowerManagerInternal { + @VisibleForTesting + final class LocalService extends PowerManagerInternal { @Override public void setScreenBrightnessOverrideFromWindowManager(int screenBrightness) { if (screenBrightness < PowerManager.BRIGHTNESS_DEFAULT diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java index 1bda412f2f89d..88de250e4b0da 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java @@ -23,7 +23,10 @@ import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; @@ -59,6 +62,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; +import android.view.Display; import androidx.test.InstrumentationRegistry; @@ -157,6 +161,10 @@ public void setUp() throws Exception { mResourcesSpy = spy(mContextSpy.getResources()); when(mContextSpy.getResources()).thenReturn(mResourcesSpy); + when(mDisplayManagerInternalMock.requestPowerState(any(), anyBoolean())).thenReturn(true); + } + + private PowerManagerService createService() { mService = new PowerManagerService(mContextSpy, new Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, @@ -166,7 +174,7 @@ Notifier createNotifier(Looper looper, Context context, IBatteryStats batterySta @Override SuspendBlocker createSuspendBlocker(PowerManagerService service, String name) { - return mock(SuspendBlocker.class); + return super.createSuspendBlocker(service, name); } @Override @@ -191,6 +199,7 @@ AmbientDisplayConfiguration createAmbientDisplayConfiguration(Context context) { return mAmbientDisplayConfigurationMock; } }); + return mService; } @After @@ -262,6 +271,7 @@ private void setPluggedIn(boolean isPluggedIn) { @Test public void testUpdatePowerScreenPolicy_UpdateDisplayPowerRequest() { + createService(); mService.updatePowerRequestFromBatterySaverPolicy(mDisplayPowerRequest); assertThat(mDisplayPowerRequest.lowPowerMode).isEqualTo(BATTERY_SAVER_ENABLED); assertThat(mDisplayPowerRequest.screenLowPowerBrightnessFactor) @@ -270,6 +280,7 @@ public void testUpdatePowerScreenPolicy_UpdateDisplayPowerRequest() { @Test public void testGetLastShutdownReasonInternal() { + createService(); SystemProperties.set(TEST_LAST_REBOOT_PROPERTY, "shutdown,thermal"); int reason = mService.getLastShutdownReasonInternal(TEST_LAST_REBOOT_PROPERTY); SystemProperties.set(TEST_LAST_REBOOT_PROPERTY, ""); @@ -278,6 +289,7 @@ public void testGetLastShutdownReasonInternal() { @Test public void testGetDesiredScreenPolicy_WithVR() throws Exception { + createService(); // Brighten up the screen mService.setWakefulnessLocked(WAKEFULNESS_AWAKE, PowerManager.WAKE_REASON_UNKNOWN, 0); assertThat(mService.getDesiredScreenPolicyLocked()).isEqualTo( @@ -307,11 +319,13 @@ public void testGetDesiredScreenPolicy_WithVR() throws Exception { @Test public void testWakefulnessAwake_InitialValue() throws Exception { + createService(); assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_AWAKE); } @Test public void testWakefulnessSleep_NoDozeSleepFlag() throws Exception { + createService(); // Start with AWAKE state startSystem(); assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_AWAKE); @@ -324,6 +338,7 @@ public void testWakefulnessSleep_NoDozeSleepFlag() throws Exception { @Test public void testWakefulnessAwake_AcquireCausesWakeup() throws Exception { + createService(); startSystem(); forceSleep(); @@ -355,6 +370,7 @@ public void testWakefulnessAwake_AcquireCausesWakeup() throws Exception { @Test public void testWakefulnessAwake_IPowerManagerWakeUp() throws Exception { + createService(); startSystem(); forceSleep(); mService.getBinderServiceInstance().wakeUp(SystemClock.uptimeMillis(), @@ -369,6 +385,8 @@ public void testWakefulnessAwake_IPowerManagerWakeUp() throws Exception { @Test public void testWakefulnessAwake_ShouldWakeUpWhenPluggedIn() throws Exception { boolean powerState; + + createService(); startSystem(); forceSleep(); @@ -444,6 +462,7 @@ public void testWakefulnessAwake_ShouldWakeUpWhenPluggedIn() throws Exception { @Test public void testWakefulnessDoze_goToSleep() throws Exception { + createService(); // Start with AWAKE state startSystem(); assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_AWAKE); @@ -457,6 +476,7 @@ public void testWakefulnessDoze_goToSleep() throws Exception { @Test public void testWasDeviceIdleFor_true() { int interval = 1000; + createService(); mService.onUserActivity(); SystemClock.sleep(interval + 1 /* just a little more */); assertThat(mService.wasDeviceIdleForInternal(interval)).isTrue(); @@ -465,12 +485,14 @@ public void testWasDeviceIdleFor_true() { @Test public void testWasDeviceIdleFor_false() { int interval = 1000; + createService(); mService.onUserActivity(); assertThat(mService.wasDeviceIdleForInternal(interval)).isFalse(); } @Test public void testForceSuspend_putsDeviceToSleep() { + createService(); mService.systemReady(null); mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -497,6 +519,8 @@ public void testForceSuspend_pakeLocksDisabled() { final int flags = PowerManager.PARTIAL_WAKE_LOCK; final String pkg = mContextSpy.getOpPackageName(); + createService(); + // Set up the Notification mock to keep track of the wakelocks that are currently // active or disabled. We'll use this to verify that wakelocks are disabled when // they should be. @@ -541,7 +565,54 @@ public void testForceSuspend_pakeLocksDisabled() { @Test public void testForceSuspend_forceSuspendFailurePropogated() { + createService(); when(mNativeWrapperMock.nativeForceSuspend()).thenReturn(false); assertThat(mService.getBinderServiceInstance().forceSuspend()).isFalse(); } + + @Test + public void testSetDozeOverrideFromDreamManager_triggersSuspendBlocker() throws Exception { + final String suspendBlockerName = "PowerManagerService.Display"; + final String tag = "acq_causes_wakeup"; + final String packageName = "pkg.name"; + final IBinder token = new Binder(); + + final boolean[] isAcquired = new boolean[1]; + doAnswer(inv -> { + if (suspendBlockerName.equals(inv.getArguments()[0])) { + isAcquired[0] = false; + } + return null; + }).when(mNativeWrapperMock).nativeReleaseSuspendBlocker(any()); + + doAnswer(inv -> { + if (suspendBlockerName.equals(inv.getArguments()[0])) { + isAcquired[0] = true; + } + return null; + }).when(mNativeWrapperMock).nativeAcquireSuspendBlocker(any()); + + // Need to create the service after we stub the mocks for this test because some of the + // mocks are used during the constructor. + createService(); + + // Start with AWAKE state + startSystem(); + assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_AWAKE); + assertTrue(isAcquired[0]); + + // Take a nap and verify we no longer hold the blocker + int flags = PowerManager.DOZE_WAKE_LOCK; + mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, + null /* workSource */, null /* historyTag */); + mService.getBinderServiceInstance().goToSleep(SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0); + assertThat(mService.getWakefulness()).isEqualTo(WAKEFULNESS_DOZING); + assertFalse(isAcquired[0]); + + // Override the display state by DreamManager and verify is reacquires the blocker. + mService.getLocalServiceInstance() + .setDozeOverrideFromDreamManager(Display.STATE_ON, PowerManager.BRIGHTNESS_DEFAULT); + assertTrue(isAcquired[0]); + } }