frameworks/base
Révision | a3c81b0748b2f682f0670e488568e155bb3827f9 (tree) |
---|---|
l'heure | 2020-03-16 19:41:39 |
Auteur | Chih-Wei Huang <cwhuang@linu...> |
Commiter | Chih-Wei Huang |
Merge tag 'android-8.1.0_r74' into oreo-x86
Android 8.1.0 release 74
@@ -127,6 +127,9 @@ public class DownloadManager { | ||
127 | 127 | */ |
128 | 128 | public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS; |
129 | 129 | |
130 | + /** {@hide} */ | |
131 | + public static final String COLUMN_FILE_NAME_HINT = Downloads.Impl.COLUMN_FILE_NAME_HINT; | |
132 | + | |
130 | 133 | /** |
131 | 134 | * Provides more detail on the status of the download. Its meaning depends on the value of |
132 | 135 | * {@link #COLUMN_STATUS}. |
@@ -164,6 +167,9 @@ public class DownloadManager { | ||
164 | 167 | */ |
165 | 168 | public static final String COLUMN_MEDIAPROVIDER_URI = Downloads.Impl.COLUMN_MEDIAPROVIDER_URI; |
166 | 169 | |
170 | + /** {@hide} */ | |
171 | + public static final String COLUMN_DESTINATION = Downloads.Impl.COLUMN_DESTINATION; | |
172 | + | |
167 | 173 | /** |
168 | 174 | * @hide |
169 | 175 | */ |
@@ -330,26 +336,22 @@ public class DownloadManager { | ||
330 | 336 | * @hide |
331 | 337 | */ |
332 | 338 | public static final String[] UNDERLYING_COLUMNS = new String[] { |
333 | - Downloads.Impl._ID, | |
334 | - Downloads.Impl._DATA + " AS " + COLUMN_LOCAL_FILENAME, | |
335 | - Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, | |
336 | - Downloads.Impl.COLUMN_DESTINATION, | |
337 | - Downloads.Impl.COLUMN_TITLE, | |
338 | - Downloads.Impl.COLUMN_DESCRIPTION, | |
339 | - Downloads.Impl.COLUMN_URI, | |
340 | - Downloads.Impl.COLUMN_STATUS, | |
341 | - Downloads.Impl.COLUMN_FILE_NAME_HINT, | |
342 | - Downloads.Impl.COLUMN_MIME_TYPE + " AS " + COLUMN_MEDIA_TYPE, | |
343 | - Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + COLUMN_TOTAL_SIZE_BYTES, | |
344 | - Downloads.Impl.COLUMN_LAST_MODIFICATION + " AS " + COLUMN_LAST_MODIFIED_TIMESTAMP, | |
345 | - Downloads.Impl.COLUMN_CURRENT_BYTES + " AS " + COLUMN_BYTES_DOWNLOADED_SO_FAR, | |
346 | - Downloads.Impl.COLUMN_ALLOW_WRITE, | |
347 | - /* add the following 'computed' columns to the cursor. | |
348 | - * they are not 'returned' by the database, but their inclusion | |
349 | - * eliminates need to have lot of methods in CursorTranslator | |
350 | - */ | |
351 | - "'placeholder' AS " + COLUMN_LOCAL_URI, | |
352 | - "'placeholder' AS " + COLUMN_REASON | |
339 | + DownloadManager.COLUMN_ID, | |
340 | + DownloadManager.COLUMN_LOCAL_FILENAME, | |
341 | + DownloadManager.COLUMN_MEDIAPROVIDER_URI, | |
342 | + DownloadManager.COLUMN_DESTINATION, | |
343 | + DownloadManager.COLUMN_TITLE, | |
344 | + DownloadManager.COLUMN_DESCRIPTION, | |
345 | + DownloadManager.COLUMN_URI, | |
346 | + DownloadManager.COLUMN_STATUS, | |
347 | + DownloadManager.COLUMN_FILE_NAME_HINT, | |
348 | + DownloadManager.COLUMN_MEDIA_TYPE, | |
349 | + DownloadManager.COLUMN_TOTAL_SIZE_BYTES, | |
350 | + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP, | |
351 | + DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR, | |
352 | + DownloadManager.COLUMN_ALLOW_WRITE, | |
353 | + DownloadManager.COLUMN_LOCAL_URI, | |
354 | + DownloadManager.COLUMN_REASON | |
353 | 355 | }; |
354 | 356 | |
355 | 357 | /** |
@@ -28,14 +28,19 @@ import android.provider.BaseColumns; | ||
28 | 28 | import android.text.TextUtils; |
29 | 29 | import android.util.Log; |
30 | 30 | |
31 | +import com.android.internal.util.ArrayUtils; | |
32 | + | |
31 | 33 | import libcore.util.EmptyArray; |
32 | 34 | |
33 | 35 | import java.util.Arrays; |
34 | 36 | import java.util.Iterator; |
37 | +import java.util.List; | |
38 | +import java.util.Locale; | |
35 | 39 | import java.util.Map; |
36 | 40 | import java.util.Map.Entry; |
37 | 41 | import java.util.Objects; |
38 | 42 | import java.util.Set; |
43 | +import java.util.regex.Matcher; | |
39 | 44 | import java.util.regex.Pattern; |
40 | 45 | |
41 | 46 | /** |
@@ -45,15 +50,24 @@ import java.util.regex.Pattern; | ||
45 | 50 | public class SQLiteQueryBuilder |
46 | 51 | { |
47 | 52 | private static final String TAG = "SQLiteQueryBuilder"; |
48 | - private static final Pattern sLimitPattern = | |
49 | - Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); | |
50 | 53 | |
51 | 54 | private Map<String, String> mProjectionMap = null; |
55 | + | |
56 | + private static final Pattern sAggregationPattern = Pattern.compile( | |
57 | + "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); | |
58 | + | |
59 | + private List<Pattern> mProjectionGreylist = null; | |
60 | + | |
52 | 61 | private String mTables = ""; |
53 | 62 | private StringBuilder mWhereClause = null; // lazily created |
54 | 63 | private boolean mDistinct; |
55 | 64 | private SQLiteDatabase.CursorFactory mFactory; |
56 | - private boolean mStrict; | |
65 | + | |
66 | + private static final int STRICT_PARENTHESES = 1 << 0; | |
67 | + private static final int STRICT_COLUMNS = 1 << 1; | |
68 | + private static final int STRICT_GRAMMAR = 1 << 2; | |
69 | + | |
70 | + private int mStrictFlags; | |
57 | 71 | |
58 | 72 | public SQLiteQueryBuilder() { |
59 | 73 | mDistinct = false; |
@@ -139,6 +153,37 @@ public class SQLiteQueryBuilder | ||
139 | 153 | } |
140 | 154 | |
141 | 155 | /** |
156 | + * Gets the projection map for the query, as last configured by | |
157 | + * {@link #setProjectionMap(Map)}. | |
158 | + * | |
159 | + * @hide | |
160 | + */ | |
161 | + public @Nullable Map<String, String> getProjectionMap() { | |
162 | + return mProjectionMap; | |
163 | + } | |
164 | + | |
165 | + /** | |
166 | + * Sets a projection greylist of columns that will be allowed through, even | |
167 | + * when {@link #setStrict(boolean)} is enabled. This provides a way for | |
168 | + * abusive custom columns like {@code COUNT(*)} to continue working. | |
169 | + * | |
170 | + * @hide | |
171 | + */ | |
172 | + public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) { | |
173 | + mProjectionGreylist = projectionGreylist; | |
174 | + } | |
175 | + | |
176 | + /** | |
177 | + * Gets the projection greylist for the query, as last configured by | |
178 | + * {@link #setProjectionGreylist(List)}. | |
179 | + * | |
180 | + * @hide | |
181 | + */ | |
182 | + public @Nullable List<Pattern> getProjectionGreylist() { | |
183 | + return mProjectionGreylist; | |
184 | + } | |
185 | + | |
186 | + /** | |
142 | 187 | * Sets the cursor factory to be used for the query. You can use |
143 | 188 | * one factory for all queries on a database but it is normally |
144 | 189 | * easier to specify the factory when doing this query. |
@@ -170,8 +215,90 @@ public class SQLiteQueryBuilder | ||
170 | 215 | * </ul> |
171 | 216 | * By default, this value is false. |
172 | 217 | */ |
173 | - public void setStrict(boolean flag) { | |
174 | - mStrict = flag; | |
218 | + public void setStrict(boolean strict) { | |
219 | + if (strict) { | |
220 | + mStrictFlags |= STRICT_PARENTHESES; | |
221 | + } else { | |
222 | + mStrictFlags &= ~STRICT_PARENTHESES; | |
223 | + } | |
224 | + } | |
225 | + | |
226 | + /** | |
227 | + * Get if the query is marked as strict, as last configured by | |
228 | + * {@link #setStrict(boolean)}. | |
229 | + * | |
230 | + * @hide | |
231 | + */ | |
232 | + public boolean isStrict() { | |
233 | + return (mStrictFlags & STRICT_PARENTHESES) != 0; | |
234 | + } | |
235 | + | |
236 | + /** | |
237 | + * When enabled, verify that all projections and {@link ContentValues} only | |
238 | + * contain valid columns as defined by {@link #setProjectionMap(Map)}. | |
239 | + * <p> | |
240 | + * This enforcement applies to {@link #insert}, {@link #query}, and | |
241 | + * {@link #update} operations. Any enforcement failures will throw an | |
242 | + * {@link IllegalArgumentException}. | |
243 | + * | |
244 | + * @hide | |
245 | + */ | |
246 | + public void setStrictColumns(boolean strictColumns) { | |
247 | + if (strictColumns) { | |
248 | + mStrictFlags |= STRICT_COLUMNS; | |
249 | + } else { | |
250 | + mStrictFlags &= ~STRICT_COLUMNS; | |
251 | + } | |
252 | + } | |
253 | + | |
254 | + /** | |
255 | + * Get if the query is marked as strict, as last configured by | |
256 | + * {@link #setStrictColumns(boolean)}. | |
257 | + * | |
258 | + * @hide | |
259 | + */ | |
260 | + public boolean isStrictColumns() { | |
261 | + return (mStrictFlags & STRICT_COLUMNS) != 0; | |
262 | + } | |
263 | + | |
264 | + /** | |
265 | + * When enabled, verify that all untrusted SQL conforms to a restricted SQL | |
266 | + * grammar. Here are the restrictions applied: | |
267 | + * <ul> | |
268 | + * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and | |
269 | + * windowing terms are rejected. | |
270 | + * <li>In {@code GROUP BY} clauses: only valid columns are allowed. | |
271 | + * <li>In {@code ORDER BY} clauses: only valid columns, collation, and | |
272 | + * ordering terms are allowed. | |
273 | + * <li>In {@code LIMIT} clauses: only numerical values and offset terms are | |
274 | + * allowed. | |
275 | + * </ul> | |
276 | + * All column references must be valid as defined by | |
277 | + * {@link #setProjectionMap(Map)}. | |
278 | + * <p> | |
279 | + * This enforcement applies to {@link #query}, {@link #update} and | |
280 | + * {@link #delete} operations. This enforcement does not apply to trusted | |
281 | + * inputs, such as those provided by {@link #appendWhere}. Any enforcement | |
282 | + * failures will throw an {@link IllegalArgumentException}. | |
283 | + * | |
284 | + * @hide | |
285 | + */ | |
286 | + public void setStrictGrammar(boolean strictGrammar) { | |
287 | + if (strictGrammar) { | |
288 | + mStrictFlags |= STRICT_GRAMMAR; | |
289 | + } else { | |
290 | + mStrictFlags &= ~STRICT_GRAMMAR; | |
291 | + } | |
292 | + } | |
293 | + | |
294 | + /** | |
295 | + * Get if the query is marked as strict, as last configured by | |
296 | + * {@link #setStrictGrammar(boolean)}. | |
297 | + * | |
298 | + * @hide | |
299 | + */ | |
300 | + public boolean isStrictGrammar() { | |
301 | + return (mStrictFlags & STRICT_GRAMMAR) != 0; | |
175 | 302 | } |
176 | 303 | |
177 | 304 | /** |
@@ -207,9 +334,6 @@ public class SQLiteQueryBuilder | ||
207 | 334 | throw new IllegalArgumentException( |
208 | 335 | "HAVING clauses are only permitted when using a groupBy clause"); |
209 | 336 | } |
210 | - if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { | |
211 | - throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); | |
212 | - } | |
213 | 337 | |
214 | 338 | StringBuilder query = new StringBuilder(120); |
215 | 339 |
@@ -383,7 +507,13 @@ public class SQLiteQueryBuilder | ||
383 | 507 | projectionIn, selection, groupBy, having, |
384 | 508 | sortOrder, limit); |
385 | 509 | |
386 | - if (mStrict && selection != null && selection.length() > 0) { | |
510 | + if (isStrictColumns()) { | |
511 | + enforceStrictColumns(projectionIn); | |
512 | + } | |
513 | + if (isStrictGrammar()) { | |
514 | + enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); | |
515 | + } | |
516 | + if (isStrict()) { | |
387 | 517 | // Validate the user-supplied selection to detect syntactic anomalies |
388 | 518 | // in the selection string that could indicate a SQL injection attempt. |
389 | 519 | // The idea is to ensure that the selection clause is a valid SQL expression |
@@ -401,7 +531,7 @@ public class SQLiteQueryBuilder | ||
401 | 531 | |
402 | 532 | // Execute wrapped query for extra protection |
403 | 533 | final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, |
404 | - having, sortOrder, limit); | |
534 | + wrap(having), sortOrder, limit); | |
405 | 535 | sql = wrappedSql; |
406 | 536 | } else { |
407 | 537 | // Execute unwrapped query |
@@ -446,7 +576,13 @@ public class SQLiteQueryBuilder | ||
446 | 576 | final String sql; |
447 | 577 | final String unwrappedSql = buildUpdate(values, selection); |
448 | 578 | |
449 | - if (mStrict) { | |
579 | + if (isStrictColumns()) { | |
580 | + enforceStrictColumns(values); | |
581 | + } | |
582 | + if (isStrictGrammar()) { | |
583 | + enforceStrictGrammar(selection, null, null, null, null); | |
584 | + } | |
585 | + if (isStrict()) { | |
450 | 586 | // Validate the user-supplied selection to detect syntactic anomalies |
451 | 587 | // in the selection string that could indicate a SQL injection attempt. |
452 | 588 | // The idea is to ensure that the selection clause is a valid SQL expression |
@@ -516,7 +652,10 @@ public class SQLiteQueryBuilder | ||
516 | 652 | final String sql; |
517 | 653 | final String unwrappedSql = buildDelete(selection); |
518 | 654 | |
519 | - if (mStrict) { | |
655 | + if (isStrictGrammar()) { | |
656 | + enforceStrictGrammar(selection, null, null, null, null); | |
657 | + } | |
658 | + if (isStrict()) { | |
520 | 659 | // Validate the user-supplied selection to detect syntactic anomalies |
521 | 660 | // in the selection string that could indicate a SQL injection attempt. |
522 | 661 | // The idea is to ensure that the selection clause is a valid SQL expression |
@@ -551,6 +690,82 @@ public class SQLiteQueryBuilder | ||
551 | 690 | return db.executeSql(sql, sqlArgs); |
552 | 691 | } |
553 | 692 | |
693 | + private void enforceStrictColumns(@Nullable String[] projection) { | |
694 | + Objects.requireNonNull(mProjectionMap, "No projection map defined"); | |
695 | + | |
696 | + computeProjection(projection); | |
697 | + } | |
698 | + | |
699 | + private void enforceStrictColumns(@NonNull ContentValues values) { | |
700 | + Objects.requireNonNull(mProjectionMap, "No projection map defined"); | |
701 | + | |
702 | + final Set<String> rawValues = values.keySet(); | |
703 | + final Iterator<String> rawValuesIt = rawValues.iterator(); | |
704 | + while (rawValuesIt.hasNext()) { | |
705 | + final String column = rawValuesIt.next(); | |
706 | + if (!mProjectionMap.containsKey(column)) { | |
707 | + throw new IllegalArgumentException("Invalid column " + column); | |
708 | + } | |
709 | + } | |
710 | + } | |
711 | + | |
712 | + private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, | |
713 | + @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { | |
714 | + SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, | |
715 | + this::enforceStrictGrammarWhereHaving); | |
716 | + SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, | |
717 | + this::enforceStrictGrammarGroupBy); | |
718 | + SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, | |
719 | + this::enforceStrictGrammarWhereHaving); | |
720 | + SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, | |
721 | + this::enforceStrictGrammarOrderBy); | |
722 | + SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, | |
723 | + this::enforceStrictGrammarLimit); | |
724 | + } | |
725 | + | |
726 | + private void enforceStrictGrammarWhereHaving(@NonNull String token) { | |
727 | + if (isTableOrColumn(token)) return; | |
728 | + if (SQLiteTokenizer.isFunction(token)) return; | |
729 | + if (SQLiteTokenizer.isType(token)) return; | |
730 | + | |
731 | + // NOTE: we explicitly don't allow SELECT subqueries, since they could | |
732 | + // leak data that should have been filtered by the trusted where clause | |
733 | + switch (token.toUpperCase(Locale.US)) { | |
734 | + case "AND": case "AS": case "BETWEEN": case "BINARY": | |
735 | + case "CASE": case "CAST": case "COLLATE": case "DISTINCT": | |
736 | + case "ELSE": case "END": case "ESCAPE": case "EXISTS": | |
737 | + case "GLOB": case "IN": case "IS": case "ISNULL": | |
738 | + case "LIKE": case "MATCH": case "NOCASE": case "NOT": | |
739 | + case "NOTNULL": case "NULL": case "OR": case "REGEXP": | |
740 | + case "RTRIM": case "THEN": case "WHEN": | |
741 | + return; | |
742 | + } | |
743 | + throw new IllegalArgumentException("Invalid token " + token); | |
744 | + } | |
745 | + | |
746 | + private void enforceStrictGrammarGroupBy(@NonNull String token) { | |
747 | + if (isTableOrColumn(token)) return; | |
748 | + throw new IllegalArgumentException("Invalid token " + token); | |
749 | + } | |
750 | + | |
751 | + private void enforceStrictGrammarOrderBy(@NonNull String token) { | |
752 | + if (isTableOrColumn(token)) return; | |
753 | + switch (token.toUpperCase(Locale.US)) { | |
754 | + case "COLLATE": case "ASC": case "DESC": | |
755 | + case "BINARY": case "RTRIM": case "NOCASE": | |
756 | + return; | |
757 | + } | |
758 | + throw new IllegalArgumentException("Invalid token " + token); | |
759 | + } | |
760 | + | |
761 | + private void enforceStrictGrammarLimit(@NonNull String token) { | |
762 | + switch (token.toUpperCase(Locale.US)) { | |
763 | + case "OFFSET": | |
764 | + return; | |
765 | + } | |
766 | + throw new IllegalArgumentException("Invalid token " + token); | |
767 | + } | |
768 | + | |
554 | 769 | /** |
555 | 770 | * Construct a SELECT statement suitable for use in a group of |
556 | 771 | * SELECT statements that will be joined through UNION operators |
@@ -611,7 +826,7 @@ public class SQLiteQueryBuilder | ||
611 | 826 | |
612 | 827 | StringBuilder sql = new StringBuilder(120); |
613 | 828 | sql.append("UPDATE "); |
614 | - sql.append(mTables); | |
829 | + sql.append(SQLiteDatabase.findEditTable(mTables)); | |
615 | 830 | sql.append(" SET "); |
616 | 831 | |
617 | 832 | final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING); |
@@ -632,7 +847,7 @@ public class SQLiteQueryBuilder | ||
632 | 847 | public String buildDelete(String selection) { |
633 | 848 | StringBuilder sql = new StringBuilder(120); |
634 | 849 | sql.append("DELETE FROM "); |
635 | - sql.append(mTables); | |
850 | + sql.append(SQLiteDatabase.findEditTable(mTables)); | |
636 | 851 | |
637 | 852 | final String where = computeWhere(selection); |
638 | 853 | appendClause(sql, " WHERE ", where); |
@@ -763,35 +978,23 @@ public class SQLiteQueryBuilder | ||
763 | 978 | return query.toString(); |
764 | 979 | } |
765 | 980 | |
766 | - private String[] computeProjection(String[] projectionIn) { | |
767 | - if (projectionIn != null && projectionIn.length > 0) { | |
768 | - if (mProjectionMap != null) { | |
769 | - String[] projection = new String[projectionIn.length]; | |
770 | - int length = projectionIn.length; | |
771 | - | |
772 | - for (int i = 0; i < length; i++) { | |
773 | - String userColumn = projectionIn[i]; | |
774 | - String column = mProjectionMap.get(userColumn); | |
775 | - | |
776 | - if (column != null) { | |
777 | - projection[i] = column; | |
778 | - continue; | |
779 | - } | |
780 | - | |
781 | - if (!mStrict && | |
782 | - ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { | |
783 | - /* A column alias already exist */ | |
784 | - projection[i] = userColumn; | |
785 | - continue; | |
786 | - } | |
981 | + private static @NonNull String maybeWithOperator(@Nullable String operator, | |
982 | + @NonNull String column) { | |
983 | + if (operator != null) { | |
984 | + return operator + "(" + column + ")"; | |
985 | + } else { | |
986 | + return column; | |
987 | + } | |
988 | + } | |
787 | 989 | |
788 | - throw new IllegalArgumentException("Invalid column " | |
789 | - + projectionIn[i]); | |
790 | - } | |
791 | - return projection; | |
792 | - } else { | |
793 | - return projectionIn; | |
990 | + /** {@hide} */ | |
991 | + public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { | |
992 | + if (!ArrayUtils.isEmpty(projectionIn)) { | |
993 | + String[] projectionOut = new String[projectionIn.length]; | |
994 | + for (int i = 0; i < projectionIn.length; i++) { | |
995 | + projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); | |
794 | 996 | } |
997 | + return projectionOut; | |
795 | 998 | } else if (mProjectionMap != null) { |
796 | 999 | // Return all columns in projection map. |
797 | 1000 | Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); |
@@ -813,7 +1016,71 @@ public class SQLiteQueryBuilder | ||
813 | 1016 | return null; |
814 | 1017 | } |
815 | 1018 | |
816 | - private @Nullable String computeWhere(@Nullable String selection) { | |
1019 | + private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { | |
1020 | + final String column = computeSingleProjection(userColumn); | |
1021 | + if (column != null) { | |
1022 | + return column; | |
1023 | + } else { | |
1024 | + throw new IllegalArgumentException("Invalid column " + userColumn); | |
1025 | + } | |
1026 | + } | |
1027 | + | |
1028 | + private @Nullable String computeSingleProjection(@NonNull String userColumn) { | |
1029 | + // When no mapping provided, anything goes | |
1030 | + if (mProjectionMap == null) { | |
1031 | + return userColumn; | |
1032 | + } | |
1033 | + | |
1034 | + String operator = null; | |
1035 | + String column = mProjectionMap.get(userColumn); | |
1036 | + | |
1037 | + // When no direct match found, look for aggregation | |
1038 | + if (column == null) { | |
1039 | + final Matcher matcher = sAggregationPattern.matcher(userColumn); | |
1040 | + if (matcher.matches()) { | |
1041 | + operator = matcher.group(1); | |
1042 | + userColumn = matcher.group(2); | |
1043 | + column = mProjectionMap.get(userColumn); | |
1044 | + } | |
1045 | + } | |
1046 | + | |
1047 | + if (column != null) { | |
1048 | + return maybeWithOperator(operator, column); | |
1049 | + } | |
1050 | + | |
1051 | + if (mStrictFlags == 0 | |
1052 | + && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { | |
1053 | + /* A column alias already exist */ | |
1054 | + return maybeWithOperator(operator, userColumn); | |
1055 | + } | |
1056 | + | |
1057 | + // If greylist is configured, we might be willing to let | |
1058 | + // this custom column bypass our strict checks. | |
1059 | + if (mProjectionGreylist != null) { | |
1060 | + boolean match = false; | |
1061 | + for (Pattern p : mProjectionGreylist) { | |
1062 | + if (p.matcher(userColumn).matches()) { | |
1063 | + match = true; | |
1064 | + break; | |
1065 | + } | |
1066 | + } | |
1067 | + | |
1068 | + if (match) { | |
1069 | + Log.w(TAG, "Allowing abusive custom column: " + userColumn); | |
1070 | + return maybeWithOperator(operator, userColumn); | |
1071 | + } | |
1072 | + } | |
1073 | + | |
1074 | + return null; | |
1075 | + } | |
1076 | + | |
1077 | + private boolean isTableOrColumn(String token) { | |
1078 | + if (mTables.equals(token)) return true; | |
1079 | + return computeSingleProjection(token) != null; | |
1080 | + } | |
1081 | + | |
1082 | + /** {@hide} */ | |
1083 | + public @Nullable String computeWhere(@Nullable String selection) { | |
817 | 1084 | final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); |
818 | 1085 | final boolean hasExternal = !TextUtils.isEmpty(selection); |
819 | 1086 |
@@ -0,0 +1,297 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package android.database.sqlite; | |
18 | + | |
19 | +import android.annotation.NonNull; | |
20 | +import android.annotation.Nullable; | |
21 | + | |
22 | +import java.util.ArrayList; | |
23 | +import java.util.List; | |
24 | +import java.util.Locale; | |
25 | +import java.util.function.Consumer; | |
26 | + | |
27 | +/** | |
28 | + * SQL Tokenizer specialized to extract tokens from SQL (snippets). | |
29 | + * <p> | |
30 | + * Based on sqlite3GetToken() in tokenzie.c in SQLite. | |
31 | + * <p> | |
32 | + * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7 | |
33 | + * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922) | |
34 | + * <p> | |
35 | + * Also draft spec: http://www.sqlite.org/draft/tokenreq.html | |
36 | + * | |
37 | + * @hide | |
38 | + */ | |
39 | +public class SQLiteTokenizer { | |
40 | + private static boolean isAlpha(char ch) { | |
41 | + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_'); | |
42 | + } | |
43 | + | |
44 | + private static boolean isNum(char ch) { | |
45 | + return ('0' <= ch && ch <= '9'); | |
46 | + } | |
47 | + | |
48 | + private static boolean isAlNum(char ch) { | |
49 | + return isAlpha(ch) || isNum(ch); | |
50 | + } | |
51 | + | |
52 | + private static boolean isAnyOf(char ch, String set) { | |
53 | + return set.indexOf(ch) >= 0; | |
54 | + } | |
55 | + | |
56 | + private static IllegalArgumentException genException(String message, String sql) { | |
57 | + throw new IllegalArgumentException(message + " in '" + sql + "'"); | |
58 | + } | |
59 | + | |
60 | + private static char peek(String s, int index) { | |
61 | + return index < s.length() ? s.charAt(index) : '\0'; | |
62 | + } | |
63 | + | |
64 | + public static final int OPTION_NONE = 0; | |
65 | + | |
66 | + /** | |
67 | + * Require that SQL contains only tokens; any comments or values will result | |
68 | + * in an exception. | |
69 | + */ | |
70 | + public static final int OPTION_TOKEN_ONLY = 1 << 0; | |
71 | + | |
72 | + /** | |
73 | + * Tokenize the given SQL, returning the list of each encountered token. | |
74 | + * | |
75 | + * @throws IllegalArgumentException if invalid SQL is encountered. | |
76 | + */ | |
77 | + public static List<String> tokenize(@Nullable String sql, int options) { | |
78 | + final ArrayList<String> res = new ArrayList<>(); | |
79 | + tokenize(sql, options, res::add); | |
80 | + return res; | |
81 | + } | |
82 | + | |
83 | + /** | |
84 | + * Tokenize the given SQL, sending each encountered token to the given | |
85 | + * {@link Consumer}. | |
86 | + * | |
87 | + * @throws IllegalArgumentException if invalid SQL is encountered. | |
88 | + */ | |
89 | + public static void tokenize(@Nullable String sql, int options, Consumer<String> checker) { | |
90 | + if (sql == null) { | |
91 | + return; | |
92 | + } | |
93 | + int pos = 0; | |
94 | + final int len = sql.length(); | |
95 | + while (pos < len) { | |
96 | + final char ch = peek(sql, pos); | |
97 | + | |
98 | + // Regular token. | |
99 | + if (isAlpha(ch)) { | |
100 | + final int start = pos; | |
101 | + pos++; | |
102 | + while (isAlNum(peek(sql, pos))) { | |
103 | + pos++; | |
104 | + } | |
105 | + final int end = pos; | |
106 | + | |
107 | + final String token = sql.substring(start, end); | |
108 | + checker.accept(token); | |
109 | + | |
110 | + continue; | |
111 | + } | |
112 | + | |
113 | + // Handle quoted tokens | |
114 | + if (isAnyOf(ch, "'\"`")) { | |
115 | + final int quoteStart = pos; | |
116 | + pos++; | |
117 | + | |
118 | + for (;;) { | |
119 | + pos = sql.indexOf(ch, pos); | |
120 | + if (pos < 0) { | |
121 | + throw genException("Unterminated quote", sql); | |
122 | + } | |
123 | + if (peek(sql, pos + 1) != ch) { | |
124 | + break; | |
125 | + } | |
126 | + // Quoted quote char -- e.g. "abc""def" is a single string. | |
127 | + pos += 2; | |
128 | + } | |
129 | + final int quoteEnd = pos; | |
130 | + pos++; | |
131 | + | |
132 | + if (ch != '\'') { | |
133 | + // Extract the token | |
134 | + final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd); | |
135 | + | |
136 | + final String token; | |
137 | + | |
138 | + // Unquote if needed. i.e. "aa""bb" -> aa"bb | |
139 | + if (tokenUnquoted.indexOf(ch) >= 0) { | |
140 | + token = tokenUnquoted.replaceAll( | |
141 | + String.valueOf(ch) + ch, String.valueOf(ch)); | |
142 | + } else { | |
143 | + token = tokenUnquoted; | |
144 | + } | |
145 | + checker.accept(token); | |
146 | + } else { | |
147 | + if ((options &= OPTION_TOKEN_ONLY) != 0) { | |
148 | + throw genException("Non-token detected", sql); | |
149 | + } | |
150 | + } | |
151 | + continue; | |
152 | + } | |
153 | + // Handle tokens enclosed in [...] | |
154 | + if (ch == '[') { | |
155 | + final int quoteStart = pos; | |
156 | + pos++; | |
157 | + | |
158 | + pos = sql.indexOf(']', pos); | |
159 | + if (pos < 0) { | |
160 | + throw genException("Unterminated quote", sql); | |
161 | + } | |
162 | + final int quoteEnd = pos; | |
163 | + pos++; | |
164 | + | |
165 | + final String token = sql.substring(quoteStart + 1, quoteEnd); | |
166 | + | |
167 | + checker.accept(token); | |
168 | + continue; | |
169 | + } | |
170 | + if ((options &= OPTION_TOKEN_ONLY) != 0) { | |
171 | + throw genException("Non-token detected", sql); | |
172 | + } | |
173 | + | |
174 | + // Detect comments. | |
175 | + if (ch == '-' && peek(sql, pos + 1) == '-') { | |
176 | + pos += 2; | |
177 | + pos = sql.indexOf('\n', pos); | |
178 | + if (pos < 0) { | |
179 | + // We disallow strings ending in an inline comment. | |
180 | + throw genException("Unterminated comment", sql); | |
181 | + } | |
182 | + pos++; | |
183 | + | |
184 | + continue; | |
185 | + } | |
186 | + if (ch == '/' && peek(sql, pos + 1) == '*') { | |
187 | + pos += 2; | |
188 | + pos = sql.indexOf("*/", pos); | |
189 | + if (pos < 0) { | |
190 | + throw genException("Unterminated comment", sql); | |
191 | + } | |
192 | + pos += 2; | |
193 | + | |
194 | + continue; | |
195 | + } | |
196 | + | |
197 | + // Semicolon is never allowed. | |
198 | + if (ch == ';') { | |
199 | + throw genException("Semicolon is not allowed", sql); | |
200 | + } | |
201 | + | |
202 | + // For this purpose, we can simply ignore other characters. | |
203 | + // (Note it doesn't handle the X'' literal properly and reports this X as a token, | |
204 | + // but that should be fine...) | |
205 | + pos++; | |
206 | + } | |
207 | + } | |
208 | + | |
209 | + /** | |
210 | + * Test if given token is a | |
211 | + * <a href="https://www.sqlite.org/lang_keywords.html">SQLite reserved | |
212 | + * keyword</a>. | |
213 | + */ | |
214 | + public static boolean isKeyword(@NonNull String token) { | |
215 | + switch (token.toUpperCase(Locale.US)) { | |
216 | + case "ABORT": case "ACTION": case "ADD": case "AFTER": | |
217 | + case "ALL": case "ALTER": case "ANALYZE": case "AND": | |
218 | + case "AS": case "ASC": case "ATTACH": case "AUTOINCREMENT": | |
219 | + case "BEFORE": case "BEGIN": case "BETWEEN": case "BINARY": | |
220 | + case "BY": case "CASCADE": case "CASE": case "CAST": | |
221 | + case "CHECK": case "COLLATE": case "COLUMN": case "COMMIT": | |
222 | + case "CONFLICT": case "CONSTRAINT": case "CREATE": case "CROSS": | |
223 | + case "CURRENT": case "CURRENT_DATE": case "CURRENT_TIME": case "CURRENT_TIMESTAMP": | |
224 | + case "DATABASE": case "DEFAULT": case "DEFERRABLE": case "DEFERRED": | |
225 | + case "DELETE": case "DESC": case "DETACH": case "DISTINCT": | |
226 | + case "DO": case "DROP": case "EACH": case "ELSE": | |
227 | + case "END": case "ESCAPE": case "EXCEPT": case "EXCLUDE": | |
228 | + case "EXCLUSIVE": case "EXISTS": case "EXPLAIN": case "FAIL": | |
229 | + case "FILTER": case "FOLLOWING": case "FOR": case "FOREIGN": | |
230 | + case "FROM": case "FULL": case "GLOB": case "GROUP": | |
231 | + case "GROUPS": case "HAVING": case "IF": case "IGNORE": | |
232 | + case "IMMEDIATE": case "IN": case "INDEX": case "INDEXED": | |
233 | + case "INITIALLY": case "INNER": case "INSERT": case "INSTEAD": | |
234 | + case "INTERSECT": case "INTO": case "IS": case "ISNULL": | |
235 | + case "JOIN": case "KEY": case "LEFT": case "LIKE": | |
236 | + case "LIMIT": case "MATCH": case "NATURAL": case "NO": | |
237 | + case "NOCASE": case "NOT": case "NOTHING": case "NOTNULL": | |
238 | + case "NULL": case "OF": case "OFFSET": case "ON": | |
239 | + case "OR": case "ORDER": case "OTHERS": case "OUTER": | |
240 | + case "OVER": case "PARTITION": case "PLAN": case "PRAGMA": | |
241 | + case "PRECEDING": case "PRIMARY": case "QUERY": case "RAISE": | |
242 | + case "RANGE": case "RECURSIVE": case "REFERENCES": case "REGEXP": | |
243 | + case "REINDEX": case "RELEASE": case "RENAME": case "REPLACE": | |
244 | + case "RESTRICT": case "RIGHT": case "ROLLBACK": case "ROW": | |
245 | + case "ROWS": case "RTRIM": case "SAVEPOINT": case "SELECT": | |
246 | + case "SET": case "TABLE": case "TEMP": case "TEMPORARY": | |
247 | + case "THEN": case "TIES": case "TO": case "TRANSACTION": | |
248 | + case "TRIGGER": case "UNBOUNDED": case "UNION": case "UNIQUE": | |
249 | + case "UPDATE": case "USING": case "VACUUM": case "VALUES": | |
250 | + case "VIEW": case "VIRTUAL": case "WHEN": case "WHERE": | |
251 | + case "WINDOW": case "WITH": case "WITHOUT": | |
252 | + return true; | |
253 | + default: | |
254 | + return false; | |
255 | + } | |
256 | + } | |
257 | + | |
258 | + /** | |
259 | + * Test if given token is a | |
260 | + * <a href="https://www.sqlite.org/lang_corefunc.html">SQLite reserved | |
261 | + * function</a>. | |
262 | + */ | |
263 | + public static boolean isFunction(@NonNull String token) { | |
264 | + switch (token.toLowerCase(Locale.US)) { | |
265 | + case "abs": case "avg": case "char": case "coalesce": | |
266 | + case "count": case "glob": case "group_concat": case "hex": | |
267 | + case "ifnull": case "instr": case "length": case "like": | |
268 | + case "likelihood": case "likely": case "lower": case "ltrim": | |
269 | + case "max": case "min": case "nullif": case "random": | |
270 | + case "randomblob": case "replace": case "round": case "rtrim": | |
271 | + case "substr": case "sum": case "total": case "trim": | |
272 | + case "typeof": case "unicode": case "unlikely": case "upper": | |
273 | + case "zeroblob": | |
274 | + return true; | |
275 | + default: | |
276 | + return false; | |
277 | + } | |
278 | + } | |
279 | + | |
280 | + /** | |
281 | + * Test if given token is a | |
282 | + * <a href="https://www.sqlite.org/datatype3.html">SQLite reserved type</a>. | |
283 | + */ | |
284 | + public static boolean isType(@NonNull String token) { | |
285 | + switch (token.toUpperCase(Locale.US)) { | |
286 | + case "INT": case "INTEGER": case "TINYINT": case "SMALLINT": | |
287 | + case "MEDIUMINT": case "BIGINT": case "INT2": case "INT8": | |
288 | + case "CHARACTER": case "VARCHAR": case "NCHAR": case "NVARCHAR": | |
289 | + case "TEXT": case "CLOB": case "BLOB": case "REAL": | |
290 | + case "DOUBLE": case "FLOAT": case "NUMERIC": case "DECIMAL": | |
291 | + case "BOOLEAN": case "DATE": case "DATETIME": | |
292 | + return true; | |
293 | + default: | |
294 | + return false; | |
295 | + } | |
296 | + } | |
297 | +} |
@@ -5263,6 +5263,18 @@ public final class Settings { | ||
5263 | 5263 | public static final String DEVICE_PROVISIONED = Global.DEVICE_PROVISIONED; |
5264 | 5264 | |
5265 | 5265 | /** |
5266 | + * Indicates whether a DPC has been downloaded during provisioning. | |
5267 | + * | |
5268 | + * <p>Type: int (0 for false, 1 for true) | |
5269 | + * | |
5270 | + * <p>If this is true, then any attempts to begin setup again should result in factory reset | |
5271 | + * | |
5272 | + * @hide | |
5273 | + */ | |
5274 | + public static final String MANAGED_PROVISIONING_DPC_DOWNLOADED = | |
5275 | + "managed_provisioning_dpc_downloaded"; | |
5276 | + | |
5277 | + /** | |
5266 | 5278 | * Whether the current user has been set up via setup wizard (0 = false, 1 = true) |
5267 | 5279 | * @hide |
5268 | 5280 | */ |
@@ -242,6 +242,7 @@ class TextLine { | ||
242 | 242 | if (runLimit > mLen) { |
243 | 243 | runLimit = mLen; |
244 | 244 | } |
245 | + if (runStart > mLen) break; | |
245 | 246 | boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; |
246 | 247 | |
247 | 248 | int segstart = runStart; |
@@ -319,6 +320,7 @@ class TextLine { | ||
319 | 320 | if (runLimit > mLen) { |
320 | 321 | runLimit = mLen; |
321 | 322 | } |
323 | + if (runStart > mLen) break; | |
322 | 324 | boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; |
323 | 325 | |
324 | 326 | int segstart = runStart; |
@@ -408,6 +410,7 @@ class TextLine { | ||
408 | 410 | if (runLimit > mLen) { |
409 | 411 | runLimit = mLen; |
410 | 412 | } |
413 | + if (runStart > mLen) break; | |
411 | 414 | boolean runIsRtl = (runs[i + 1] & Layout.RUN_RTL_FLAG) != 0; |
412 | 415 | |
413 | 416 | int segstart = runStart; |
@@ -16,6 +16,7 @@ | ||
16 | 16 | |
17 | 17 | package android.widget; |
18 | 18 | |
19 | +import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; | |
19 | 20 | import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH; |
20 | 21 | import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX; |
21 | 22 | import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; |
@@ -28,11 +29,13 @@ import android.annotation.FloatRange; | ||
28 | 29 | import android.annotation.IntDef; |
29 | 30 | import android.annotation.NonNull; |
30 | 31 | import android.annotation.Nullable; |
32 | +import android.annotation.RequiresPermission; | |
31 | 33 | import android.annotation.Size; |
32 | 34 | import android.annotation.StringRes; |
33 | 35 | import android.annotation.StyleRes; |
34 | 36 | import android.annotation.XmlRes; |
35 | 37 | import android.app.Activity; |
38 | +import android.app.ActivityManager; | |
36 | 39 | import android.app.assist.AssistStructure; |
37 | 40 | import android.content.ClipData; |
38 | 41 | import android.content.ClipDescription; |
@@ -66,6 +69,7 @@ import android.os.Parcel; | ||
66 | 69 | import android.os.Parcelable; |
67 | 70 | import android.os.ParcelableParcel; |
68 | 71 | import android.os.SystemClock; |
72 | +import android.os.UserHandle; | |
69 | 73 | import android.provider.Settings; |
70 | 74 | import android.text.BoringLayout; |
71 | 75 | import android.text.DynamicLayout; |
@@ -686,6 +690,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | ||
686 | 690 | |
687 | 691 | private InputFilter[] mFilters = NO_FILTERS; |
688 | 692 | |
693 | + /** | |
694 | + * To keep the information to indicate if there is necessary to restrict the power of | |
695 | + * INTERACT_ACROSS_USERS_FULL. | |
696 | + * <p> | |
697 | + * SystemUI always run as user 0 to process all of direct reply. SystemUI has the poer of | |
698 | + * INTERACT_ACROSS_USERS_FULL. However, all of the notifications not only belong to user 0 but | |
699 | + * also to the other users in multiple user environment. | |
700 | + * </p> | |
701 | + * | |
702 | + * @see #setRestrictedAcrossUser(boolean) | |
703 | + */ | |
704 | + private boolean mIsRestrictedAcrossUser; | |
705 | + | |
689 | 706 | private volatile Locale mCurrentSpellCheckerLocaleCache; |
690 | 707 | |
691 | 708 | // It is possible to have a selection even when mEditor is null (programmatically set, like when |
@@ -10041,6 +10058,24 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | ||
10041 | 10058 | } |
10042 | 10059 | |
10043 | 10060 | /** |
10061 | + * To notify the TextView to restricted the power of the app granted INTERACT_ACROSS_USERS_FULL | |
10062 | + * permission. | |
10063 | + * <p> | |
10064 | + * Most of applications should not granted the INTERACT_ACROSS_USERS_FULL permssion. | |
10065 | + * SystemUI is the special one that run in user 0 process to handle multiple user notification. | |
10066 | + * Unforunately, the power of INTERACT_ACROSS_USERS_FULL should be limited or restricted for | |
10067 | + * preventing from information leak.</p> | |
10068 | + * <p>This function call is called for SystemUI Keyguard and Notification.</p> | |
10069 | + * | |
10070 | + * @param isRestricted is true if the power of INTERACT_ACROSS_USERS_FULL should be limited. | |
10071 | + * @hide | |
10072 | + */ | |
10073 | + @RequiresPermission(INTERACT_ACROSS_USERS_FULL) | |
10074 | + public final void setRestrictedAcrossUser(boolean isRestricted) { | |
10075 | + mIsRestrictedAcrossUser = isRestricted; | |
10076 | + } | |
10077 | + | |
10078 | + /** | |
10044 | 10079 | * This is a temporary method. Future versions may support multi-locale text. |
10045 | 10080 | * Caveat: This method may not return the latest text services locale, but this should be |
10046 | 10081 | * acceptable and it's more important to make this method asynchronous. |
@@ -11073,6 +11108,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | ||
11073 | 11108 | } |
11074 | 11109 | |
11075 | 11110 | boolean canCut() { |
11111 | + if (mIsRestrictedAcrossUser | |
11112 | + && UserHandle.myUserId() != ActivityManager.getCurrentUser()) { | |
11113 | + // When it's restricted, and the curren user is not the process user. It can't cut | |
11114 | + // because it may cut the text of the user 10 into the clipboard of user 0. | |
11115 | + return false; | |
11116 | + } | |
11076 | 11117 | if (hasPasswordTransformationMethod()) { |
11077 | 11118 | return false; |
11078 | 11119 | } |
@@ -11086,6 +11127,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | ||
11086 | 11127 | } |
11087 | 11128 | |
11088 | 11129 | boolean canCopy() { |
11130 | + if (mIsRestrictedAcrossUser | |
11131 | + && UserHandle.myUserId() != ActivityManager.getCurrentUser()) { | |
11132 | + // When it's restricted, and the curren user is not the process user. It can't copy | |
11133 | + // because it may copy the text of the user 10 to the clipboard of user 0. | |
11134 | + return false; | |
11135 | + } | |
11089 | 11136 | if (hasPasswordTransformationMethod()) { |
11090 | 11137 | return false; |
11091 | 11138 | } |
@@ -11115,6 +11162,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener | ||
11115 | 11162 | } |
11116 | 11163 | |
11117 | 11164 | boolean canPaste() { |
11165 | + if (mIsRestrictedAcrossUser | |
11166 | + && UserHandle.myUserId() != ActivityManager.getCurrentUser()) { | |
11167 | + // When it's restricted, and the curren user is not the process user. It can't paste | |
11168 | + // because it may copy the text from the user 0 clipboard in current user is 10. | |
11169 | + return false; | |
11170 | + } | |
11118 | 11171 | return (mText instanceof Editable |
11119 | 11172 | && mEditor != null && mEditor.mKeyListener != null |
11120 | 11173 | && getSelectionStart() >= 0 |
@@ -0,0 +1,169 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package android.database.sqlite; | |
18 | + | |
19 | +import static org.junit.Assert.assertEquals; | |
20 | +import static org.junit.Assert.assertTrue; | |
21 | +import static org.junit.Assert.fail; | |
22 | + | |
23 | +import org.junit.Test; | |
24 | + | |
25 | +import java.util.ArrayList; | |
26 | +import java.util.Arrays; | |
27 | +import java.util.List; | |
28 | + | |
29 | +public class SQLiteTokenizerTest { | |
30 | + private List<String> getTokens(String sql) { | |
31 | + return SQLiteTokenizer.tokenize(sql, SQLiteTokenizer.OPTION_NONE); | |
32 | + } | |
33 | + | |
34 | + private void checkTokens(String sql, String spaceSeparatedExpectedTokens) { | |
35 | + final List<String> expected = spaceSeparatedExpectedTokens == null | |
36 | + ? new ArrayList<>() | |
37 | + : Arrays.asList(spaceSeparatedExpectedTokens.split(" +")); | |
38 | + | |
39 | + assertEquals(expected, getTokens(sql)); | |
40 | + } | |
41 | + | |
42 | + private void assertInvalidSql(String sql, String message) { | |
43 | + try { | |
44 | + getTokens(sql); | |
45 | + fail("Didn't throw InvalidSqlException"); | |
46 | + } catch (IllegalArgumentException e) { | |
47 | + assertTrue("Expected " + e.getMessage() + " to contain " + message, | |
48 | + e.getMessage().contains(message)); | |
49 | + } | |
50 | + } | |
51 | + | |
52 | + @Test | |
53 | + public void testWhitespaces() { | |
54 | + checkTokens(" select \t\r\n a\n\n ", "select a"); | |
55 | + checkTokens("a b", "a b"); | |
56 | + } | |
57 | + | |
58 | + @Test | |
59 | + public void testComment() { | |
60 | + checkTokens("--\n", null); | |
61 | + checkTokens("a--\n", "a"); | |
62 | + checkTokens("a--abcdef\n", "a"); | |
63 | + checkTokens("a--abcdef\nx", "a x"); | |
64 | + checkTokens("a--\nx", "a x"); | |
65 | + assertInvalidSql("a--abcdef", "Unterminated comment"); | |
66 | + assertInvalidSql("a--abcdef\ndef--", "Unterminated comment"); | |
67 | + | |
68 | + checkTokens("/**/", null); | |
69 | + assertInvalidSql("/*", "Unterminated comment"); | |
70 | + assertInvalidSql("/*/", "Unterminated comment"); | |
71 | + assertInvalidSql("/*\n* /*a", "Unterminated comment"); | |
72 | + checkTokens("a/**/", "a"); | |
73 | + checkTokens("/**/b", "b"); | |
74 | + checkTokens("a/**/b", "a b"); | |
75 | + checkTokens("a/* -- \n* /* **/b", "a b"); | |
76 | + } | |
77 | + | |
78 | + @Test | |
79 | + public void testStrings() { | |
80 | + assertInvalidSql("'", "Unterminated quote"); | |
81 | + assertInvalidSql("a'", "Unterminated quote"); | |
82 | + assertInvalidSql("a'''", "Unterminated quote"); | |
83 | + assertInvalidSql("a''' ", "Unterminated quote"); | |
84 | + checkTokens("''", null); | |
85 | + checkTokens("''''", null); | |
86 | + checkTokens("a''''b", "a b"); | |
87 | + checkTokens("a' '' 'b", "a b"); | |
88 | + checkTokens("'abc'", null); | |
89 | + checkTokens("'abc\ndef'", null); | |
90 | + checkTokens("a'abc\ndef'", "a"); | |
91 | + checkTokens("'abc\ndef'b", "b"); | |
92 | + checkTokens("a'abc\ndef'b", "a b"); | |
93 | + checkTokens("a'''abc\nd''ef'''b", "a b"); | |
94 | + } | |
95 | + | |
96 | + @Test | |
97 | + public void testDoubleQuotes() { | |
98 | + assertInvalidSql("\"", "Unterminated quote"); | |
99 | + assertInvalidSql("a\"", "Unterminated quote"); | |
100 | + assertInvalidSql("a\"\"\"", "Unterminated quote"); | |
101 | + assertInvalidSql("a\"\"\" ", "Unterminated quote"); | |
102 | + checkTokens("\"\"", ""); | |
103 | + checkTokens("\"\"\"\"", "\""); | |
104 | + checkTokens("a\"\"\"\"b", "a \" b"); | |
105 | + checkTokens("a\"\t\"\"\t\"b", "a \t\"\t b"); | |
106 | + checkTokens("\"abc\"", "abc"); | |
107 | + checkTokens("\"abc\ndef\"", "abc\ndef"); | |
108 | + checkTokens("a\"abc\ndef\"", "a abc\ndef"); | |
109 | + checkTokens("\"abc\ndef\"b", "abc\ndef b"); | |
110 | + checkTokens("a\"abc\ndef\"b", "a abc\ndef b"); | |
111 | + checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", "a \"abc\nd\"ef\" b"); | |
112 | + } | |
113 | + | |
114 | + @Test | |
115 | + public void testBackQuotes() { | |
116 | + assertInvalidSql("`", "Unterminated quote"); | |
117 | + assertInvalidSql("a`", "Unterminated quote"); | |
118 | + assertInvalidSql("a```", "Unterminated quote"); | |
119 | + assertInvalidSql("a``` ", "Unterminated quote"); | |
120 | + checkTokens("``", ""); | |
121 | + checkTokens("````", "`"); | |
122 | + checkTokens("a````b", "a ` b"); | |
123 | + checkTokens("a`\t``\t`b", "a \t`\t b"); | |
124 | + checkTokens("`abc`", "abc"); | |
125 | + checkTokens("`abc\ndef`", "abc\ndef"); | |
126 | + checkTokens("a`abc\ndef`", "a abc\ndef"); | |
127 | + checkTokens("`abc\ndef`b", "abc\ndef b"); | |
128 | + checkTokens("a`abc\ndef`b", "a abc\ndef b"); | |
129 | + checkTokens("a```abc\nd``ef```b", "a `abc\nd`ef` b"); | |
130 | + } | |
131 | + | |
132 | + @Test | |
133 | + public void testBrackets() { | |
134 | + assertInvalidSql("[", "Unterminated quote"); | |
135 | + assertInvalidSql("a[", "Unterminated quote"); | |
136 | + assertInvalidSql("a[ ", "Unterminated quote"); | |
137 | + assertInvalidSql("a[[ ", "Unterminated quote"); | |
138 | + checkTokens("[]", ""); | |
139 | + checkTokens("[[]", "["); | |
140 | + checkTokens("a[[]b", "a [ b"); | |
141 | + checkTokens("a[\t[\t]b", "a \t[\t b"); | |
142 | + checkTokens("[abc]", "abc"); | |
143 | + checkTokens("[abc\ndef]", "abc\ndef"); | |
144 | + checkTokens("a[abc\ndef]", "a abc\ndef"); | |
145 | + checkTokens("[abc\ndef]b", "abc\ndef b"); | |
146 | + checkTokens("a[abc\ndef]b", "a abc\ndef b"); | |
147 | + checkTokens("a[[abc\nd[ef[]b", "a [abc\nd[ef[ b"); | |
148 | + } | |
149 | + | |
150 | + @Test | |
151 | + public void testSemicolons() { | |
152 | + assertInvalidSql(";", "Semicolon is not allowed"); | |
153 | + assertInvalidSql(" ;", "Semicolon is not allowed"); | |
154 | + assertInvalidSql("; ", "Semicolon is not allowed"); | |
155 | + assertInvalidSql("-;-", "Semicolon is not allowed"); | |
156 | + checkTokens("--;\n", null); | |
157 | + checkTokens("/*;*/", null); | |
158 | + checkTokens("';'", null); | |
159 | + checkTokens("[;]", ";"); | |
160 | + checkTokens("`;`", ";"); | |
161 | + } | |
162 | + | |
163 | + @Test | |
164 | + public void testTokens() { | |
165 | + checkTokens("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef"); | |
166 | + checkTokens("a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f", | |
167 | + "a abc a00b _1 _123 abc d e f"); | |
168 | + } | |
169 | +} |
@@ -25,6 +25,7 @@ import android.content.SharedPreferences; | ||
25 | 25 | import android.os.ParcelUuid; |
26 | 26 | import android.os.SystemClock; |
27 | 27 | import android.text.TextUtils; |
28 | +import android.util.EventLog; | |
28 | 29 | import android.util.Log; |
29 | 30 | import android.bluetooth.BluetoothAdapter; |
30 | 31 |
@@ -831,10 +832,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> | ||
831 | 832 | == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || |
832 | 833 | mDevice.getBluetoothClass().getDeviceClass() |
833 | 834 | == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) { |
834 | - setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); | |
835 | - } else { | |
836 | - setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); | |
835 | + EventLog.writeEvent(0x534e4554, "138529441", -1, ""); | |
837 | 836 | } |
837 | + setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); | |
838 | 838 | } |
839 | 839 | } |
840 | 840 | } |
@@ -79,6 +79,7 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView | ||
79 | 79 | |
80 | 80 | @Override |
81 | 81 | protected void resetState() { |
82 | + mPasswordEntry.setRestrictedAcrossUser(true); | |
82 | 83 | mSecurityMessageDisplay.setMessage(""); |
83 | 84 | final boolean wasDisabled = mPasswordEntry.isEnabled(); |
84 | 85 | setPasswordEntryEnabled(true); |
@@ -175,6 +176,7 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView | ||
175 | 176 | Context.INPUT_METHOD_SERVICE); |
176 | 177 | |
177 | 178 | mPasswordEntry = findViewById(getPasswordTextViewId()); |
179 | + mPasswordEntry.setRestrictedAcrossUser(true); | |
178 | 180 | mPasswordEntryDisabler = new TextViewInputDisabler(mPasswordEntry); |
179 | 181 | mPasswordEntry.setKeyListener(TextKeyListener.getInstance()); |
180 | 182 | mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT |
@@ -0,0 +1,87 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.android.systemui.statusbar; | |
18 | + | |
19 | +import android.annotation.NonNull; | |
20 | +import android.app.Notification; | |
21 | +import android.os.Handler; | |
22 | +import android.os.Looper; | |
23 | +import android.util.ArraySet; | |
24 | + | |
25 | +import com.android.internal.annotations.VisibleForTesting; | |
26 | +import com.android.systemui.statusbar.NotificationData; | |
27 | + | |
28 | +/** | |
29 | + * Extends the lifetime of foreground notification services such that they show for at least | |
30 | + * five seconds | |
31 | + */ | |
32 | +public class ForegroundServiceLifetimeExtender implements NotificationLifetimeExtender { | |
33 | + private static final String TAG = "FGSLifetimeExtender"; | |
34 | + | |
35 | + @VisibleForTesting | |
36 | + public static final int MIN_FGS_TIME_MS = 5000; | |
37 | + | |
38 | + private NotificationSafeToRemoveCallback mNotificationSafeToRemoveCallback; | |
39 | + private ArraySet<NotificationData.Entry> mManagedEntries = new ArraySet<>(); | |
40 | + private Handler mHandler = new Handler(Looper.getMainLooper()); | |
41 | + | |
42 | + public ForegroundServiceLifetimeExtender() { | |
43 | + } | |
44 | + | |
45 | + @Override | |
46 | + public void setCallback(@NonNull NotificationSafeToRemoveCallback callback) { | |
47 | + mNotificationSafeToRemoveCallback = callback; | |
48 | + } | |
49 | + | |
50 | + @Override | |
51 | + public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) { | |
52 | + if ((entry.notification.getNotification().flags | |
53 | + & Notification.FLAG_FOREGROUND_SERVICE) == 0) { | |
54 | + return false; | |
55 | + } | |
56 | + long currentTime = System.currentTimeMillis(); | |
57 | + return currentTime - entry.notification.getPostTime() < MIN_FGS_TIME_MS; | |
58 | + } | |
59 | + | |
60 | + @Override | |
61 | + public boolean shouldExtendLifetimeForPendingNotification( | |
62 | + @NonNull NotificationData.Entry entry) { | |
63 | + return shouldExtendLifetime(entry); | |
64 | + } | |
65 | + | |
66 | + @Override | |
67 | + public void setShouldManageLifetime( | |
68 | + @NonNull NotificationData.Entry entry, boolean shouldManage) { | |
69 | + android.util.Log.d("FGSExtender", "setShouldManageLifetime " + shouldManage); | |
70 | + if (!shouldManage) { | |
71 | + mManagedEntries.remove(entry); | |
72 | + return; | |
73 | + } | |
74 | + mManagedEntries.add(entry); | |
75 | + Runnable r = () -> { | |
76 | + if (mManagedEntries.contains(entry)) { | |
77 | + mManagedEntries.remove(entry); | |
78 | + if (mNotificationSafeToRemoveCallback != null) { | |
79 | + mNotificationSafeToRemoveCallback.onSafeToRemove(entry.key); | |
80 | + } | |
81 | + } | |
82 | + }; | |
83 | + long delayAmt = MIN_FGS_TIME_MS | |
84 | + - (System.currentTimeMillis() - entry.notification.getPostTime()); | |
85 | + mHandler.postDelayed(r, delayAmt); | |
86 | + } | |
87 | +} |
@@ -0,0 +1,81 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.android.systemui.statusbar; | |
18 | + | |
19 | +import android.annotation.NonNull; | |
20 | + | |
21 | +import com.android.systemui.statusbar.NotificationData; | |
22 | + | |
23 | +/** | |
24 | + * Interface for anything that may need to keep notifications managed even after | |
25 | + * {@link NotificationListener} removes it. The lifetime extender is in charge of performing the | |
26 | + * callback when the notification is then safe to remove. | |
27 | + */ | |
28 | +public interface NotificationLifetimeExtender { | |
29 | + | |
30 | + /** | |
31 | + * Set the handler to callback to when the notification is safe to remove. | |
32 | + * | |
33 | + * @param callback the handler to callback | |
34 | + */ | |
35 | + void setCallback(@NonNull NotificationSafeToRemoveCallback callback); | |
36 | + | |
37 | + /** | |
38 | + * Determines whether or not the extender needs the notification kept after removal. | |
39 | + * | |
40 | + * @param entry the entry containing the notification to check | |
41 | + * @return true if the notification lifetime should be extended | |
42 | + */ | |
43 | + boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry); | |
44 | + | |
45 | + /** | |
46 | + * It's possible that a notification was canceled before it ever became visible. This callback | |
47 | + * gives lifetime extenders a chance to make sure it shows up. For example if a foreground | |
48 | + * service is canceled too quickly but we still want to make sure a FGS notification shows. | |
49 | + * @param pendingEntry the canceled (but pending) entry | |
50 | + * @return true if the notification lifetime should be extended | |
51 | + */ | |
52 | + default boolean shouldExtendLifetimeForPendingNotification( | |
53 | + @NonNull NotificationData.Entry pendingEntry) { | |
54 | + return false; | |
55 | + } | |
56 | + | |
57 | + /** | |
58 | + * Sets whether or not the lifetime should be managed by the extender. In practice, if | |
59 | + * shouldManage is true, this is where the extender starts managing the entry internally and is | |
60 | + * now responsible for calling {@link NotificationSafeToRemoveCallback#onSafeToRemove(String)} | |
61 | + * when the entry is safe to remove. If shouldManage is false, the extender no longer needs to | |
62 | + * worry about it (either because we will be removing it anyway or the entry is no longer | |
63 | + * removed due to an update). | |
64 | + * | |
65 | + * @param entry the entry that needs an extended lifetime | |
66 | + * @param shouldManage true if the extender should manage the entry now, false otherwise | |
67 | + */ | |
68 | + void setShouldManageLifetime(@NonNull NotificationData.Entry entry, boolean shouldManage); | |
69 | + | |
70 | + /** | |
71 | + * The callback for when the notification is now safe to remove (i.e. its lifetime has ended). | |
72 | + */ | |
73 | + interface NotificationSafeToRemoveCallback { | |
74 | + /** | |
75 | + * Called when the lifetime extender determines it's safe to remove. | |
76 | + * | |
77 | + * @param key key of the entry that is now safe to remove | |
78 | + */ | |
79 | + void onSafeToRemove(String key); | |
80 | + } | |
81 | +} |
@@ -110,6 +110,7 @@ import android.service.vr.IVrManager; | ||
110 | 110 | import android.service.vr.IVrStateCallbacks; |
111 | 111 | import android.text.TextUtils; |
112 | 112 | import android.util.ArraySet; |
113 | +import android.util.ArrayMap; | |
113 | 114 | import android.util.DisplayMetrics; |
114 | 115 | import android.util.EventLog; |
115 | 116 | import android.util.Log; |
@@ -204,6 +205,7 @@ import com.android.systemui.statusbar.DismissView; | ||
204 | 205 | import com.android.systemui.statusbar.DragDownHelper; |
205 | 206 | import com.android.systemui.statusbar.EmptyShadeView; |
206 | 207 | import com.android.systemui.statusbar.ExpandableNotificationRow; |
208 | +import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender; | |
207 | 209 | import com.android.systemui.statusbar.GestureRecorder; |
208 | 210 | import com.android.systemui.statusbar.KeyboardShortcuts; |
209 | 211 | import com.android.systemui.statusbar.KeyguardIndicationController; |
@@ -211,6 +213,7 @@ import com.android.systemui.statusbar.NotificationData; | ||
211 | 213 | import com.android.systemui.statusbar.NotificationData.Entry; |
212 | 214 | import com.android.systemui.statusbar.NotificationGuts; |
213 | 215 | import com.android.systemui.statusbar.NotificationInfo; |
216 | +import com.android.systemui.statusbar.NotificationLifetimeExtender; | |
214 | 217 | import com.android.systemui.statusbar.NotificationShelf; |
215 | 218 | import com.android.systemui.statusbar.NotificationSnooze; |
216 | 219 | import com.android.systemui.statusbar.RemoteInputController; |
@@ -751,9 +754,9 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
751 | 754 | @Nullable private View mAmbientIndicationContainer; |
752 | 755 | private String mKeyToRemoveOnGutsClosed; |
753 | 756 | private SysuiColorExtractor mColorExtractor; |
754 | - private ForegroundServiceController mForegroundServiceController; | |
755 | 757 | private ScreenLifecycle mScreenLifecycle; |
756 | 758 | @VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle; |
759 | + protected ForegroundServiceController mForegroundServiceController; | |
757 | 760 | |
758 | 761 | private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) { |
759 | 762 | final int N = array.size(); |
@@ -807,6 +810,9 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
807 | 810 | mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); |
808 | 811 | |
809 | 812 | mForegroundServiceController = Dependency.get(ForegroundServiceController.class); |
813 | + mFGSExtender = new ForegroundServiceLifetimeExtender(); | |
814 | + mFGSExtender.setCallback(key -> removeNotification(key, mLatestRankingMap)); | |
815 | + | |
810 | 816 | |
811 | 817 | mDisplay = mWindowManager.getDefaultDisplay(); |
812 | 818 | updateDisplaySize(); |
@@ -1808,6 +1814,11 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
1808 | 1814 | } |
1809 | 1815 | Entry entry = mNotificationData.get(key); |
1810 | 1816 | |
1817 | + if (entry != null && mFGSExtender.shouldExtendLifetime(entry)) { | |
1818 | + extendLifetime(entry, mFGSExtender); | |
1819 | + return; | |
1820 | + } | |
1821 | + | |
1811 | 1822 | if (entry != null && mRemoteInputController.isRemoteInputActive(entry) |
1812 | 1823 | && (entry.row != null && !entry.row.isDismissed())) { |
1813 | 1824 | mLatestRankingMap = ranking; |
@@ -1846,9 +1857,35 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
1846 | 1857 | } |
1847 | 1858 | } |
1848 | 1859 | } |
1860 | + // Make sure no lifetime extension is happening anymore | |
1861 | + cancelLifetimeExtension(entry); | |
1849 | 1862 | setAreThereNotifications(); |
1850 | 1863 | } |
1851 | 1864 | |
1865 | + /** Lifetime extension keeps entries around after they would've otherwise been canceled */ | |
1866 | + private void extendLifetime(Entry entry, NotificationLifetimeExtender extender) { | |
1867 | + // Cancel any other extender which might be holding on to this notification entry | |
1868 | + NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry); | |
1869 | + if (activeExtender != null && activeExtender != extender) { | |
1870 | + activeExtender.setShouldManageLifetime(entry, false); | |
1871 | + } | |
1872 | + mRetainedNotifications.put(entry, extender); | |
1873 | + extender.setShouldManageLifetime(entry, true); | |
1874 | + } | |
1875 | + | |
1876 | + /** Tells the current extender (if any) to stop extending the entry's lifetime */ | |
1877 | + private void cancelLifetimeExtension(NotificationData.Entry entry) { | |
1878 | + NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry); | |
1879 | + if (activeExtender != null) { | |
1880 | + activeExtender.setShouldManageLifetime(entry, false); | |
1881 | + } | |
1882 | + } | |
1883 | + | |
1884 | + @VisibleForTesting | |
1885 | + public Map<Entry, NotificationLifetimeExtender> getRetainedNotificationMap() { | |
1886 | + return mRetainedNotifications; | |
1887 | + } | |
1888 | + | |
1852 | 1889 | /** |
1853 | 1890 | * Ensures that the group children are cancelled immediately when the group summary is cancelled |
1854 | 1891 | * instead of waiting for the notification manager to send all cancels. Otherwise this could |
@@ -3533,6 +3570,17 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
3533 | 3570 | } |
3534 | 3571 | } |
3535 | 3572 | |
3573 | + pw.println(" Lifetime-extended notifications:"); | |
3574 | + if (mRetainedNotifications.isEmpty()) { | |
3575 | + pw.println(" None"); | |
3576 | + } else { | |
3577 | + for (Map.Entry<NotificationData.Entry, NotificationLifetimeExtender> entry | |
3578 | + : mRetainedNotifications.entrySet()) { | |
3579 | + pw.println(" " + entry.getKey().notification + " retained by " | |
3580 | + + entry.getValue().getClass().getName()); | |
3581 | + } | |
3582 | + } | |
3583 | + | |
3536 | 3584 | pw.print(" mInteractingWindows="); pw.println(mInteractingWindows); |
3537 | 3585 | pw.print(" mStatusBarWindowState="); |
3538 | 3586 | pw.println(windowStateToString(mStatusBarWindowState)); |
@@ -5703,6 +5751,11 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
5703 | 5751 | |
5704 | 5752 | protected RemoteInputController mRemoteInputController; |
5705 | 5753 | |
5754 | + // A lifetime extender that watches for foreground service notifications | |
5755 | + @VisibleForTesting protected NotificationLifetimeExtender mFGSExtender; | |
5756 | + private final Map<Entry, NotificationLifetimeExtender> mRetainedNotifications = | |
5757 | + new ArrayMap<>(); | |
5758 | + | |
5706 | 5759 | // for heads up notifications |
5707 | 5760 | protected HeadsUpManager mHeadsUpManager; |
5708 | 5761 |
@@ -7384,6 +7437,9 @@ public class StatusBar extends SystemUI implements DemoMode, | ||
7384 | 7437 | Log.w(TAG, "Notification that was kept for guts was updated. " + key); |
7385 | 7438 | } |
7386 | 7439 | |
7440 | + // No need to keep the lifetime extension around if an update comes in for it | |
7441 | + cancelLifetimeExtension(entry); | |
7442 | + | |
7387 | 7443 | Notification n = notification.getNotification(); |
7388 | 7444 | mNotificationData.updateRanking(ranking); |
7389 | 7445 |
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.policy; | ||
18 | 18 | |
19 | 19 | import android.animation.Animator; |
20 | 20 | import android.animation.AnimatorListenerAdapter; |
21 | +import android.app.ActivityManager; | |
21 | 22 | import android.app.Notification; |
22 | 23 | import android.app.PendingIntent; |
23 | 24 | import android.app.RemoteInput; |
@@ -27,7 +28,9 @@ import android.content.pm.ShortcutManager; | ||
27 | 28 | import android.graphics.Rect; |
28 | 29 | import android.graphics.drawable.Drawable; |
29 | 30 | import android.os.Bundle; |
31 | +import android.os.UserHandle; | |
30 | 32 | import android.text.Editable; |
33 | +import android.text.InputType; | |
31 | 34 | import android.text.TextWatcher; |
32 | 35 | import android.util.AttributeSet; |
33 | 36 | import android.util.Log; |
@@ -176,6 +179,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene | ||
176 | 179 | LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); |
177 | 180 | v.mController = controller; |
178 | 181 | v.mEntry = entry; |
182 | + v.mEditText.setRestrictedAcrossUser(true); | |
179 | 183 | v.setTag(VIEW_TAG); |
180 | 184 | |
181 | 185 | return v; |
@@ -278,12 +282,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene | ||
278 | 282 | if (mWrapper != null) { |
279 | 283 | mWrapper.setRemoteInputVisible(true); |
280 | 284 | } |
281 | - mController.addRemoteInput(mEntry, mToken); | |
285 | + | |
286 | + // Disable suggestions on non-owner (secondary) user. | |
287 | + // SpellCheckerService of primary user runs on secondary as well which shows | |
288 | + // "Add to dictionary" dialog on the primary user. (See b/123232892) | |
289 | + // Note: this doesn't affect work-profile users on P or older versions. | |
290 | + if (UserHandle.myUserId() != ActivityManager.getCurrentUser()) { | |
291 | + mEditText.setInputType( | |
292 | + mEditText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); | |
293 | + } | |
294 | + | |
282 | 295 | mEditText.setInnerFocusable(true); |
283 | 296 | mEditText.mShowImeOnInputConnection = true; |
284 | 297 | mEditText.setText(mEntry.remoteInputText); |
285 | 298 | mEditText.setSelection(mEditText.getText().length()); |
286 | 299 | mEditText.requestFocus(); |
300 | + mController.addRemoteInput(mEntry, mToken); | |
287 | 301 | updateSendButton(); |
288 | 302 | } |
289 | 303 |
@@ -0,0 +1,85 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.android.systemui.statusbar; | |
18 | + | |
19 | +import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS; | |
20 | + | |
21 | +import static org.junit.Assert.assertFalse; | |
22 | +import static org.junit.Assert.assertTrue; | |
23 | +import static org.mockito.Mockito.mock; | |
24 | +import static org.mockito.Mockito.when; | |
25 | + | |
26 | +import android.app.Notification; | |
27 | +import android.service.notification.NotificationListenerService.Ranking; | |
28 | +import android.service.notification.StatusBarNotification; | |
29 | +import android.support.test.filters.SmallTest; | |
30 | +import android.support.test.runner.AndroidJUnit4; | |
31 | + | |
32 | +import com.android.systemui.R; | |
33 | +import com.android.systemui.SysuiTestCase; | |
34 | +import com.android.systemui.statusbar.NotificationData.Entry; | |
35 | + | |
36 | +import org.junit.Before; | |
37 | +import org.junit.Test; | |
38 | +import org.junit.runner.RunWith; | |
39 | + | |
40 | +@RunWith(AndroidJUnit4.class) | |
41 | +@SmallTest | |
42 | +public class ForegroundServiceLifetimeExtenderTest extends SysuiTestCase { | |
43 | + private ForegroundServiceLifetimeExtender mExtender = new ForegroundServiceLifetimeExtender(); | |
44 | + private StatusBarNotification mSbn; | |
45 | + private NotificationData.Entry mEntry; | |
46 | + private Notification mNotif; | |
47 | + | |
48 | + @Before | |
49 | + public void setup() { | |
50 | + mNotif = new Notification.Builder(mContext, "") | |
51 | + .setSmallIcon(R.drawable.ic_person) | |
52 | + .setContentTitle("Title") | |
53 | + .setContentText("Text") | |
54 | + .build(); | |
55 | + mSbn = mock(StatusBarNotification.class); | |
56 | + when(mSbn.getNotification()).thenReturn(mNotif); | |
57 | + mEntry = new NotificationData.Entry(mSbn); | |
58 | + } | |
59 | + | |
60 | + /** | |
61 | + * ForegroundServiceLifetimeExtenderTest | |
62 | + */ | |
63 | + @Test | |
64 | + public void testShouldExtendLifetime_should_foreground() { | |
65 | + // Extend the lifetime of a FGS notification iff it has not been visible | |
66 | + // for the minimum time | |
67 | + mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE; | |
68 | + when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis()); | |
69 | + assertTrue(mExtender.shouldExtendLifetime(mEntry)); | |
70 | + } | |
71 | + | |
72 | + @Test | |
73 | + public void testShouldExtendLifetime_shouldNot_foreground() { | |
74 | + mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE; | |
75 | + when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1); | |
76 | + assertFalse(mExtender.shouldExtendLifetime(mEntry)); | |
77 | + } | |
78 | + | |
79 | + @Test | |
80 | + public void testShouldExtendLifetime_shouldNot_notForeground() { | |
81 | + mNotif.flags = 0; | |
82 | + when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1); | |
83 | + assertFalse(mExtender.shouldExtendLifetime(mEntry)); | |
84 | + } | |
85 | +} |
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; | ||
18 | 18 | |
19 | 19 | import static android.app.NotificationManager.IMPORTANCE_HIGH; |
20 | 20 | |
21 | +import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS; | |
22 | + | |
21 | 23 | import static junit.framework.Assert.assertFalse; |
22 | 24 | import static junit.framework.Assert.assertTrue; |
23 | 25 | import static junit.framework.TestCase.fail; |
@@ -33,6 +35,7 @@ import static org.mockito.Mockito.times; | ||
33 | 35 | import static org.mockito.Mockito.verify; |
34 | 36 | import static org.mockito.Mockito.when; |
35 | 37 | |
38 | +import android.app.ActivityManager; | |
36 | 39 | import android.app.Notification; |
37 | 40 | import android.app.trust.TrustManager; |
38 | 41 | import android.content.Context; |
@@ -47,6 +50,7 @@ import android.os.PowerManager; | ||
47 | 50 | import android.os.RemoteException; |
48 | 51 | import android.os.UserHandle; |
49 | 52 | import android.service.notification.StatusBarNotification; |
53 | +import android.service.notification.NotificationListenerService.RankingMap; | |
50 | 54 | import android.support.test.filters.SmallTest; |
51 | 55 | import android.support.test.metricshelper.MetricsAsserts; |
52 | 56 | import android.testing.AndroidTestingRunner; |
@@ -65,7 +69,11 @@ import com.android.internal.logging.testing.FakeMetricsLogger; | ||
65 | 69 | import com.android.internal.statusbar.IStatusBarService; |
66 | 70 | import com.android.keyguard.KeyguardHostView.OnDismissAction; |
67 | 71 | import com.android.keyguard.KeyguardStatusView; |
72 | +import com.android.systemui.ForegroundServiceController; | |
68 | 73 | import com.android.systemui.R; |
74 | +import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender; | |
75 | +import com.android.systemui.statusbar.NotificationLifetimeExtender; | |
76 | +import com.android.systemui.statusbar.RemoteInputController; | |
69 | 77 | import com.android.systemui.SysuiTestCase; |
70 | 78 | import com.android.systemui.assist.AssistManager; |
71 | 79 | import com.android.systemui.keyguard.WakefulnessLifecycle; |
@@ -89,13 +97,20 @@ import org.junit.runner.RunWith; | ||
89 | 97 | |
90 | 98 | import java.io.ByteArrayOutputStream; |
91 | 99 | import java.io.PrintWriter; |
100 | + | |
92 | 101 | import java.util.ArrayList; |
102 | +import java.util.Map; | |
103 | + | |
104 | +import junit.framework.Assert; | |
93 | 105 | |
94 | 106 | @SmallTest |
95 | 107 | @RunWith(AndroidTestingRunner.class) |
96 | 108 | @RunWithLooper |
97 | 109 | public class StatusBarTest extends SysuiTestCase { |
98 | 110 | |
111 | + private static final String TEST_PACKAGE_NAME = "test"; | |
112 | + private static final int TEST_UID = 123; | |
113 | + | |
99 | 114 | StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; |
100 | 115 | UnlockMethodCache mUnlockMethodCache; |
101 | 116 | KeyguardIndicationController mKeyguardIndicationController; |
@@ -108,8 +123,12 @@ public class StatusBarTest extends SysuiTestCase { | ||
108 | 123 | SystemServicesProxy mSystemServicesProxy; |
109 | 124 | NotificationPanelView mNotificationPanelView; |
110 | 125 | IStatusBarService mBarService; |
126 | + RemoteInputController mRemoteInputController; | |
127 | + ForegroundServiceController mForegroundServiceController; | |
111 | 128 | ArrayList<Entry> mNotificationList; |
112 | 129 | private DisplayMetrics mDisplayMetrics = new DisplayMetrics(); |
130 | + private ForegroundServiceLifetimeExtender mFGSExtender = | |
131 | + new ForegroundServiceLifetimeExtender(); | |
113 | 132 | |
114 | 133 | @Before |
115 | 134 | public void setup() throws Exception { |
@@ -135,6 +154,8 @@ public class StatusBarTest extends SysuiTestCase { | ||
135 | 154 | when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0)); |
136 | 155 | mNotificationList = mock(ArrayList.class); |
137 | 156 | IPowerManager powerManagerService = mock(IPowerManager.class); |
157 | + mRemoteInputController = mock(RemoteInputController.class); | |
158 | + mForegroundServiceController = mock(ForegroundServiceController.class); | |
138 | 159 | HandlerThread handlerThread = new HandlerThread("TestThread"); |
139 | 160 | handlerThread.start(); |
140 | 161 | mPowerManager = new PowerManager(mContext, powerManagerService, |
@@ -143,10 +164,11 @@ public class StatusBarTest extends SysuiTestCase { | ||
143 | 164 | mBarService = mock(IStatusBarService.class); |
144 | 165 | |
145 | 166 | mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger); |
167 | + mFGSExtender.setCallback(key -> mStatusBar.removeNotification(key, mock(RankingMap.class))); | |
146 | 168 | mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache, |
147 | 169 | mKeyguardIndicationController, mStackScroller, mHeadsUpManager, |
148 | 170 | mNotificationData, mPowerManager, mSystemServicesProxy, mNotificationPanelView, |
149 | - mBarService); | |
171 | + mBarService, mFGSExtender, mRemoteInputController, mForegroundServiceController); | |
150 | 172 | mStatusBar.mContext = mContext; |
151 | 173 | mStatusBar.mComponents = mContext.getComponents(); |
152 | 174 | doAnswer(invocation -> { |
@@ -428,6 +450,54 @@ public class StatusBarTest extends SysuiTestCase { | ||
428 | 450 | |
429 | 451 | |
430 | 452 | @Test |
453 | + public void testForegroundServiceNotificationKeptForFiveSeconds() throws Exception { | |
454 | + RankingMap rm = mock(RankingMap.class); | |
455 | + | |
456 | + // sbn posted "just now" | |
457 | + Notification n = new Notification.Builder(mContext, "") | |
458 | + .setSmallIcon(R.drawable.ic_person) | |
459 | + .setContentTitle("Title") | |
460 | + .setContentText("Text") | |
461 | + .build(); | |
462 | + n.flags |= Notification.FLAG_FOREGROUND_SERVICE; | |
463 | + StatusBarNotification sbn = | |
464 | + new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, | |
465 | + 0, n, new UserHandle(ActivityManager.getCurrentUser()), null, | |
466 | + System.currentTimeMillis()); | |
467 | + NotificationData.Entry entry = new NotificationData.Entry(sbn); | |
468 | + when(mNotificationData.get(any())).thenReturn(entry); | |
469 | + mStatusBar.removeNotification(sbn.getKey(), rm); | |
470 | + Map<NotificationData.Entry, NotificationLifetimeExtender> map = | |
471 | + mStatusBar.getRetainedNotificationMap(); | |
472 | + Assert.assertTrue(map.containsKey(entry)); | |
473 | + } | |
474 | + | |
475 | + @Test | |
476 | + public void testForegroundServiceNotification_notRetainedIfShownForFiveSeconds() | |
477 | + throws Exception { | |
478 | + | |
479 | + RankingMap rm = mock(RankingMap.class); | |
480 | + | |
481 | + // sbn posted "more than 5 seconds ago" | |
482 | + Notification n = new Notification.Builder(mContext, "") | |
483 | + .setSmallIcon(R.drawable.ic_person) | |
484 | + .setContentTitle("Title") | |
485 | + .setContentText("Text") | |
486 | + .build(); | |
487 | + n.flags |= Notification.FLAG_FOREGROUND_SERVICE; | |
488 | + StatusBarNotification sbn = | |
489 | + new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, | |
490 | + 0, n, new UserHandle(ActivityManager.getCurrentUser()), null, | |
491 | + System.currentTimeMillis() - MIN_FGS_TIME_MS - 1); | |
492 | + NotificationData.Entry entry = new NotificationData.Entry(sbn); | |
493 | + when(mNotificationData.get(any())).thenReturn(entry); | |
494 | + mStatusBar.removeNotification(sbn.getKey(), rm); | |
495 | + Map<NotificationData.Entry, NotificationLifetimeExtender> map = | |
496 | + mStatusBar.getRetainedNotificationMap(); | |
497 | + Assert.assertFalse(map.containsKey(entry)); | |
498 | + } | |
499 | + | |
500 | + @Test | |
431 | 501 | public void testLogHidden() { |
432 | 502 | try { |
433 | 503 | mStatusBar.handleVisibleToUserChanged(false); |
@@ -515,7 +585,8 @@ public class StatusBarTest extends SysuiTestCase { | ||
515 | 585 | UnlockMethodCache unlock, KeyguardIndicationController key, |
516 | 586 | NotificationStackScrollLayout stack, HeadsUpManager hum, NotificationData nd, |
517 | 587 | PowerManager pm, SystemServicesProxy ssp, NotificationPanelView panelView, |
518 | - IStatusBarService barService) { | |
588 | + IStatusBarService barService, ForegroundServiceLifetimeExtender fgsExtender, | |
589 | + RemoteInputController ric, ForegroundServiceController fsc) { | |
519 | 590 | mStatusBarKeyguardViewManager = man; |
520 | 591 | mUnlockMethodCache = unlock; |
521 | 592 | mKeyguardIndicationController = key; |
@@ -527,6 +598,10 @@ public class StatusBarTest extends SysuiTestCase { | ||
527 | 598 | mSystemServicesProxy = ssp; |
528 | 599 | mNotificationPanel = panelView; |
529 | 600 | mBarService = barService; |
601 | + mFGSExtender = fgsExtender; | |
602 | + mRemoteInputController = ric; | |
603 | + mForegroundServiceController = fsc; | |
604 | + | |
530 | 605 | mWakefulnessLifecycle = createAwakeWakefulnessLifecycle(); |
531 | 606 | mScrimController = mock(ScrimController.class); |
532 | 607 | } |
@@ -547,4 +622,4 @@ public class StatusBarTest extends SysuiTestCase { | ||
547 | 622 | mState = state; |
548 | 623 | } |
549 | 624 | } |
550 | -} | |
\ No newline at end of file | ||
625 | +} |
@@ -3574,7 +3574,8 @@ public class ActivityManagerService extends IActivityManager.Stub | ||
3574 | 3574 | final int procCount = procs.size(); |
3575 | 3575 | for (int i = 0; i < procCount; i++) { |
3576 | 3576 | final int procUid = procs.keyAt(i); |
3577 | - if (UserHandle.isApp(procUid) || !UserHandle.isSameUser(procUid, uid)) { | |
3577 | + if (UserHandle.isApp(procUid) || !UserHandle.isSameUser(procUid, uid) | |
3578 | + || UserHandle.isIsolated(procUid)) { | |
3578 | 3579 | // Don't use an app process or different user process for system component. |
3579 | 3580 | continue; |
3580 | 3581 | } |
@@ -21,6 +21,7 @@ import static android.Manifest.permission.CONNECTIVITY_INTERNAL; | ||
21 | 21 | import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; |
22 | 22 | import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; |
23 | 23 | import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; |
24 | +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; | |
24 | 25 | import static android.content.pm.PackageManager.GET_PERMISSIONS; |
25 | 26 | |
26 | 27 | import android.content.BroadcastReceiver; |
@@ -39,6 +40,8 @@ import android.os.UserManager; | ||
39 | 40 | import android.text.TextUtils; |
40 | 41 | import android.util.Log; |
41 | 42 | |
43 | +import com.android.internal.util.ArrayUtils; | |
44 | + | |
42 | 45 | import java.util.ArrayList; |
43 | 46 | import java.util.HashMap; |
44 | 47 | import java.util.HashSet; |
@@ -150,15 +153,13 @@ public class PermissionMonitor { | ||
150 | 153 | update(mUsers, mApps, true); |
151 | 154 | } |
152 | 155 | |
153 | - private boolean hasPermission(PackageInfo app, String permission) { | |
154 | - if (app.requestedPermissions != null) { | |
155 | - for (String p : app.requestedPermissions) { | |
156 | - if (permission.equals(p)) { | |
157 | - return true; | |
158 | - } | |
159 | - } | |
156 | + private boolean hasPermission(final PackageInfo app, final String permission) { | |
157 | + if (app.requestedPermissions == null || app.requestedPermissionsFlags == null) { | |
158 | + return false; | |
160 | 159 | } |
161 | - return false; | |
160 | + final int index = ArrayUtils.indexOf(app.requestedPermissions, permission); | |
161 | + if (index < 0 || index >= app.requestedPermissionsFlags.length) return false; | |
162 | + return (app.requestedPermissionsFlags[index] & REQUESTED_PERMISSION_GRANTED) != 0; | |
162 | 163 | } |
163 | 164 | |
164 | 165 | private boolean hasNetworkPermission(PackageInfo app) { |
@@ -4746,8 +4746,15 @@ public class NotificationManagerService extends SystemService { | ||
4746 | 4746 | userId, mustHaveFlags, mustNotHaveFlags, reason, listenerName); |
4747 | 4747 | |
4748 | 4748 | synchronized (mNotificationLock) { |
4749 | - // Look for the notification, searching both the posted and enqueued lists. | |
4750 | - NotificationRecord r = findNotificationLocked(pkg, tag, id, userId); | |
4749 | + // If the notification is currently enqueued, repost this runnable so it has a | |
4750 | + // chance to notify listeners | |
4751 | + if ((findNotificationByListLocked( | |
4752 | + mEnqueuedNotifications, pkg, tag, id, userId)) != null) { | |
4753 | + mHandler.post(this); | |
4754 | + } | |
4755 | + // Look for the notification in the posted list, since we already checked enq | |
4756 | + NotificationRecord r = findNotificationByListLocked( | |
4757 | + mNotificationList, pkg, tag, id, userId); | |
4751 | 4758 | if (r != null) { |
4752 | 4759 | // The notification was found, check if it should be removed. |
4753 | 4760 |
@@ -440,6 +440,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub { | ||
440 | 440 | |
441 | 441 | params.installFlags &= ~PackageManager.INSTALL_FROM_ADB; |
442 | 442 | params.installFlags &= ~PackageManager.INSTALL_ALL_USERS; |
443 | + params.installFlags &= ~PackageManager.INSTALL_ALLOW_TEST; | |
443 | 444 | params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; |
444 | 445 | if ((params.installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0 |
445 | 446 | && !mPm.isCallerVerifier(callingUid)) { |
@@ -19557,7 +19557,7 @@ public class PackageManagerService extends IPackageManager.Stub | ||
19557 | 19557 | continue; |
19558 | 19558 | } |
19559 | 19559 | List<VersionedPackage> libClientPackages = getPackagesUsingSharedLibraryLPr( |
19560 | - libEntry.info, 0, currUserId); | |
19560 | + libEntry.info, MATCH_KNOWN_PACKAGES, currUserId); | |
19561 | 19561 | if (!ArrayUtils.isEmpty(libClientPackages)) { |
19562 | 19562 | Slog.w(TAG, "Not removing package " + pkg.manifestPackageName |
19563 | 19563 | + " hosting lib " + libEntry.info.getName() + " version " |
@@ -19890,7 +19890,8 @@ public class PackageManagerService extends IPackageManager.Stub | ||
19890 | 19890 | * Tries to delete system package. |
19891 | 19891 | */ |
19892 | 19892 | private boolean deleteSystemPackageLIF(PackageParser.Package deletedPkg, |
19893 | - PackageSetting deletedPs, int[] allUserHandles, int flags, PackageRemovedInfo outInfo, | |
19893 | + PackageSetting deletedPs, int[] allUserHandles, int flags, | |
19894 | + @Nullable PackageRemovedInfo outInfo, | |
19894 | 19895 | boolean writeSettings) { |
19895 | 19896 | if (deletedPs.parentPackageName != null) { |
19896 | 19897 | Slog.w(TAG, "Attempt to delete child system package " + deletedPkg.packageName); |
@@ -19898,7 +19899,7 @@ public class PackageManagerService extends IPackageManager.Stub | ||
19898 | 19899 | } |
19899 | 19900 | |
19900 | 19901 | final boolean applyUserRestrictions |
19901 | - = (allUserHandles != null) && (outInfo.origUsers != null); | |
19902 | + = (allUserHandles != null) && outInfo != null && (outInfo.origUsers != null); | |
19902 | 19903 | final PackageSetting disabledPs; |
19903 | 19904 | // Confirm if the system package has been updated |
19904 | 19905 | // An updated system app can be deleted. This will also have to restore |
@@ -19928,19 +19929,21 @@ public class PackageManagerService extends IPackageManager.Stub | ||
19928 | 19929 | } |
19929 | 19930 | } |
19930 | 19931 | |
19931 | - // Delete the updated package | |
19932 | - outInfo.isRemovedPackageSystemUpdate = true; | |
19933 | - if (outInfo.removedChildPackages != null) { | |
19934 | - final int childCount = (deletedPs.childPackageNames != null) | |
19935 | - ? deletedPs.childPackageNames.size() : 0; | |
19936 | - for (int i = 0; i < childCount; i++) { | |
19937 | - String childPackageName = deletedPs.childPackageNames.get(i); | |
19938 | - if (disabledPs.childPackageNames != null && disabledPs.childPackageNames | |
19939 | - .contains(childPackageName)) { | |
19940 | - PackageRemovedInfo childInfo = outInfo.removedChildPackages.get( | |
19941 | - childPackageName); | |
19942 | - if (childInfo != null) { | |
19943 | - childInfo.isRemovedPackageSystemUpdate = true; | |
19932 | + if (outInfo != null) { | |
19933 | + // Delete the updated package | |
19934 | + outInfo.isRemovedPackageSystemUpdate = true; | |
19935 | + if (outInfo.removedChildPackages != null) { | |
19936 | + final int childCount = (deletedPs.childPackageNames != null) | |
19937 | + ? deletedPs.childPackageNames.size() : 0; | |
19938 | + for (int i = 0; i < childCount; i++) { | |
19939 | + String childPackageName = deletedPs.childPackageNames.get(i); | |
19940 | + if (disabledPs.childPackageNames != null && disabledPs.childPackageNames | |
19941 | + .contains(childPackageName)) { | |
19942 | + PackageRemovedInfo childInfo = outInfo.removedChildPackages.get( | |
19943 | + childPackageName); | |
19944 | + if (childInfo != null) { | |
19945 | + childInfo.isRemovedPackageSystemUpdate = true; | |
19946 | + } | |
19944 | 19947 | } |
19945 | 19948 | } |
19946 | 19949 | } |
@@ -24203,9 +24206,9 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); | ||
24203 | 24206 | mSettings.writeKernelMappingLPr(ps); |
24204 | 24207 | } |
24205 | 24208 | |
24206 | - final UserManager um = mContext.getSystemService(UserManager.class); | |
24209 | + final UserManagerService um = sUserManager; | |
24207 | 24210 | UserManagerInternal umInternal = getUserManagerInternal(); |
24208 | - for (UserInfo user : um.getUsers()) { | |
24211 | + for (UserInfo user : um.getUsers(false /* excludeDying */)) { | |
24209 | 24212 | final int flags; |
24210 | 24213 | if (umInternal.isUserUnlockingOrUnlocked(user.id)) { |
24211 | 24214 | flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE; |
@@ -24845,8 +24848,9 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName()); | ||
24845 | 24848 | continue; |
24846 | 24849 | } |
24847 | 24850 | final String packageName = ps.pkg.packageName; |
24848 | - // Skip over if system app | |
24849 | - if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0) { | |
24851 | + // Skip over if system app or static shared library | |
24852 | + if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0 | |
24853 | + || !TextUtils.isEmpty(ps.pkg.staticSharedLibName)) { | |
24850 | 24854 | continue; |
24851 | 24855 | } |
24852 | 24856 | if (DEBUG_CLEAN_APKS) { |
@@ -2618,6 +2618,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { | ||
2618 | 2618 | attrs.hideTimeoutMilliseconds = TOAST_WINDOW_TIMEOUT; |
2619 | 2619 | } |
2620 | 2620 | attrs.windowAnimations = com.android.internal.R.style.Animation_Toast; |
2621 | + // Toasts can't be clickable | |
2622 | + attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; | |
2621 | 2623 | break; |
2622 | 2624 | } |
2623 | 2625 |
@@ -0,0 +1,148 @@ | ||
1 | +/* | |
2 | + * Copyright (C) 2019 The Android Open Source Project | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package com.android.server.wallpaper; | |
18 | + | |
19 | +import static android.opengl.EGL14.EGL_ALPHA_SIZE; | |
20 | +import static android.opengl.EGL14.EGL_BLUE_SIZE; | |
21 | +import static android.opengl.EGL14.EGL_CONFIG_CAVEAT; | |
22 | +import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION; | |
23 | +import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY; | |
24 | +import static android.opengl.EGL14.EGL_DEPTH_SIZE; | |
25 | +import static android.opengl.EGL14.EGL_GREEN_SIZE; | |
26 | +import static android.opengl.EGL14.EGL_HEIGHT; | |
27 | +import static android.opengl.EGL14.EGL_NONE; | |
28 | +import static android.opengl.EGL14.EGL_NO_CONTEXT; | |
29 | +import static android.opengl.EGL14.EGL_NO_DISPLAY; | |
30 | +import static android.opengl.EGL14.EGL_NO_SURFACE; | |
31 | +import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; | |
32 | +import static android.opengl.EGL14.EGL_RED_SIZE; | |
33 | +import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; | |
34 | +import static android.opengl.EGL14.EGL_STENCIL_SIZE; | |
35 | +import static android.opengl.EGL14.EGL_WIDTH; | |
36 | +import static android.opengl.EGL14.eglChooseConfig; | |
37 | +import static android.opengl.EGL14.eglCreateContext; | |
38 | +import static android.opengl.EGL14.eglCreatePbufferSurface; | |
39 | +import static android.opengl.EGL14.eglDestroyContext; | |
40 | +import static android.opengl.EGL14.eglDestroySurface; | |
41 | +import static android.opengl.EGL14.eglGetDisplay; | |
42 | +import static android.opengl.EGL14.eglGetError; | |
43 | +import static android.opengl.EGL14.eglInitialize; | |
44 | +import static android.opengl.EGL14.eglMakeCurrent; | |
45 | +import static android.opengl.EGL14.eglTerminate; | |
46 | +import static android.opengl.GLES20.GL_MAX_TEXTURE_SIZE; | |
47 | +import static android.opengl.GLES20.glGetIntegerv; | |
48 | + | |
49 | +import android.opengl.EGLConfig; | |
50 | +import android.opengl.EGLContext; | |
51 | +import android.opengl.EGLDisplay; | |
52 | +import android.opengl.EGLSurface; | |
53 | +import android.opengl.GLUtils; | |
54 | +import android.os.SystemProperties; | |
55 | +import android.util.Log; | |
56 | + | |
57 | +class GLHelper { | |
58 | + private static final String TAG = GLHelper.class.getSimpleName(); | |
59 | + private static final int sMaxTextureSize; | |
60 | + | |
61 | + static { | |
62 | + int maxTextureSize = SystemProperties.getInt("sys.max_texture_size", 0); | |
63 | + sMaxTextureSize = maxTextureSize > 0 ? maxTextureSize : retrieveTextureSizeFromGL(); | |
64 | + } | |
65 | + | |
66 | + private static int retrieveTextureSizeFromGL() { | |
67 | + try { | |
68 | + String err; | |
69 | + | |
70 | + // Before we can retrieve info from GL, | |
71 | + // we have to create EGLContext, EGLConfig and EGLDisplay first. | |
72 | + // We will fail at querying info from GL once one of above failed. | |
73 | + // When this happens, we will use defValue instead. | |
74 | + EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); | |
75 | + if (eglDisplay == null || eglDisplay == EGL_NO_DISPLAY) { | |
76 | + err = "eglGetDisplay failed: " + GLUtils.getEGLErrorString(eglGetError()); | |
77 | + throw new RuntimeException(err); | |
78 | + } | |
79 | + | |
80 | + if (!eglInitialize(eglDisplay, null, 0 /* majorOffset */, null, 1 /* minorOffset */)) { | |
81 | + err = "eglInitialize failed: " + GLUtils.getEGLErrorString(eglGetError()); | |
82 | + throw new RuntimeException(err); | |
83 | + } | |
84 | + | |
85 | + EGLConfig eglConfig = null; | |
86 | + int[] configsCount = new int[1]; | |
87 | + EGLConfig[] configs = new EGLConfig[1]; | |
88 | + int[] configSpec = new int[] { | |
89 | + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, | |
90 | + EGL_RED_SIZE, 8, | |
91 | + EGL_GREEN_SIZE, 8, | |
92 | + EGL_BLUE_SIZE, 8, | |
93 | + EGL_ALPHA_SIZE, 0, | |
94 | + EGL_DEPTH_SIZE, 0, | |
95 | + EGL_STENCIL_SIZE, 0, | |
96 | + EGL_CONFIG_CAVEAT, EGL_NONE, | |
97 | + EGL_NONE | |
98 | + }; | |
99 | + | |
100 | + if (!eglChooseConfig(eglDisplay, configSpec, 0 /* attrib_listOffset */, | |
101 | + configs, 0 /* configOffset */, 1 /* config_size */, | |
102 | + configsCount, 0 /* num_configOffset */)) { | |
103 | + err = "eglChooseConfig failed: " + GLUtils.getEGLErrorString(eglGetError()); | |
104 | + throw new RuntimeException(err); | |
105 | + } else if (configsCount[0] > 0) { | |
106 | + eglConfig = configs[0]; | |
107 | + } | |
108 | + | |
109 | + if (eglConfig == null) { | |
110 | + throw new RuntimeException("eglConfig not initialized!"); | |
111 | + } | |
112 | + | |
113 | + int[] attr_list = new int[] {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; | |
114 | + EGLContext eglContext = eglCreateContext( | |
115 | + eglDisplay, eglConfig, EGL_NO_CONTEXT, attr_list, 0 /* offset */); | |
116 | + | |
117 | + if (eglContext == null || eglContext == EGL_NO_CONTEXT) { | |
118 | + err = "eglCreateContext failed: " + GLUtils.getEGLErrorString(eglGetError()); | |
119 | + throw new RuntimeException(err); | |
120 | + } | |
121 | + | |
122 | + // We create a push buffer temporarily for querying info from GL. | |
123 | + int[] attrs = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; | |
124 | + EGLSurface eglSurface = | |
125 | + eglCreatePbufferSurface(eglDisplay, eglConfig, attrs, 0 /* offset */); | |
126 | + eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext); | |
127 | + | |
128 | + // Now, we are ready to query the info from GL. | |
129 | + int[] maxSize = new int[1]; | |
130 | + glGetIntegerv(GL_MAX_TEXTURE_SIZE, maxSize, 0 /* offset */); | |
131 | + | |
132 | + // We have got the info we want, release all egl resources. | |
133 | + eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); | |
134 | + eglDestroySurface(eglDisplay, eglSurface); | |
135 | + eglDestroyContext(eglDisplay, eglContext); | |
136 | + eglTerminate(eglDisplay); | |
137 | + return maxSize[0]; | |
138 | + } catch (RuntimeException e) { | |
139 | + Log.w(TAG, "Retrieve from GL failed", e); | |
140 | + return Integer.MAX_VALUE; | |
141 | + } | |
142 | + } | |
143 | + | |
144 | + static int getMaxTextureSize() { | |
145 | + return sMaxTextureSize; | |
146 | + } | |
147 | +} | |
148 | + |
@@ -125,6 +125,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub { | ||
125 | 125 | static final boolean DEBUG = false; |
126 | 126 | static final boolean DEBUG_LIVE = DEBUG || true; |
127 | 127 | |
128 | + // This 100MB limitation is defined in DisplayListCanvas. | |
129 | + private static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; | |
130 | + | |
128 | 131 | public static class Lifecycle extends SystemService { |
129 | 132 | private WallpaperManagerService mService; |
130 | 133 |
@@ -522,7 +525,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub { | ||
522 | 525 | } |
523 | 526 | |
524 | 527 | // scale if the crop height winds up not matching the recommended metrics |
525 | - needScale = (wallpaper.height != cropHint.height()); | |
528 | + // also take care of invalid dimensions. | |
529 | + needScale = wallpaper.height != cropHint.height() | |
530 | + || cropHint.height() > GLHelper.getMaxTextureSize() | |
531 | + || cropHint.width() > GLHelper.getMaxTextureSize(); | |
526 | 532 | |
527 | 533 | if (DEBUG) { |
528 | 534 | Slog.v(TAG, "crop: w=" + cropHint.width() + " h=" + cropHint.height()); |
@@ -534,14 +540,29 @@ public class WallpaperManagerService extends IWallpaperManager.Stub { | ||
534 | 540 | if (!needCrop && !needScale) { |
535 | 541 | // Simple case: the nominal crop fits what we want, so we take |
536 | 542 | // the whole thing and just copy the image file directly. |
537 | - if (DEBUG) { | |
538 | - Slog.v(TAG, "Null crop of new wallpaper; copying"); | |
543 | + | |
544 | + // TODO: It is not accurate to estimate bitmap size without decoding it, | |
545 | + // may be we can try to remove this optimized way in the future, | |
546 | + // that means, we will always go into the 'else' block. | |
547 | + | |
548 | + // This is just a quick estimation, may be smaller than it is. | |
549 | + long estimateSize = options.outWidth * options.outHeight * 4; | |
550 | + | |
551 | + // A bitmap over than MAX_BITMAP_SIZE will make drawBitmap() fail. | |
552 | + // Please see: DisplayListCanvas#throwIfCannotDraw. | |
553 | + if (estimateSize < MAX_BITMAP_SIZE) { | |
554 | + success = FileUtils.copyFile(wallpaper.wallpaperFile, wallpaper.cropFile); | |
539 | 555 | } |
540 | - success = FileUtils.copyFile(wallpaper.wallpaperFile, wallpaper.cropFile); | |
556 | + | |
541 | 557 | if (!success) { |
542 | 558 | wallpaper.cropFile.delete(); |
543 | 559 | // TODO: fall back to default wallpaper in this case |
544 | 560 | } |
561 | + | |
562 | + if (DEBUG) { | |
563 | + Slog.v(TAG, "Null crop of new wallpaper, estimate size=" + estimateSize | |
564 | + + ", success=" + success); | |
565 | + } | |
545 | 566 | } else { |
546 | 567 | // Fancy case: crop and scale. First, we decode and scale down if appropriate. |
547 | 568 | FileOutputStream f = null; |
@@ -555,48 +576,78 @@ public class WallpaperManagerService extends IWallpaperManager.Stub { | ||
555 | 576 | // We calculate the largest power-of-two under the actual ratio rather than |
556 | 577 | // just let the decode take care of it because we also want to remap where the |
557 | 578 | // cropHint rectangle lies in the decoded [super]rect. |
558 | - final BitmapFactory.Options scaler; | |
559 | 579 | final int actualScale = cropHint.height() / wallpaper.height; |
560 | 580 | int scale = 1; |
561 | - while (2*scale < actualScale) { | |
581 | + while (2*scale <= actualScale) { | |
562 | 582 | scale *= 2; |
563 | 583 | } |
564 | - if (scale > 1) { | |
565 | - scaler = new BitmapFactory.Options(); | |
566 | - scaler.inSampleSize = scale; | |
584 | + options.inSampleSize = scale; | |
585 | + options.inJustDecodeBounds = false; | |
586 | + | |
587 | + final Rect estimateCrop = new Rect(cropHint); | |
588 | + estimateCrop.scale(1f / options.inSampleSize); | |
589 | + final float hRatio = (float) wallpaper.height / estimateCrop.height(); | |
590 | + final int destHeight = (int) (estimateCrop.height() * hRatio); | |
591 | + final int destWidth = (int) (estimateCrop.width() * hRatio); | |
592 | + | |
593 | + // We estimated an invalid crop, try to adjust the cropHint to get a valid one. | |
594 | + if (destWidth > GLHelper.getMaxTextureSize()) { | |
595 | + int newHeight = (int) (wallpaper.height / hRatio); | |
596 | + int newWidth = (int) (wallpaper.width / hRatio); | |
597 | + | |
567 | 598 | if (DEBUG) { |
568 | - Slog.v(TAG, "Downsampling cropped rect with scale " + scale); | |
599 | + Slog.v(TAG, "Invalid crop dimensions, trying to adjust."); | |
569 | 600 | } |
570 | - } else { | |
571 | - scaler = null; | |
601 | + | |
602 | + estimateCrop.set(cropHint); | |
603 | + estimateCrop.left += (cropHint.width() - newWidth) / 2; | |
604 | + estimateCrop.top += (cropHint.height() - newHeight) / 2; | |
605 | + estimateCrop.right = estimateCrop.left + newWidth; | |
606 | + estimateCrop.bottom = estimateCrop.top + newHeight; | |
607 | + cropHint.set(estimateCrop); | |
608 | + estimateCrop.scale(1f / options.inSampleSize); | |
609 | + } | |
610 | + | |
611 | + // We've got the safe cropHint; now we want to scale it properly to | |
612 | + // the desired rectangle. | |
613 | + // That's a height-biased operation: make it fit the hinted height. | |
614 | + final int safeHeight = (int) (estimateCrop.height() * hRatio); | |
615 | + final int safeWidth = (int) (estimateCrop.width() * hRatio); | |
616 | + | |
617 | + if (DEBUG) { | |
618 | + Slog.v(TAG, "Decode parameters:"); | |
619 | + Slog.v(TAG, " cropHint=" + cropHint + ", estimateCrop=" + estimateCrop); | |
620 | + Slog.v(TAG, " down sampling=" + options.inSampleSize | |
621 | + + ", hRatio=" + hRatio); | |
622 | + Slog.v(TAG, " dest=" + destWidth + "x" + destHeight); | |
623 | + Slog.v(TAG, " safe=" + safeWidth + "x" + safeHeight); | |
624 | + Slog.v(TAG, " maxTextureSize=" + GLHelper.getMaxTextureSize()); | |
572 | 625 | } |
573 | - Bitmap cropped = decoder.decodeRegion(cropHint, scaler); | |
626 | + | |
627 | + Bitmap cropped = decoder.decodeRegion(cropHint, options); | |
574 | 628 | decoder.recycle(); |
575 | 629 | |
576 | 630 | if (cropped == null) { |
577 | 631 | Slog.e(TAG, "Could not decode new wallpaper"); |
578 | 632 | } else { |
579 | - // We've got the extracted crop; now we want to scale it properly to | |
580 | - // the desired rectangle. That's a height-biased operation: make it | |
581 | - // fit the hinted height, and accept whatever width we end up with. | |
582 | - cropHint.offsetTo(0, 0); | |
583 | - cropHint.right /= scale; // adjust by downsampling factor | |
584 | - cropHint.bottom /= scale; | |
585 | - final float heightR = ((float)wallpaper.height) / ((float)cropHint.height()); | |
586 | - if (DEBUG) { | |
587 | - Slog.v(TAG, "scale " + heightR + ", extracting " + cropHint); | |
588 | - } | |
589 | - final int destWidth = (int)(cropHint.width() * heightR); | |
633 | + // We are safe to create final crop with safe dimensions now. | |
590 | 634 | final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped, |
591 | - destWidth, wallpaper.height, true); | |
635 | + safeWidth, safeHeight, true); | |
592 | 636 | if (DEBUG) { |
593 | 637 | Slog.v(TAG, "Final extract:"); |
594 | 638 | Slog.v(TAG, " dims: w=" + wallpaper.width |
595 | 639 | + " h=" + wallpaper.height); |
596 | - Slog.v(TAG, " out: w=" + finalCrop.getWidth() | |
640 | + Slog.v(TAG, " out: w=" + finalCrop.getWidth() | |
597 | 641 | + " h=" + finalCrop.getHeight()); |
598 | 642 | } |
599 | 643 | |
644 | + // A bitmap over than MAX_BITMAP_SIZE will make drawBitmap() fail. | |
645 | + // Please see: DisplayListCanvas#throwIfCannotDraw. | |
646 | + if (finalCrop.getByteCount() > MAX_BITMAP_SIZE) { | |
647 | + throw new RuntimeException( | |
648 | + "Too large bitmap, limit=" + MAX_BITMAP_SIZE); | |
649 | + } | |
650 | + | |
600 | 651 | f = new FileOutputStream(wallpaper.cropFile); |
601 | 652 | bos = new BufferedOutputStream(f, 32*1024); |
602 | 653 | finalCrop.compress(Bitmap.CompressFormat.JPEG, 100, bos); |
@@ -1509,6 +1560,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub { | ||
1509 | 1560 | if (!isWallpaperSupported(callingPackage)) { |
1510 | 1561 | return; |
1511 | 1562 | } |
1563 | + | |
1564 | + // Make sure both width and height are not larger than max texture size. | |
1565 | + width = Math.min(width, GLHelper.getMaxTextureSize()); | |
1566 | + height = Math.min(height, GLHelper.getMaxTextureSize()); | |
1567 | + | |
1512 | 1568 | synchronized (mLock) { |
1513 | 1569 | int userId = UserHandle.getCallingUserId(); |
1514 | 1570 | WallpaperData wallpaper = getWallpaperSafeLocked(userId, FLAG_SYSTEM); |
@@ -546,6 +546,21 @@ public class NotificationManagerServiceTest extends NotificationTestCase { | ||
546 | 546 | assertEquals(0, mNotificationManagerService.getNotificationRecordCount()); |
547 | 547 | } |
548 | 548 | |
549 | + | |
550 | + @Test | |
551 | + public void testCancelImmediatelyAfterEnqueueNotifiesListeners_ForegroundServiceFlag() | |
552 | + throws Exception { | |
553 | + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; | |
554 | + sbn.getNotification().flags = | |
555 | + Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE; | |
556 | + mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", | |
557 | + sbn.getId(), sbn.getNotification(), sbn.getUserId()); | |
558 | + mBinderService.cancelNotificationWithTag(PKG, "tag", sbn.getId(), sbn.getUserId()); | |
559 | + waitForIdle(); | |
560 | + verify(mListeners, times(1)).notifyPostedLocked(any(), any()); | |
561 | + verify(mListeners, times(1)).notifyRemovedLocked(any(), anyInt()); | |
562 | + } | |
563 | + | |
549 | 564 | @Test |
550 | 565 | public void testCancelNotificationWhilePostedAndEnqueued() throws Exception { |
551 | 566 | mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, |