Compare commits
10 commits
a66561c10d
...
e59f0dc0f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e59f0dc0f7 | ||
|
|
5aa6cfdb9d | ||
|
|
c1130ab78e | ||
|
|
cb9e782f47 | ||
|
|
36ba443da8 | ||
|
|
77a7d3901d | ||
|
|
dac6a39634 | ||
|
|
b4540f01f9 | ||
|
|
6133ed66b3 | ||
|
|
6d906d8230 |
33 changed files with 16320 additions and 26 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.env/
|
||||
*.pyo
|
||||
PLAN.md
|
||||
47
README.md
47
README.md
|
|
@ -1,3 +1,9 @@
|
|||
__WARNING__: This repo is almost entirely vibe-coded. If you choose to use it, you should always manually verify the JSON / download patches against the ASB web page. This repo saves you from clicking on every single link just to download the patches, it does NOT absolve you from the responsibility of using the patches.
|
||||
|
||||
It is also possible that these patches do not apply cleanly over your AOSP checkout, because Google, in their infinite wisdom, does not provide links to version-specific forward-ports / backports when there are conflicts. It is outside of the scope of this repo to resolve that.
|
||||
|
||||
---
|
||||
|
||||
# Android Security Bulletin Scraper
|
||||
|
||||
A Python tool to scrape Android Security Bulletins from https://source.android.com/docs/security/bulletin/asb-overview.
|
||||
|
|
@ -19,17 +25,56 @@ A Python tool to scrape Android Security Bulletins from https://source.android.c
|
|||
|
||||
## Usage
|
||||
|
||||
### Scrape ASB Data
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python scrape_asb.py
|
||||
```
|
||||
|
||||
This will scrape all monthly bulletins from the past year and save them to `asb_data/`.
|
||||
|
||||
## Output
|
||||
|
||||
The tool creates a `asb_data/` directory with one JSON file per month (e.g., `2025-12.json`).
|
||||
The scraper creates an `asb_data/` directory with one JSON file per month (e.g., `2025-12.json`).
|
||||
|
||||
The patch downloader creates an `patches/` directory with subdirectories organized by component path (e.g., `patches/2025-12/frameworks/base/`).
|
||||
|
||||
## Patch Downloader
|
||||
|
||||
After running the scraper, you can download git patches for specific vulnerabilities:
|
||||
|
||||
```bash
|
||||
python download_patches.py asb_data/2025-12.json -a 14 -s /path/to/android/source
|
||||
```
|
||||
|
||||
Or with custom output directory:
|
||||
|
||||
```bash
|
||||
python download_patches.py asb_data/2025-12.json -a 14 -s /path/to/android/source -o /custom/output/path
|
||||
```
|
||||
|
||||
**Note:** The `--source-tree` and `--android-version` flags are required.
|
||||
|
||||
The patch downloader:
|
||||
- Requires an existing Android source tree (no cloning)
|
||||
- Downloads only the commits referenced in the ASB JSON
|
||||
- Does not modify the source tree (read-only operations)
|
||||
- Organizes patches in subdirectories matching their AOSP location (e.g., `patches/2025-12/frameworks/base/`)
|
||||
- Saves patches with meaningful filenames: `{datetime}_{commit_subject}_{commit_hash}.patch`
|
||||
|
||||
### Required arguments
|
||||
|
||||
- `-a, --android-version`: Filter patches for a specific Android version (e.g., 14, 15, 16)
|
||||
- `-s, --source-tree`: Path to your existing Android source tree checkout
|
||||
|
||||
### Optional arguments
|
||||
|
||||
- `-o, --output-dir`: Output directory for patches (default: `patches/`)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- requests
|
||||
- beautifulsoup4
|
||||
- git (for cloning repositories, format-patch, and using existing source tree)
|
||||
|
|
|
|||
22
asb_data/2026-04.json
Normal file
22
asb_data/2026-04.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"month": "2026-04",
|
||||
"url": "https://source.android.com/docs/security/bulletin/2026/2026-04-01",
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"cve": "CVE-2026-0049",
|
||||
"references": [
|
||||
"https://android.googlesource.com/platform/external/dng_sdk/+/80a267ed1ac714acff455e85ae28c1732777d5b6",
|
||||
"https://android.googlesource.com/platform/frameworks/base/+/78ec493d6192240da0d0d37be93c6921eff403e7"
|
||||
],
|
||||
"type": "DoS",
|
||||
"severity": "Critical",
|
||||
"affected_android_versions": [
|
||||
"14",
|
||||
"15",
|
||||
"16",
|
||||
"16-qpr2"
|
||||
]
|
||||
}
|
||||
],
|
||||
"vulnerability_count": 1
|
||||
}
|
||||
199
download_patches.py
Executable file
199
download_patches.py
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Download git patches from Android Security Bulletins."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils import parse_android_version
|
||||
from utils.logging_config import logger, setup_logging
|
||||
|
||||
|
||||
def parse_patch_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Parse a patch URL to extract component path and commit hash."""
|
||||
match = re.match(r'https://android\.googlesource\.com/platform/([^/]+(?:/[^/]+)*)/\+/([a-f0-9]+)$', url)
|
||||
if match:
|
||||
component = match.group(1)
|
||||
commit_hash = match.group(2)
|
||||
return component, commit_hash
|
||||
return None, None
|
||||
|
||||
|
||||
def get_repo_url_from_patch_url(url: str) -> Optional[str]:
|
||||
"""Extract repo URL from a patch URL."""
|
||||
match = re.match(r'https://android\.googlesource\.com/platform/([^/]+(?:/[^/]+)*?)/\+', url)
|
||||
if match:
|
||||
component = match.group(1)
|
||||
return f'https://android.googlesource.com/platform/{component}.git'
|
||||
return None
|
||||
|
||||
|
||||
def fetch_commit(commit_hash: str, repo_path: Path, repo_url: str) -> bool:
|
||||
"""Fetch a specific commit from the remote repository."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'fetch', repo_url, commit_hash],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
timeout=60
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to fetch {commit_hash}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_commit_subject(commit_hash: str, repo_path: Path) -> Optional[str]:
|
||||
"""Get the commit subject line from the git repository."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'log', '-1', '--format=%s', commit_hash],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to get commit subject for {commit_hash}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_commit_datetime(commit_hash: str, repo_path: Path) -> Optional[str]:
|
||||
"""Get the commit datetime from the git repository."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'log', '-1', '--format=%ci', commit_hash],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to get commit datetime for {commit_hash}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Download git patches from ASB JSON')
|
||||
parser.add_argument('json_file', help='Path to ASB JSON file')
|
||||
parser.add_argument('--android-version', '-a', required=True, help='Filter by Android version (e.g., 14, 15, 16)')
|
||||
parser.add_argument('--output-dir', '-o', default='patches', help='Output directory for patches')
|
||||
parser.add_argument('--cve', '-c', help='Process only a specific CVE')
|
||||
parser.add_argument('--source-tree', '-s', required=True, help='Path to existing Android source tree')
|
||||
parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level (default: INFO)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
setup_logging(args.log_level)
|
||||
|
||||
with open(args.json_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
target_version = args.android_version
|
||||
if target_version:
|
||||
data['vulnerabilities'] = [
|
||||
v for v in data['vulnerabilities']
|
||||
if target_version in v.get('affected_android_versions', [])
|
||||
]
|
||||
|
||||
target_cve = args.cve
|
||||
if target_cve:
|
||||
data['vulnerabilities'] = [
|
||||
v for v in data['vulnerabilities'] if v['cve'] == target_cve
|
||||
]
|
||||
|
||||
output_dir = Path(args.output_dir) / data['month']
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"Processing {data['month']}: {data['url']}")
|
||||
logger.info(f"Total vulnerabilities: {data['vulnerability_count']}")
|
||||
if target_version:
|
||||
logger.info(f"Filtering for Android version: {target_version}")
|
||||
|
||||
component_patches = defaultdict(list)
|
||||
|
||||
for vuln in data['vulnerabilities']:
|
||||
cve = vuln['cve']
|
||||
for ref in vuln.get('references', []):
|
||||
component, commit_hash = parse_patch_url(ref)
|
||||
if component and commit_hash:
|
||||
component_patches[component].append((commit_hash, cve, ref))
|
||||
|
||||
if not component_patches:
|
||||
logger.info("No valid patches found")
|
||||
return
|
||||
|
||||
for component, patches in tqdm(component_patches.items(), desc="Processing components"):
|
||||
logger.info(f"\nProcessing {component} ({len(patches)} patches)...")
|
||||
|
||||
repo_path = Path(args.source_tree) / component
|
||||
if not repo_path.exists():
|
||||
logger.warning(f" Component {component} not found in source tree")
|
||||
continue
|
||||
logger.info(f" Using existing source tree: {repo_path}")
|
||||
|
||||
repo_url = get_repo_url_from_patch_url(patches[0][2])
|
||||
if not repo_url:
|
||||
logger.warning(f" Could not determine repo URL for {component}")
|
||||
continue
|
||||
|
||||
for commit_hash, cve, ref in tqdm(patches, desc=f"Fetching {component}", leave=False):
|
||||
if not fetch_commit(commit_hash, repo_path, repo_url):
|
||||
logger.warning(f" Failed to fetch {commit_hash[:12]} ({cve})")
|
||||
|
||||
patches_with_info = []
|
||||
for commit_hash, cve, ref in patches:
|
||||
commit_datetime = get_commit_datetime(commit_hash, repo_path)
|
||||
commit_subject = get_commit_subject(commit_hash, repo_path) or 'unknown'
|
||||
if commit_datetime:
|
||||
patches_with_info.append((commit_datetime, commit_hash, cve, commit_subject))
|
||||
else:
|
||||
patches_with_info.append(('unknown', commit_hash, cve, commit_subject))
|
||||
|
||||
sorted_patches = sorted(patches_with_info, key=lambda x: x[0])
|
||||
|
||||
for commit_datetime, commit_hash, cve, subject in sorted_patches:
|
||||
date_str = commit_datetime.replace(':', '-').replace(' ', '_') if commit_datetime != 'unknown' else 'unknown'
|
||||
safe_subject = re.sub(r'[^a-zA-Z0-9\-_ ]', '', subject)[:50].replace(' ', '_').strip('_')
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'format-patch', '-1', commit_hash, '--stdout'],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
patch_subdir = output_dir / component
|
||||
patch_subdir.mkdir(parents=True, exist_ok=True)
|
||||
patch_file = patch_subdir / f"{date_str}_{safe_subject}_{commit_hash[:12]}.patch"
|
||||
with open(patch_file, 'w') as f:
|
||||
f.write(result.stdout)
|
||||
logger.info(f" {commit_hash[:12]} ({cve}, {commit_datetime[:10]}) -> {component}/{patch_file.name}")
|
||||
else:
|
||||
logger.warning(f" Failed to create patch: {commit_hash[:12]} ({cve})")
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f" Timeout fetching {commit_hash[:12]} ({cve})")
|
||||
except Exception as e:
|
||||
logger.error(f" Error: {commit_hash[:12]} ({cve}) - {e}")
|
||||
|
||||
logger.info(f"\nDone! Patches saved to {output_dir}/")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
200
patches/2025-09/art_3eb64751e5a7.patch
Normal file
200
patches/2025-09/art_3eb64751e5a7.patch
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
From 3eb64751e5a7e69b06a0979edbdfe9c4d503ea57 Mon Sep 17 00:00:00 2001
|
||||
From: Nicolas Geoffray <ngeoffray@google.com>
|
||||
Date: Wed, 11 Jun 2025 14:48:25 +0100
|
||||
Subject: [PATCH] Throw an exception in JNI::NewObject for abstract classes.
|
||||
|
||||
Test: 863-serialization
|
||||
Bug: 421834866
|
||||
Flag: EXEMPT bugfix
|
||||
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a4826745b63bdab1db7536680e1c8e947a56f7be)
|
||||
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a5889a1a85117d0b168ff5bb9e0c123d2cd6409f)
|
||||
Merged-In: I4ccf22f85b4ae0325e9f8e29503149bbda533e86
|
||||
Change-Id: I4ccf22f85b4ae0325e9f8e29503149bbda533e86
|
||||
---
|
||||
runtime/jni/check_jni.cc | 10 ++--
|
||||
runtime/jni/jni_internal.cc | 21 ++++++++
|
||||
test/863-serialization/expected-stderr.txt | 0
|
||||
test/863-serialization/expected-stdout.txt | 0
|
||||
test/863-serialization/info.txt | 2 +
|
||||
test/863-serialization/src/Main.java | 62 ++++++++++++++++++++++
|
||||
6 files changed, 90 insertions(+), 5 deletions(-)
|
||||
create mode 100644 test/863-serialization/expected-stderr.txt
|
||||
create mode 100644 test/863-serialization/expected-stdout.txt
|
||||
create mode 100644 test/863-serialization/info.txt
|
||||
create mode 100644 test/863-serialization/src/Main.java
|
||||
|
||||
diff --git a/runtime/jni/check_jni.cc b/runtime/jni/check_jni.cc
|
||||
index a05a3e97f2..24e3256d81 100644
|
||||
--- a/runtime/jni/check_jni.cc
|
||||
+++ b/runtime/jni/check_jni.cc
|
||||
@@ -747,10 +747,10 @@ class ScopedCheck {
|
||||
return true;
|
||||
}
|
||||
|
||||
- bool CheckInstantiableNonArray(ScopedObjectAccess& soa, jclass jc)
|
||||
+ bool CheckNonArray(ScopedObjectAccess& soa, jclass jc)
|
||||
REQUIRES_SHARED(Locks::mutator_lock_) {
|
||||
ObjPtr<mirror::Class> c = soa.Decode<mirror::Class>(jc);
|
||||
- if (!c->IsInstantiableNonArray()) {
|
||||
+ if (c->IsArrayClass()) {
|
||||
AbortF("can't make objects of type %s: %p", c->PrettyDescriptor().c_str(), c.Ptr());
|
||||
return false;
|
||||
}
|
||||
@@ -2195,7 +2195,7 @@ class CheckJNI {
|
||||
ScopedObjectAccess soa(env);
|
||||
ScopedCheck sc(kFlag_Default, __FUNCTION__);
|
||||
JniValueType args[2] = {{.E = env}, {.c = c}};
|
||||
- if (sc.Check(soa, true, "Ec", args) && sc.CheckInstantiableNonArray(soa, c)) {
|
||||
+ if (sc.Check(soa, true, "Ec", args) && sc.CheckNonArray(soa, c)) {
|
||||
JniValueType result;
|
||||
result.L = baseEnv(env)->AllocObject(env, c);
|
||||
if (sc.Check(soa, false, "L", &result)) {
|
||||
@@ -2211,7 +2211,7 @@ class CheckJNI {
|
||||
ScopedCheck sc(kFlag_Default, __FUNCTION__);
|
||||
VarArgs rest(mid, vargs);
|
||||
JniValueType args[4] = {{.E = env}, {.c = c}, {.m = mid}, {.va = &rest}};
|
||||
- if (sc.Check(soa, true, "Ecm.", args) && sc.CheckInstantiableNonArray(soa, c) &&
|
||||
+ if (sc.Check(soa, true, "Ecm.", args) && sc.CheckNonArray(soa, c) &&
|
||||
sc.CheckConstructor(mid)) {
|
||||
JniValueType result;
|
||||
result.L = baseEnv(env)->NewObjectV(env, c, mid, vargs);
|
||||
@@ -2237,7 +2237,7 @@ class CheckJNI {
|
||||
ScopedCheck sc(kFlag_Default, __FUNCTION__);
|
||||
VarArgs rest(mid, vargs);
|
||||
JniValueType args[4] = {{.E = env}, {.c = c}, {.m = mid}, {.va = &rest}};
|
||||
- if (sc.Check(soa, true, "Ecm.", args) && sc.CheckInstantiableNonArray(soa, c) &&
|
||||
+ if (sc.Check(soa, true, "Ecm.", args) && sc.CheckNonArray(soa, c) &&
|
||||
sc.CheckConstructor(mid)) {
|
||||
JniValueType result;
|
||||
result.L = baseEnv(env)->NewObjectA(env, c, mid, vargs);
|
||||
diff --git a/runtime/jni/jni_internal.cc b/runtime/jni/jni_internal.cc
|
||||
index 1dde2de741..9129d9ed41 100644
|
||||
--- a/runtime/jni/jni_internal.cc
|
||||
+++ b/runtime/jni/jni_internal.cc
|
||||
@@ -922,6 +922,13 @@ class JNI {
|
||||
if (c == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
+ if (UNLIKELY(!c->IsInstantiable())) {
|
||||
+ soa.Self()->ThrowNewExceptionF(
|
||||
+ "Ljava/lang/InstantiationException;", "Can't instantiate %s %s",
|
||||
+ c->IsInterface() ? "interface" : "abstract class",
|
||||
+ c->PrettyDescriptor().c_str());
|
||||
+ return nullptr;
|
||||
+ }
|
||||
if (c->IsStringClass()) {
|
||||
gc::AllocatorType allocator_type = Runtime::Current()->GetHeap()->GetCurrentAllocator();
|
||||
return soa.AddLocalReference<jobject>(
|
||||
@@ -949,6 +956,13 @@ class JNI {
|
||||
if (c == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
+ if (UNLIKELY(!c->IsInstantiable())) {
|
||||
+ soa.Self()->ThrowNewExceptionF(
|
||||
+ "Ljava/lang/InstantiationException;", "Can't instantiate %s %s",
|
||||
+ c->IsInterface() ? "interface" : "abstract class",
|
||||
+ c->PrettyDescriptor().c_str());
|
||||
+ return nullptr;
|
||||
+ }
|
||||
if (c->IsStringClass()) {
|
||||
// Replace calls to String.<init> with equivalent StringFactory call.
|
||||
jmethodID sf_mid = jni::EncodeArtMethod<kEnableIndexIds>(
|
||||
@@ -975,6 +989,13 @@ class JNI {
|
||||
if (c == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
+ if (UNLIKELY(!c->IsInstantiable())) {
|
||||
+ soa.Self()->ThrowNewExceptionF(
|
||||
+ "Ljava/lang/InstantiationException;", "Can't instantiate %s %s",
|
||||
+ c->IsInterface() ? "interface" : "abstract class",
|
||||
+ c->PrettyDescriptor().c_str());
|
||||
+ return nullptr;
|
||||
+ }
|
||||
if (c->IsStringClass()) {
|
||||
// Replace calls to String.<init> with equivalent StringFactory call.
|
||||
jmethodID sf_mid = jni::EncodeArtMethod<kEnableIndexIds>(
|
||||
diff --git a/test/863-serialization/expected-stderr.txt b/test/863-serialization/expected-stderr.txt
|
||||
new file mode 100644
|
||||
index 0000000000..e69de29bb2
|
||||
diff --git a/test/863-serialization/expected-stdout.txt b/test/863-serialization/expected-stdout.txt
|
||||
new file mode 100644
|
||||
index 0000000000..e69de29bb2
|
||||
diff --git a/test/863-serialization/info.txt b/test/863-serialization/info.txt
|
||||
new file mode 100644
|
||||
index 0000000000..a9693a9f64
|
||||
--- /dev/null
|
||||
+++ b/test/863-serialization/info.txt
|
||||
@@ -0,0 +1,2 @@
|
||||
+Regression test for JNI::NewObject where we forgot to check if a class is
|
||||
+instantiable.
|
||||
diff --git a/test/863-serialization/src/Main.java b/test/863-serialization/src/Main.java
|
||||
new file mode 100644
|
||||
index 0000000000..72cc01f896
|
||||
--- /dev/null
|
||||
+++ b/test/863-serialization/src/Main.java
|
||||
@@ -0,0 +1,62 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+import java.io.ByteArrayInputStream;
|
||||
+import java.io.InvalidClassException;
|
||||
+import java.io.ObjectInputStream;
|
||||
+
|
||||
+public class Main {
|
||||
+
|
||||
+ public static void main(String[] args) throws Exception {
|
||||
+ deserializeHexToConcurrentHashMap();
|
||||
+ }
|
||||
+
|
||||
+ public static byte[] hexStringToByteArray(String hexString) {
|
||||
+ if (hexString == null || hexString.isEmpty()) {
|
||||
+ return new byte[0];
|
||||
+ }
|
||||
+ if (hexString.length() % 2 != 0) {
|
||||
+ throw new IllegalArgumentException("Hex string must have an even number of characters.");
|
||||
+ }
|
||||
+ int len = hexString.length();
|
||||
+ byte[] data = new byte[len / 2];
|
||||
+ for (int i = 0; i < len; i += 2) {
|
||||
+ int highNibble = Character.digit(hexString.charAt(i), 16);
|
||||
+ int lowNibble = Character.digit(hexString.charAt(i + 1), 16);
|
||||
+ if (highNibble == -1 || lowNibble == -1) {
|
||||
+ throw new IllegalArgumentException(
|
||||
+ "Invalid hex character in string: " + hexString.charAt(i) + hexString.charAt(i + 1));
|
||||
+ }
|
||||
+ data[i / 2] = (byte) ((highNibble << 4) + lowNibble);
|
||||
+ }
|
||||
+ return data;
|
||||
+ }
|
||||
+
|
||||
+ public static void deserializeHexToConcurrentHashMap() throws Exception {
|
||||
+ byte[] bytes = hexStringToByteArray("ACED0005737200266A6176612E7574696C2E636F6E63757272656E742E436F6E63757272656E74486173684D61706499DE129D87293D0300007870737200146A6176612E746578742E44617465466F726D6174642CA1E4C22615FC0200007870737200146A6176612E746578742E44617465466F726D6174642CA1E4C22615FC020000787070707878000000");
|
||||
+ ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
|
||||
+ ObjectInputStream ois = new ObjectInputStream(bis);
|
||||
+ try {
|
||||
+ Object deserializedObject = ois.readObject();
|
||||
+ throw new Error("Expected InvalidClassException");
|
||||
+ } catch (InvalidClassException e) {
|
||||
+ // expected
|
||||
+ if (!(e.getCause() instanceof InstantiationException)) {
|
||||
+ throw new Error("Expected InstantiationException");
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
6109
patches/2026-03/external/dng_sdk/2026-01-07_21-35-05_-0800_Update_to_DNG_SDK_171_2410_6b5cf2a88ebd.patch
vendored
Normal file
6109
patches/2026-03/external/dng_sdk/2026-01-07_21-35-05_-0800_Update_to_DNG_SDK_171_2410_6b5cf2a88ebd.patch
vendored
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,36 @@
|
|||
From b51a58ecec96558e1c6b1d47728f45a8795dc7ab Mon Sep 17 00:00:00 2001
|
||||
From: Nate Myren <ntmyren@google.com>
|
||||
Date: Tue, 7 Oct 2025 14:03:49 -0700
|
||||
Subject: [PATCH] Ensure sandboxed UIDs are treated as untrusted in Appops
|
||||
|
||||
They should not be considered "system" app for the purposes of
|
||||
attribution tag vaildation
|
||||
|
||||
Bug: 443742082
|
||||
Test: atest AppOpsMemoryUsageTest
|
||||
Flag: EXEMPT CVE_FIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:1bc6b146137f76589146dff5cd82363de7ccfb7d
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:066eff80abf013531320b8280637d1d00dd553a1
|
||||
Merged-In: I0c4ac8eaa8966027ad01375dde58b05febec3ffb
|
||||
Change-Id: I0c4ac8eaa8966027ad01375dde58b05febec3ffb
|
||||
---
|
||||
services/core/java/com/android/server/appop/AppOpsService.java | 3 +++
|
||||
1 file changed, 3 insertions(+)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
index ae8a8d943ccc..9c9d338099dd 100644
|
||||
--- a/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
@@ -5043,6 +5043,9 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
if (packageName == null) {
|
||||
return true;
|
||||
}
|
||||
+ if (Process.isSdkSandboxUid(uid)) {
|
||||
+ return false;
|
||||
+ }
|
||||
int appId = UserHandle.getAppId(uid);
|
||||
if (appId > 0 && appId < Process.FIRST_APPLICATION_UID) {
|
||||
return true;
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
From a4523e227733ae20eafe4ec3e85474a5b7ebf7c6 Mon Sep 17 00:00:00 2001
|
||||
From: Nate Myren <ntmyren@google.com>
|
||||
Date: Wed, 15 Oct 2025 15:48:49 -0700
|
||||
Subject: [PATCH] Prohibit untrusted proxys from specifying proxied attribution
|
||||
tags
|
||||
|
||||
Only trusted proxies should be allowed to specify tags. Also prevents
|
||||
startOperationDryRun from editing the know attribution tags, as the dry
|
||||
run should change no state.
|
||||
|
||||
Bug: 445917646
|
||||
Test: atest AttributionTest
|
||||
Flag: EXEMPT CVE_FIX
|
||||
(cherry picked from commit 110db0acb84cbd21f9da9391ab242d141ebf390c)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:94d32dce4639ba16b321667719678d2a6b42d9c3
|
||||
Merged-In: I14ab1389384fd28009edd9cceceaacdb97fb96e5
|
||||
Change-Id: I14ab1389384fd28009edd9cceceaacdb97fb96e5
|
||||
---
|
||||
.../android/server/appop/AppOpsService.java | 112 ++++++++++++++----
|
||||
.../android/server/appop/AttributedOp.java | 14 +++
|
||||
2 files changed, 105 insertions(+), 21 deletions(-)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
index 9c9d338099dd..85a5ac8bf3ca 100644
|
||||
--- a/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
|
||||
@@ -39,6 +39,7 @@ import static android.app.AppOpsManager.OP_CAMERA_SANDBOXED;
|
||||
import static android.app.AppOpsManager.OP_FLAGS_ALL;
|
||||
import static android.app.AppOpsManager.OP_FLAG_SELF;
|
||||
import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
|
||||
+import static android.app.AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
|
||||
import static android.app.AppOpsManager.OP_NONE;
|
||||
import static android.app.AppOpsManager.OP_PLAY_AUDIO;
|
||||
import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
|
||||
@@ -3189,7 +3190,7 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
public int checkPackage(int uid, String packageName) {
|
||||
Objects.requireNonNull(packageName);
|
||||
try {
|
||||
- verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true);
|
||||
+ verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true, true);
|
||||
// When the caller is the system, it's possible that the packageName is the special
|
||||
// one (e.g., "root") which isn't actually existed.
|
||||
if (resolveNonAppUid(packageName) == uid
|
||||
@@ -3407,8 +3408,10 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
@OpFlags int flags, boolean shouldCollectAsyncNotedOp, @Nullable String message,
|
||||
boolean shouldCollectMessage, int notedCount) {
|
||||
PackageVerificationResult pvr;
|
||||
+ boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0;
|
||||
try {
|
||||
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
|
||||
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName,
|
||||
+ proxyTrusted);
|
||||
if (!pvr.isAttributionTagValid) {
|
||||
attributionTag = null;
|
||||
}
|
||||
@@ -4072,8 +4075,10 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
|
||||
int attributionChainId) {
|
||||
PackageVerificationResult pvr;
|
||||
+ boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0;
|
||||
try {
|
||||
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
|
||||
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName,
|
||||
+ proxyTrusted);
|
||||
if (!pvr.isAttributionTagValid) {
|
||||
attributionTag = null;
|
||||
}
|
||||
@@ -4206,8 +4211,10 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
int proxyUid, String proxyPackageName, @OpFlags int flags,
|
||||
boolean startIfModeDefault) {
|
||||
PackageVerificationResult pvr;
|
||||
+ boolean proxyTrusted = (flags & OP_FLAG_UNTRUSTED_PROXIED) == 0;
|
||||
try {
|
||||
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
|
||||
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName,
|
||||
+ proxyTrusted);
|
||||
if (!pvr.isAttributionTagValid) {
|
||||
attributionTag = null;
|
||||
}
|
||||
@@ -4223,7 +4230,9 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
|
||||
boolean isRestricted = false;
|
||||
synchronized (this) {
|
||||
- final Ops ops = getOpsLocked(uid, packageName, attributionTag,
|
||||
+ // Edit is true (so we create the Ops object if needed), but attribution tag is given as
|
||||
+ // null, so we don't cache any information about it.
|
||||
+ final Ops ops = getOpsLocked(uid, packageName, null,
|
||||
pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
|
||||
if (ops == null) {
|
||||
if (DEBUG) {
|
||||
@@ -4402,8 +4411,11 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
int virtualDeviceId) {
|
||||
PackageVerificationResult pvr;
|
||||
try {
|
||||
+ // assume the proxy is trusted, since we aren't sure. We'll search for the attribution
|
||||
+ // tag with trusted flags, and if we don't find it, search for a null tag with
|
||||
+ // untrusted flags
|
||||
pvr = verifyAndGetBypass(proxiedUid, proxiedPackageName, attributionTag,
|
||||
- proxyUid, proxyPackageName);
|
||||
+ proxyUid, proxyPackageName, /* isProxyTrusted */ true);
|
||||
if (!pvr.isAttributionTagValid) {
|
||||
attributionTag = null;
|
||||
}
|
||||
@@ -4413,8 +4425,9 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
+ boolean hasProxy = proxyUid != Process.INVALID_UID;
|
||||
Op op = getOpLocked(code, proxiedUid, proxiedPackageName, attributionTag,
|
||||
- pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
|
||||
+ pvr.isAttributionTagValid, pvr.bypass, /* edit */ false);
|
||||
if (op == null) {
|
||||
Slog.e(TAG, "Operation not found: uid=" + proxiedUid + " pkg=" + proxiedPackageName
|
||||
+ "("
|
||||
@@ -4422,9 +4435,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
return;
|
||||
}
|
||||
final AttributedOp attributedOp =
|
||||
- op.mDeviceAttributedOps.getOrDefault(
|
||||
- getPersistentDeviceIdForOp(virtualDeviceId, code),
|
||||
- new ArrayMap<>()).get(attributionTag);
|
||||
+ getAttributedOpWithClientId(op, clientId, attributionTag, virtualDeviceId,
|
||||
+ hasProxy);
|
||||
if (attributedOp == null) {
|
||||
Slog.e(TAG, "Attribution not found: uid=" + proxiedUid
|
||||
+ " pkg=" + proxiedPackageName + "("
|
||||
@@ -4442,6 +4454,54 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
+ private AttributedOp getAttributedOpWithClientId(Op op, IBinder clientId,
|
||||
+ String attributionTag, int virtualDeviceId, boolean hasProxy) {
|
||||
+ AttributedOp attributedOp =
|
||||
+ op.mDeviceAttributedOps.getOrDefault(
|
||||
+ getPersistentDeviceIdForOp(virtualDeviceId, op.op),
|
||||
+ new ArrayMap<>()).get(attributionTag);
|
||||
+ if (!hasProxy) {
|
||||
+ return attributedOp;
|
||||
+ }
|
||||
+ boolean hasTrustedInProgressEvent = attributedOp != null
|
||||
+ && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId
|
||||
+ && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) == 0));
|
||||
+ if (hasTrustedInProgressEvent) {
|
||||
+ return attributedOp;
|
||||
+ }
|
||||
+
|
||||
+ // We failed to find a trusted in progress event that matches the clientId. Check if the
|
||||
+ // tag is valid in the package, and look for an untrusted access matching that tag, if so
|
||||
+ boolean tagValid = false;
|
||||
+ try {
|
||||
+ tagValid = verifyAndGetBypass(op.uid, op.packageName, attributionTag)
|
||||
+ .isAttributionTagValid;
|
||||
+ } catch (SecurityException e) {
|
||||
+ // assume tag is invalid
|
||||
+ }
|
||||
+ if (tagValid) {
|
||||
+ boolean hasUntrustedInProgressEvent = attributedOp != null
|
||||
+ && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId
|
||||
+ && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) != 0));
|
||||
+ if (hasUntrustedInProgressEvent) {
|
||||
+ return attributedOp;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // The tag was not valid, or we failed to find an untrusted event. Look for an untrusted
|
||||
+ // event with the null attribution tag
|
||||
+ attributedOp = op.mDeviceAttributedOps.getOrDefault(
|
||||
+ getPersistentDeviceIdForOp(virtualDeviceId, op.op),
|
||||
+ new ArrayMap<>()).get(null);
|
||||
+ boolean hasUntrustedNullEvent = attributedOp != null
|
||||
+ && attributedOp.hasInProgressEvent((event -> event.getClientId() == clientId
|
||||
+ && (event.getFlags() & OP_FLAG_UNTRUSTED_PROXIED) != 0));
|
||||
+ if (hasUntrustedNullEvent) {
|
||||
+ return attributedOp;
|
||||
+ }
|
||||
+ return null;
|
||||
+ }
|
||||
+
|
||||
void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull
|
||||
String packageName, @Nullable String attributionTag, int virtualDeviceId,
|
||||
boolean active, @AttributionFlags int attributionFlags, int attributionChainId) {
|
||||
@@ -4881,20 +4941,22 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
}
|
||||
|
||||
/**
|
||||
- * @see #verifyAndGetBypass(int, String, String, int, String, boolean)
|
||||
+ * @see #verifyAndGetBypass(int, String, String, int, String, boolean, boolean)
|
||||
*/
|
||||
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
|
||||
@Nullable String attributionTag) {
|
||||
- return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null);
|
||||
+ return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null,
|
||||
+ true);
|
||||
}
|
||||
|
||||
/**
|
||||
- * @see #verifyAndGetBypass(int, String, String, int, String, boolean)
|
||||
+ * @see #verifyAndGetBypass(int, String, String, int, String, boolean, boolean)
|
||||
*/
|
||||
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
|
||||
- @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName) {
|
||||
+ @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName,
|
||||
+ boolean isProxyTrusted) {
|
||||
return verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName,
|
||||
- false);
|
||||
+ isProxyTrusted, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4907,6 +4969,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
* @param attributionTag attribution tag or {@code null} if no need to verify
|
||||
* @param proxyUid The proxy uid, from which the attribution tag is to be pulled
|
||||
* @param proxyPackageName The proxy package, from which the attribution tag may be pulled
|
||||
+ * @param isProxyTrusted Whether or not the proxy package is trusted. If it isn't, then the
|
||||
+ * proxy attribution tag will not be used
|
||||
* @param suppressErrorLogs Whether to print to logcat about nonmatching parameters
|
||||
*
|
||||
* @return PackageVerificationResult containing {@link RestrictionBypass} and whether the
|
||||
@@ -4914,7 +4978,7 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
*/
|
||||
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
|
||||
@Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName,
|
||||
- boolean suppressErrorLogs) {
|
||||
+ boolean isProxyTrusted, boolean suppressErrorLogs) {
|
||||
if (uid == Process.ROOT_UID) {
|
||||
// For backwards compatibility, don't check package name for root UID, unless someone
|
||||
// is claiming to be a proxy for root, which should never happen in normal usage.
|
||||
@@ -4922,7 +4986,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
// system app (or is null), in order to prevent abusive apps clogging the appops
|
||||
// system with unlimited attribution tags via proxy calls.
|
||||
return new PackageVerificationResult(null,
|
||||
- /* isAttributionTagValid */ isPackageNullOrSystem(proxyPackageName, proxyUid));
|
||||
+ /* isAttributionTagValid */ isProxyTrusted
|
||||
+ && isPackageNullOrSystem(proxyPackageName, proxyUid));
|
||||
}
|
||||
if (Process.isSdkSandboxUid(uid)) {
|
||||
// SDK sandbox processes run in their own UID range, but their associated
|
||||
@@ -4986,7 +5051,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
// system app (or is null), in order to prevent abusive apps clogging the appops
|
||||
// system with unlimited attribution tags via proxy calls.
|
||||
return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED,
|
||||
- /* isAttributionTagValid */ isPackageNullOrSystem(proxyPackageName, proxyUid));
|
||||
+ /* isAttributionTagValid */ isProxyTrusted
|
||||
+ && isPackageNullOrSystem(proxyPackageName, proxyUid));
|
||||
}
|
||||
|
||||
int userId = UserHandle.getUserId(uid);
|
||||
@@ -5007,8 +5073,9 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
if (!isAttributionTagValid) {
|
||||
AndroidPackage proxyPkg = proxyPackageName != null
|
||||
? pmInt.getPackage(proxyPackageName) : null;
|
||||
- // Re-check in proxy.
|
||||
- isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag);
|
||||
+ // Re-check in proxy, if trusted.
|
||||
+ isAttributionTagValid =
|
||||
+ isProxyTrusted && isAttributionInPackage(proxyPkg, attributionTag);
|
||||
String msg;
|
||||
if (pkg != null && isAttributionTagValid) {
|
||||
msg = "attributionTag " + attributionTag + " declared in manifest of the proxy"
|
||||
@@ -5035,7 +5102,6 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid
|
||||
+ otherUidMessage);
|
||||
}
|
||||
-
|
||||
return new PackageVerificationResult(bypass, isAttributionTagValid);
|
||||
}
|
||||
|
||||
@@ -5050,6 +5116,10 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
if (appId > 0 && appId < Process.FIRST_APPLICATION_UID) {
|
||||
return true;
|
||||
}
|
||||
+ if (mPackageManagerInternal.getPackageUid(packageName, PackageManager.MATCH_ALL,
|
||||
+ UserHandle.getUserId(uid)) != uid) {
|
||||
+ return false;
|
||||
+ }
|
||||
return mPackageManagerInternal.isSystemPackage(packageName);
|
||||
}
|
||||
|
||||
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
|
||||
index b9bc27d9c696..73f9fede9ed6 100644
|
||||
--- a/services/core/java/com/android/server/appop/AttributedOp.java
|
||||
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
|
||||
@@ -40,6 +40,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.function.Consumer;
|
||||
+import java.util.function.Predicate;
|
||||
|
||||
final class AttributedOp {
|
||||
private final @NonNull AppOpsService mAppOpsService;
|
||||
@@ -620,6 +621,19 @@ final class AttributedOp {
|
||||
return mPausedInProgressEvents != null && !mPausedInProgressEvents.isEmpty();
|
||||
}
|
||||
|
||||
+ public boolean hasInProgressEvent(Predicate<InProgressStartOpEvent> predicate) {
|
||||
+ ArrayMap<IBinder, InProgressStartOpEvent> events =
|
||||
+ isPaused() ? mPausedInProgressEvents : mInProgressEvents;
|
||||
+ if (events == null || events.isEmpty()) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ for (int i = 0; i < events.size(); i++) {
|
||||
+ if (predicate.test(events.valueAt(i))) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+ }
|
||||
boolean hasAnyTime() {
|
||||
return (mAccessEvents != null && mAccessEvents.size() > 0)
|
||||
|| (mRejectEvents != null && mRejectEvents.size() > 0);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
From e770e9f0234158f4631c7147b64a1d70e0843d0b Mon Sep 17 00:00:00 2001
|
||||
From: Nate Myren <ntmyren@google.com>
|
||||
Date: Wed, 5 Nov 2025 14:36:49 -0800
|
||||
Subject: [PATCH] Trim permission, permission group names
|
||||
|
||||
Bug: 453649815
|
||||
Test: atest AppSecurityTests
|
||||
Flag: EXEMPT CVE_FIX
|
||||
(cherry picked from commit 595cf99ecd42927eebf804638a4623313f3f14db)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:08ea2a452c271ccf258d63efc0126c7fa13d3312
|
||||
Merged-In: I673ad83d05c9825177967e4f0a960e8841610b71
|
||||
Change-Id: I673ad83d05c9825177967e4f0a960e8841610b71
|
||||
---
|
||||
.../internal/pm/pkg/component/ParsedPermissionUtils.java | 7 ++++++-
|
||||
1 file changed, 6 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java
|
||||
index d4dabf51d4c7..0dd0969ae35e 100644
|
||||
--- a/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java
|
||||
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedPermissionUtils.java
|
||||
@@ -152,6 +152,8 @@ public class ParsedPermissionUtils {
|
||||
}
|
||||
}
|
||||
|
||||
+ permission.setName(permission.getName().trim());
|
||||
+
|
||||
permission.setProtectionLevel(
|
||||
PermissionInfo.fixProtectionLevel(permission.getProtectionLevel()));
|
||||
|
||||
@@ -199,6 +201,8 @@ public class ParsedPermissionUtils {
|
||||
sa.recycle();
|
||||
}
|
||||
|
||||
+ permission.setName(permission.getName().trim());
|
||||
+
|
||||
int index = permission.getName().indexOf('.');
|
||||
if (index > 0) {
|
||||
index = permission.getName().indexOf('.', index + 1);
|
||||
@@ -248,7 +252,8 @@ public class ParsedPermissionUtils {
|
||||
.setBackgroundRequestDetailRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_backgroundRequestDetail, 0))
|
||||
.setRequestRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_request, 0))
|
||||
.setPriority(sa.getInt(R.styleable.AndroidManifestPermissionGroup_priority, 0))
|
||||
- .setFlags(sa.getInt(R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags,0));
|
||||
+ .setFlags(sa.getInt(R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags,0))
|
||||
+ .setName(permissionGroup.getName().trim());
|
||||
// @formatter:on
|
||||
} finally {
|
||||
sa.recycle();
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
From cea235f00865ff73344f1efa9494e47beecc3fd5 Mon Sep 17 00:00:00 2001
|
||||
From: Shawn Lin <shawnlin@google.com>
|
||||
Date: Wed, 15 Oct 2025 12:18:38 +0000
|
||||
Subject: [PATCH] Fixed "Unlock your phone" unexpectedlly turned ON after OTA
|
||||
|
||||
If the new setting key is not set, we should use the value of the old
|
||||
ones as the default value.
|
||||
|
||||
Bug: 444673089
|
||||
Test: atest BiometricServiceTest
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit a3799e443e9fdbbcbc96824b8d2fc1e4c683bb7e)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:2b8bc2b15f2aceb4e2a379209997afce16427e76
|
||||
Merged-In: I902c7fd9781037ba05b30821b4fa22aa1509f3fd
|
||||
Change-Id: I902c7fd9781037ba05b30821b4fa22aa1509f3fd
|
||||
---
|
||||
.../server/biometrics/BiometricService.java | 45 ++++++++-
|
||||
.../biometrics/BiometricServiceTest.java | 91 +++++++++++++++++++
|
||||
2 files changed, 132 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
|
||||
index cf5fa9699ca9..7485929147e5 100644
|
||||
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
|
||||
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
|
||||
@@ -412,20 +412,39 @@ public class BiometricService extends SystemService {
|
||||
notifyEnabledOnKeyguardCallbacks(userId, TYPE_ANY_BIOMETRIC);
|
||||
}
|
||||
} else if (FACE_KEYGUARD_ENABLED.equals(uri)) {
|
||||
+ final int biometricKeyguardEnabled = Settings.Secure.getIntForUser(
|
||||
+ mContentResolver,
|
||||
+ Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED,
|
||||
+ -1 /* default */,
|
||||
+ userId);
|
||||
+ // For OTA case: if FACE_KEYGUARD_ENABLED is not set and BIOMETRIC_APP_ENABLED is
|
||||
+ // set, set the default value of the former to that of the latter.
|
||||
+ final boolean defaultValue = biometricKeyguardEnabled == -1
|
||||
+ ? DEFAULT_KEYGUARD_ENABLED : biometricKeyguardEnabled == 1;
|
||||
mFaceEnabledOnKeyguard.put(userId, Settings.Secure.getIntForUser(
|
||||
mContentResolver,
|
||||
Settings.Secure.FACE_KEYGUARD_ENABLED,
|
||||
- DEFAULT_KEYGUARD_ENABLED ? 1 : 0 /* default */,
|
||||
+ defaultValue ? 1 : 0 /* default */,
|
||||
userId) != 0);
|
||||
|
||||
if (userId == ActivityManager.getCurrentUser() && !selfChange) {
|
||||
notifyEnabledOnKeyguardCallbacks(userId, TYPE_FACE);
|
||||
}
|
||||
} else if (FINGERPRINT_KEYGUARD_ENABLED.equals(uri)) {
|
||||
+ final int biometricKeyguardEnabled = Settings.Secure.getIntForUser(
|
||||
+ mContentResolver,
|
||||
+ Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED,
|
||||
+ -1 /* default */,
|
||||
+ userId);
|
||||
+ // For OTA case: if FINGERPRINT_KEYGUARD_ENABLED is not set and
|
||||
+ // BIOMETRIC_APP_ENABLED is set, set the default value of the former to that of the
|
||||
+ // latter.
|
||||
+ final boolean defaultValue = biometricKeyguardEnabled == -1
|
||||
+ ? DEFAULT_KEYGUARD_ENABLED : biometricKeyguardEnabled == 1;
|
||||
mFingerprintEnabledOnKeyguard.put(userId, Settings.Secure.getIntForUser(
|
||||
mContentResolver,
|
||||
Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED,
|
||||
- DEFAULT_KEYGUARD_ENABLED ? 1 : 0 /* default */,
|
||||
+ defaultValue ? 1 : 0 /* default */,
|
||||
userId) != 0);
|
||||
|
||||
if (userId == ActivityManager.getCurrentUser() && !selfChange) {
|
||||
@@ -438,16 +457,34 @@ public class BiometricService extends SystemService {
|
||||
DEFAULT_APP_ENABLED ? 1 : 0 /* default */,
|
||||
userId) != 0);
|
||||
} else if (FACE_APP_ENABLED.equals(uri)) {
|
||||
+ final int biometricAppEnabled = Settings.Secure.getIntForUser(
|
||||
+ mContentResolver,
|
||||
+ Settings.Secure.BIOMETRIC_APP_ENABLED,
|
||||
+ -1 /* default */,
|
||||
+ userId);
|
||||
+ // For OTA case: if FACE_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is set,
|
||||
+ // set the default value of the former to that of the latter.
|
||||
+ final boolean defaultValue = biometricAppEnabled == -1
|
||||
+ ? DEFAULT_APP_ENABLED : biometricAppEnabled == 1;
|
||||
mFaceEnabledForApps.put(userId, Settings.Secure.getIntForUser(
|
||||
mContentResolver,
|
||||
Settings.Secure.FACE_APP_ENABLED,
|
||||
- DEFAULT_APP_ENABLED ? 1 : 0 /* default */,
|
||||
+ defaultValue ? 1 : 0 /* default */,
|
||||
userId) != 0);
|
||||
} else if (FINGERPRINT_APP_ENABLED.equals(uri)) {
|
||||
+ final int biometricAppEnabled = Settings.Secure.getIntForUser(
|
||||
+ mContentResolver,
|
||||
+ Settings.Secure.BIOMETRIC_APP_ENABLED,
|
||||
+ -1 /* default */,
|
||||
+ userId);
|
||||
+ // For OTA case: if FINGERPRINT_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is
|
||||
+ // set, set the default value of the former to that of the latter.
|
||||
+ final boolean defaultValue = biometricAppEnabled == -1
|
||||
+ ? DEFAULT_APP_ENABLED : biometricAppEnabled == 1;
|
||||
mFingerprintEnabledForApps.put(userId, Settings.Secure.getIntForUser(
|
||||
mContentResolver,
|
||||
Settings.Secure.FINGERPRINT_APP_ENABLED,
|
||||
- DEFAULT_APP_ENABLED ? 1 : 0 /* default */,
|
||||
+ defaultValue ? 1 : 0 /* default */,
|
||||
userId) != 0);
|
||||
} else if (MANDATORY_BIOMETRICS_ENABLED.equals(uri)) {
|
||||
updateMandatoryBiometricsForAllProfiles(userId);
|
||||
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
|
||||
index 9918a9a35c33..3061bab24518 100644
|
||||
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
|
||||
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
|
||||
@@ -21,6 +21,12 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_CREDENTIAL
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
|
||||
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
|
||||
import static android.hardware.biometrics.BiometricManager.Authenticators;
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED;
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FACE_APP_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FACE_KEYGUARD_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FINGERPRINT_APP_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED;
|
||||
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
|
||||
|
||||
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
|
||||
@@ -40,6 +46,7 @@ import static junit.framework.TestCase.assertNotNull;
|
||||
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
+import static org.junit.Assume.assumeTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -2147,6 +2154,90 @@ public class BiometricServiceTest {
|
||||
invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG));
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
|
||||
+ public void
|
||||
+ testEnabledForApps_biometricAppEnableOff_fpAppEnabledNotSet_returnFalse()
|
||||
+ throws Exception {
|
||||
+ final Context context = ApplicationProvider.getApplicationContext();
|
||||
+ final int value = Settings.Secure.getIntForUser(context.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, -1, context.getUserId());
|
||||
+ assumeTrue("FINGERPRINT_APP_ENABLED is set. Skipped", value == -1);
|
||||
+
|
||||
+ Settings.Secure.putIntForUser(context.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, context.getUserId());
|
||||
+
|
||||
+ final BiometricService.SettingObserver settingObserver =
|
||||
+ new BiometricService.SettingObserver(
|
||||
+ context, mBiometricHandlerProvider.getBiometricCallbackHandler(),
|
||||
+ new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager);
|
||||
+
|
||||
+ assertFalse(settingObserver.getEnabledForApps(context.getUserId(), TYPE_FINGERPRINT));
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
|
||||
+ public void
|
||||
+ testKeyguardEnabled_biometricKeyguardEnableOff_fpKeyguardEnabledNotSet_returnFalse()
|
||||
+ throws Exception {
|
||||
+ final Context context = ApplicationProvider.getApplicationContext();
|
||||
+ final int value = Settings.Secure.getIntForUser(context.getContentResolver(),
|
||||
+ FINGERPRINT_KEYGUARD_ENABLED, -1, context.getUserId());
|
||||
+ assumeTrue("FINGERPRINT_KEYGUARD_ENABLED is set. Skipped", value == -1);
|
||||
+
|
||||
+ Settings.Secure.putIntForUser(context.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, context.getUserId());
|
||||
+
|
||||
+ final BiometricService.SettingObserver settingObserver =
|
||||
+ new BiometricService.SettingObserver(
|
||||
+ context, mBiometricHandlerProvider.getBiometricCallbackHandler(),
|
||||
+ new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager);
|
||||
+
|
||||
+ assertFalse(settingObserver.getEnabledOnKeyguard(context.getUserId(), TYPE_FINGERPRINT));
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
|
||||
+ public void
|
||||
+ testEnabledForApps_biometricAppEnableOff_faceAppEnabledNotSet_returnFalse()
|
||||
+ throws Exception {
|
||||
+ final Context context = ApplicationProvider.getApplicationContext();
|
||||
+ final int value = Settings.Secure.getIntForUser(context.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, -1, context.getUserId());
|
||||
+ assumeTrue("FACE_APP_ENABLED is set. Skipped", value == -1);
|
||||
+
|
||||
+ Settings.Secure.putIntForUser(context.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, context.getUserId());
|
||||
+
|
||||
+ final BiometricService.SettingObserver settingObserver =
|
||||
+ new BiometricService.SettingObserver(
|
||||
+ context, mBiometricHandlerProvider.getBiometricCallbackHandler(),
|
||||
+ new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager);
|
||||
+
|
||||
+ assertFalse(settingObserver.getEnabledForApps(context.getUserId(), TYPE_FACE));
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ @RequiresFlagsEnabled(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
|
||||
+ public void
|
||||
+ testKeyguardEnabled_biometricKeyguardEnableOff_faceKeyguardEnabledNotSet_returnFalse()
|
||||
+ throws Exception {
|
||||
+ final Context context = ApplicationProvider.getApplicationContext();
|
||||
+ final int value = Settings.Secure.getIntForUser(context.getContentResolver(),
|
||||
+ FACE_KEYGUARD_ENABLED, -1, context.getUserId());
|
||||
+ assumeTrue("FACE_KEYGUARD_ENABLED is set. Skipped", value == -1);
|
||||
+
|
||||
+ Settings.Secure.putIntForUser(context.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, context.getUserId());
|
||||
+
|
||||
+ final BiometricService.SettingObserver settingObserver =
|
||||
+ new BiometricService.SettingObserver(
|
||||
+ context, mBiometricHandlerProvider.getBiometricCallbackHandler(),
|
||||
+ new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager);
|
||||
+
|
||||
+ assertFalse(settingObserver.getEnabledOnKeyguard(context.getUserId(), TYPE_FACE));
|
||||
+ }
|
||||
+
|
||||
// Helper methods
|
||||
|
||||
private int invokeCanAuthenticate(BiometricService service, int authenticators)
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,503 @@
|
|||
From 2bca2265ff3e26b09f9b31c31063147a94e4c5aa Mon Sep 17 00:00:00 2001
|
||||
From: Hiroki Sato <hirokisato@google.com>
|
||||
Date: Mon, 20 Oct 2025 19:13:15 +0900
|
||||
Subject: [PATCH] Harden InputMethodInfo parsing against large metadata
|
||||
|
||||
An IME's metadata can reference arbitrarily large strings (e.g.,
|
||||
@string/large_text), which can lead to OOM or large Binder transactions
|
||||
during parsing. The previous check only validated the raw XML file
|
||||
size, failing to account for the size of these resolved string
|
||||
references.
|
||||
|
||||
This patch hardens the InputMethodInfo constructor by enforcing a 200KB
|
||||
cumulative limit on all resolved metadata attributes. A new
|
||||
MetadataReadBytesTracker now sums the actual size of all read
|
||||
attributes, including the full length of any strings, and parsing is
|
||||
aborted if this 200KB limit is exceeded.
|
||||
|
||||
Bug: 449416164
|
||||
Bug: 449181366
|
||||
Bug: 449393786
|
||||
Bug: 449227003
|
||||
Test: CtsInputMethodTestCases:{InputMethodRegistrationTest,InputMethodInfoTest}
|
||||
Test: InputMethodCoreTests:{InputMethodSubtypeArrayTest,InputMethodInfoTest}
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit 7afc13faace7cfafd0353482db33504c5e269d69)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:311c7f2c2b8b927571884765c7322a21f8115383
|
||||
Merged-In: I43f7be8eb80abeb39863a3b01d3a606beb90120c
|
||||
Change-Id: I43f7be8eb80abeb39863a3b01d3a606beb90120c
|
||||
---
|
||||
.../view/inputmethod/InputMethodInfo.java | 284 ++++++++++++++----
|
||||
.../view/inputmethod/InputMethodInfoTest.java | 98 ++++++
|
||||
2 files changed, 323 insertions(+), 59 deletions(-)
|
||||
|
||||
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
|
||||
index 31c7b7b20ef7..f81e2fab44aa 100644
|
||||
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
|
||||
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
|
||||
@@ -50,6 +50,8 @@ import android.util.Slog;
|
||||
import android.util.Xml;
|
||||
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
|
||||
|
||||
+import com.android.internal.annotations.VisibleForTesting;
|
||||
+
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
@@ -314,70 +316,72 @@ public final class InputMethodInfo implements Parcelable {
|
||||
"Meta-data does not start with input-method tag");
|
||||
}
|
||||
|
||||
- TypedArray sa = res.obtainAttributes(attrs,
|
||||
- com.android.internal.R.styleable.InputMethod);
|
||||
- settingsActivityComponent = sa.getString(
|
||||
- com.android.internal.R.styleable.InputMethod_settingsActivity);
|
||||
- if (Flags.imeSwitcherRevampApi()) {
|
||||
- languageSettingsActivityComponent = sa.getString(
|
||||
- com.android.internal.R.styleable.InputMethod_languageSettingsActivity);
|
||||
- }
|
||||
- if ((si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH)
|
||||
- || (settingsActivityComponent != null
|
||||
- && settingsActivityComponent.length()
|
||||
- > COMPONENT_NAME_MAX_LENGTH)
|
||||
- || (languageSettingsActivityComponent != null
|
||||
- && languageSettingsActivityComponent.length()
|
||||
- > COMPONENT_NAME_MAX_LENGTH)) {
|
||||
- throw new XmlPullParserException(
|
||||
- "Activity name exceeds maximum of 1000 characters");
|
||||
+ final MetadataReadBytesTracker readTracker = new MetadataReadBytesTracker();
|
||||
+ try (TypedArrayWrapper sa = TypedArrayWrapper.createForMethod(
|
||||
+ res.obtainAttributes(attrs, com.android.internal.R.styleable.InputMethod),
|
||||
+ readTracker)) {
|
||||
+ settingsActivityComponent = sa.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_settingsActivity);
|
||||
+ if (Flags.imeSwitcherRevampApi()) {
|
||||
+ languageSettingsActivityComponent = sa.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity);
|
||||
+ }
|
||||
+ isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly,
|
||||
+ false);
|
||||
+ isVirtualDeviceOnly = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable.InputMethod_isVirtualDeviceOnly, false);
|
||||
+ isDefaultResId = sa.getResourceId(
|
||||
+ com.android.internal.R.styleable.InputMethod_isDefault, 0);
|
||||
+ supportsSwitchingToNextInputMethod = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable
|
||||
+ .InputMethod_supportsSwitchingToNextInputMethod,
|
||||
+ false);
|
||||
+ inlineSuggestionsEnabled = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions,
|
||||
+ false);
|
||||
+ supportsInlineSuggestionsWithTouchExploration = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable
|
||||
+ .InputMethod_supportsInlineSuggestionsWithTouchExploration, false);
|
||||
+ suppressesSpellChecker = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false);
|
||||
+ showInInputMethodPicker = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true);
|
||||
+ mHandledConfigChanges = sa.getInt(
|
||||
+ com.android.internal.R.styleable.InputMethod_configChanges, 0);
|
||||
+ mSupportsStylusHandwriting = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable.InputMethod_supportsStylusHandwriting,
|
||||
+ false);
|
||||
+ mSupportsConnectionlessStylusHandwriting = sa.getBoolean(
|
||||
+ com.android.internal.R.styleable
|
||||
+ .InputMethod_supportsConnectionlessStylusHandwriting, false);
|
||||
+ stylusHandwritingSettingsActivity = sa.getString(
|
||||
+ com.android.internal.R.styleable
|
||||
+ .InputMethod_stylusHandwritingSettingsActivity);
|
||||
}
|
||||
|
||||
- isVrOnly = sa.getBoolean(com.android.internal.R.styleable.InputMethod_isVrOnly, false);
|
||||
- isVirtualDeviceOnly = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_isVirtualDeviceOnly, false);
|
||||
- isDefaultResId = sa.getResourceId(
|
||||
- com.android.internal.R.styleable.InputMethod_isDefault, 0);
|
||||
- supportsSwitchingToNextInputMethod = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,
|
||||
- false);
|
||||
- inlineSuggestionsEnabled = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, false);
|
||||
- supportsInlineSuggestionsWithTouchExploration = sa.getBoolean(
|
||||
- com.android.internal.R.styleable
|
||||
- .InputMethod_supportsInlineSuggestionsWithTouchExploration, false);
|
||||
- suppressesSpellChecker = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false);
|
||||
- showInInputMethodPicker = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_showInInputMethodPicker, true);
|
||||
- mHandledConfigChanges = sa.getInt(
|
||||
- com.android.internal.R.styleable.InputMethod_configChanges, 0);
|
||||
- mSupportsStylusHandwriting = sa.getBoolean(
|
||||
- com.android.internal.R.styleable.InputMethod_supportsStylusHandwriting, false);
|
||||
- mSupportsConnectionlessStylusHandwriting = sa.getBoolean(
|
||||
- com.android.internal.R.styleable
|
||||
- .InputMethod_supportsConnectionlessStylusHandwriting, false);
|
||||
- stylusHandwritingSettingsActivity = sa.getString(
|
||||
- com.android.internal.R.styleable.InputMethod_stylusHandwritingSettingsActivity);
|
||||
- sa.recycle();
|
||||
-
|
||||
final int depth = parser.getDepth();
|
||||
// Parse all subtypes
|
||||
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
||||
&& type != XmlPullParser.END_DOCUMENT) {
|
||||
- if (type == XmlPullParser.START_TAG) {
|
||||
- nodeName = parser.getName();
|
||||
- if (!"subtype".equals(nodeName)) {
|
||||
- throw new XmlPullParserException(
|
||||
- "Meta-data in input-method does not start with subtype tag");
|
||||
- }
|
||||
- final TypedArray a = res.obtainAttributes(
|
||||
- attrs, com.android.internal.R.styleable.InputMethod_Subtype);
|
||||
+ if (type != XmlPullParser.START_TAG) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ nodeName = parser.getName();
|
||||
+ if (!"subtype".equals(nodeName)) {
|
||||
+ throw new XmlPullParserException(
|
||||
+ "Meta-data in input-method does not start with subtype tag");
|
||||
+ }
|
||||
+
|
||||
+ final InputMethodSubtype subtype;
|
||||
+ try (TypedArrayWrapper a = TypedArrayWrapper.createForSubtype(
|
||||
+ res.obtainAttributes(attrs,
|
||||
+ com.android.internal.R.styleable.InputMethod_Subtype),
|
||||
+ readTracker)) {
|
||||
String pkLanguageTag = a.getString(com.android.internal.R.styleable
|
||||
.InputMethod_Subtype_physicalKeyboardHintLanguageTag);
|
||||
String pkLayoutType = a.getString(com.android.internal.R.styleable
|
||||
.InputMethod_Subtype_physicalKeyboardHintLayoutType);
|
||||
- final InputMethodSubtype subtype = new InputMethodSubtypeBuilder()
|
||||
+ subtype = new InputMethodSubtypeBuilder()
|
||||
.setSubtypeNameResId(a.getResourceId(com.android.internal.R.styleable
|
||||
.InputMethod_Subtype_label, 0))
|
||||
.setSubtypeIconResId(a.getResourceId(com.android.internal.R.styleable
|
||||
@@ -402,12 +406,11 @@ public final class InputMethodInfo implements Parcelable {
|
||||
.InputMethod_Subtype_subtypeId, 0 /* use Arrays.hashCode */))
|
||||
.setIsAsciiCapable(a.getBoolean(com.android.internal.R.styleable
|
||||
.InputMethod_Subtype_isAsciiCapable, false)).build();
|
||||
- a.recycle();
|
||||
- if (!subtype.isAuxiliary()) {
|
||||
- isAuxIme = false;
|
||||
- }
|
||||
- subtypes.add(subtype);
|
||||
}
|
||||
+ if (!subtype.isAuxiliary()) {
|
||||
+ isAuxIme = false;
|
||||
+ }
|
||||
+ subtypes.add(subtype);
|
||||
}
|
||||
} catch (NameNotFoundException | IndexOutOfBoundsException | NumberFormatException e) {
|
||||
throw new XmlPullParserException(
|
||||
@@ -471,6 +474,11 @@ public final class InputMethodInfo implements Parcelable {
|
||||
return;
|
||||
}
|
||||
|
||||
+ if (si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH) {
|
||||
+ throw new XmlPullParserException(
|
||||
+ "Input method name exceeds " + COMPONENT_NAME_MAX_LENGTH + " characters");
|
||||
+ }
|
||||
+
|
||||
// Validate file size using InputStream.skip()
|
||||
long totalBytesSkipped = 0;
|
||||
// Loop to ensure we skip the required number of bytes, as a single
|
||||
@@ -1132,4 +1140,162 @@ public final class InputMethodInfo implements Parcelable {
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
+
|
||||
+ /**
|
||||
+ * A wrapper class for {@link TypedArray} that enforces limits on the size of the metadata
|
||||
+ * read from the XML. Methods throw an {@link XmlPullParserException} if the limit is surpassed.
|
||||
+ *
|
||||
+ * <p>This class works in conjunction with {@link MetadataReadBytesTracker} to:
|
||||
+ * <ul>
|
||||
+ * <li>Limit the length of individual string attributes. For
|
||||
+ * {@code settingsActivity} and {@code languageSettingsActivity}, the maximum length is
|
||||
+ * {@link #COMPONENT_NAME_MAX_LENGTH}. For other string attributes, the maximum length is
|
||||
+ * {@link #STRING_ATTRIBUTES_MAX_CHAR_LENGTH}.</li>
|
||||
+ * <li>Track the total amount of data read from the metadata XML. The
|
||||
+ * {@link MetadataReadBytesTracker} ensures that the cumulative size of all attributes
|
||||
+ * does not exceed {@link #MAX_METADATA_SIZE_BYTES}.
|
||||
+ * </ul>
|
||||
+ *
|
||||
+ * @hide
|
||||
+ */
|
||||
+ @VisibleForTesting
|
||||
+ public static final class TypedArrayWrapper implements AutoCloseable {
|
||||
+ /** The underlying {@link TypedArray} to read from. */
|
||||
+ @NonNull
|
||||
+ private final TypedArray mTypedArray;
|
||||
+ /** Tracker for enforcing metadata size limits. */
|
||||
+ @NonNull
|
||||
+ private final MetadataReadBytesTracker mReadTracker;
|
||||
+ /** {@code true} if parsing a {@code <subtype>} tag, {@code false} otherwise. */
|
||||
+ private final boolean mIsReadingSubtype;
|
||||
+
|
||||
+ /**
|
||||
+ * Creates a {@link TypedArrayWrapper} for parsing attributes of the main
|
||||
+ * {@code <input-method>} tag.
|
||||
+ *
|
||||
+ * @param wrapped The {@link TypedArray} obtained for the {@code <input-method>} tag.
|
||||
+ * @param readTracker The tracker for monitoring data size.
|
||||
+ * @return A new {@link TypedArrayWrapper} instance.
|
||||
+ */
|
||||
+ @NonNull
|
||||
+ @VisibleForTesting
|
||||
+ public static TypedArrayWrapper createForMethod(
|
||||
+ @NonNull TypedArray wrapped, @NonNull MetadataReadBytesTracker readTracker) {
|
||||
+ return new TypedArrayWrapper(wrapped, readTracker, false);
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Creates a {@link TypedArrayWrapper} for parsing attributes of a {@code <subtype>} tag.
|
||||
+ *
|
||||
+ * @param wrapped The {@link TypedArray} obtained for the {@code <subtype>} tag.
|
||||
+ * @param readTracker The tracker for monitoring data size.
|
||||
+ * @return A new {@link TypedArrayWrapper} instance.
|
||||
+ */
|
||||
+ @NonNull
|
||||
+ @VisibleForTesting
|
||||
+ public static TypedArrayWrapper createForSubtype(
|
||||
+ @NonNull TypedArray wrapped, @NonNull MetadataReadBytesTracker readTracker) {
|
||||
+ return new TypedArrayWrapper(wrapped, readTracker, true);
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Constructs a new wrapper.
|
||||
+ */
|
||||
+ private TypedArrayWrapper(@NonNull TypedArray wrapped,
|
||||
+ @NonNull MetadataReadBytesTracker readTracker, boolean isReadingSubtype) {
|
||||
+ mTypedArray = wrapped;
|
||||
+ mReadTracker = readTracker;
|
||||
+ mIsReadingSubtype = isReadingSubtype;
|
||||
+ }
|
||||
+
|
||||
+ /** Retrieves an integer value for the attribute at {@code index}. */
|
||||
+ @VisibleForTesting
|
||||
+ public int getInt(int index, int defaultValue) throws XmlPullParserException {
|
||||
+ if (!mTypedArray.hasValue(index)) {
|
||||
+ return defaultValue;
|
||||
+ }
|
||||
+ final int ret = mTypedArray.getInt(index, defaultValue);
|
||||
+ mReadTracker.onReadBytes(Integer.BYTES);
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ /** Retrieves the string value for the attribute at {@code index}. */
|
||||
+ @VisibleForTesting
|
||||
+ public String getString(int index) throws XmlPullParserException {
|
||||
+ final String ret = mTypedArray.getString(index);
|
||||
+ final int maxLen = getMaxLength(index);
|
||||
+ if (ret != null && ret.length() > maxLen) {
|
||||
+ throw new XmlPullParserException(
|
||||
+ "String resources in input method exceed the length limit of "
|
||||
+ + maxLen + " characters");
|
||||
+ }
|
||||
+ mReadTracker.onReadBytes(ret == null ? 0 : ret.length() * Character.BYTES);
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ /** Retrieves a boolean value for the attribute at {@code index}. */
|
||||
+ @VisibleForTesting
|
||||
+ public boolean getBoolean(int index, boolean defaultValue) throws XmlPullParserException {
|
||||
+ if (!mTypedArray.hasValue(index)) {
|
||||
+ return defaultValue;
|
||||
+ }
|
||||
+ final boolean ret = mTypedArray.getBoolean(index, defaultValue);
|
||||
+ mReadTracker.onReadBytes(1);
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ /** Retrieves a resource identifier for the attribute at {@code index}. */
|
||||
+ @VisibleForTesting
|
||||
+ public int getResourceId(int index, int defaultValue) throws XmlPullParserException {
|
||||
+ if (!mTypedArray.hasValue(index)) {
|
||||
+ return defaultValue;
|
||||
+ }
|
||||
+ final int ret = mTypedArray.getResourceId(index, defaultValue);
|
||||
+ mReadTracker.onReadBytes(Integer.BYTES);
|
||||
+ return ret;
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public void close() {
|
||||
+ mTypedArray.recycle();
|
||||
+ }
|
||||
+
|
||||
+ private int getMaxLength(int index) {
|
||||
+ // Note that the Android resource has limit DEFAULT_MAX_STRING_ATTR_LENGTH = 32_768.
|
||||
+ if (mIsReadingSubtype) {
|
||||
+ // No limits for strings in subtype for now.
|
||||
+ return Integer.MAX_VALUE;
|
||||
+ } else {
|
||||
+ return switch (index) {
|
||||
+ // TODO(b/456008595): Consider to add
|
||||
+ // InputMethod_stylusHandwritingSettingsActivity
|
||||
+ case com.android.internal.R.styleable.InputMethod_settingsActivity,
|
||||
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity ->
|
||||
+ COMPONENT_NAME_MAX_LENGTH;
|
||||
+ default ->
|
||||
+ // TODO(b/456008595): Consider to introduce limits.
|
||||
+ Integer.MAX_VALUE;
|
||||
+ };
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /** @hide */
|
||||
+ @VisibleForTesting
|
||||
+ public static final class MetadataReadBytesTracker {
|
||||
+ private int mRemainingBytes = MAX_METADATA_SIZE_BYTES;
|
||||
+
|
||||
+ @VisibleForTesting
|
||||
+ public MetadataReadBytesTracker() {
|
||||
+ }
|
||||
+
|
||||
+ private void onReadBytes(int bytes) throws XmlPullParserException {
|
||||
+ mRemainingBytes -= bytes;
|
||||
+ if (mRemainingBytes < 0) {
|
||||
+ throw new XmlPullParserException(
|
||||
+ "The input method service has metadata exceeds the "
|
||||
+ + MAX_METADATA_SIZE_BYTES + " byte limit");
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
|
||||
index dfe7d0306905..a7b2bba045dc 100644
|
||||
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
|
||||
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
|
||||
@@ -16,18 +16,27 @@
|
||||
|
||||
package android.view.inputmethod;
|
||||
|
||||
+import static android.view.inputmethod.InputMethodInfo.COMPONENT_NAME_MAX_LENGTH;
|
||||
+
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
+import static org.junit.Assert.assertThrows;
|
||||
+import static org.mockito.Mockito.mock;
|
||||
+import static org.mockito.Mockito.verify;
|
||||
+import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.XmlRes;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
+import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
+import android.view.inputmethod.InputMethodInfo.MetadataReadBytesTracker;
|
||||
+import android.view.inputmethod.InputMethodInfo.TypedArrayWrapper;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
@@ -38,6 +47,7 @@ import com.android.frameworks.inputmethodcoretests.R;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
+import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -133,6 +143,94 @@ public class InputMethodInfoTest {
|
||||
assertThat(clone.isVirtualDeviceOnly(), is(true));
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ public void testTypedArrayWrapper() throws Exception {
|
||||
+ final TypedArray mockTypedArray = mock(TypedArray.class);
|
||||
+ when(mockTypedArray.hasValue(0)).thenReturn(true);
|
||||
+ when(mockTypedArray.getInt(0, 0)).thenReturn(123);
|
||||
+ when(mockTypedArray.getString(1)).thenReturn("hello");
|
||||
+ when(mockTypedArray.hasValue(2)).thenReturn(true);
|
||||
+ when(mockTypedArray.getBoolean(2, false)).thenReturn(true);
|
||||
+ when(mockTypedArray.hasValue(3)).thenReturn(true);
|
||||
+ when(mockTypedArray.getResourceId(3, 0)).thenReturn(456);
|
||||
+
|
||||
+ try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray,
|
||||
+ new MetadataReadBytesTracker())) {
|
||||
+ assertThat(wrapper.getInt(0, 0), is(123));
|
||||
+ assertThat(wrapper.getString(1), is("hello"));
|
||||
+ assertThat(wrapper.getBoolean(2, false), is(true));
|
||||
+ assertThat(wrapper.getResourceId(3, 0), is(456));
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testTypedArrayWrapper_getString_throwsExceptionWhenStringTooLong()
|
||||
+ throws Exception {
|
||||
+ final TypedArray mockTypedArray = mock(TypedArray.class);
|
||||
+ final String longStringA = "a".repeat(COMPONENT_NAME_MAX_LENGTH + 1);
|
||||
+ final String longStringB = "b".repeat(COMPONENT_NAME_MAX_LENGTH + 1);
|
||||
+ when(mockTypedArray.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_settingsActivity))
|
||||
+ .thenReturn(longStringA);
|
||||
+ when(mockTypedArray.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity))
|
||||
+ .thenReturn(longStringB);
|
||||
+
|
||||
+ try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray,
|
||||
+ new MetadataReadBytesTracker())) {
|
||||
+ assertThrows(
|
||||
+ XmlPullParserException.class,
|
||||
+ () -> wrapper.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_settingsActivity));
|
||||
+ assertThrows(
|
||||
+ XmlPullParserException.class,
|
||||
+ () -> wrapper.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity));
|
||||
+ }
|
||||
+
|
||||
+ // The same index can be used for method and subtype for different attributes.
|
||||
+ // This verifies the same index returns the correct string for subtypes.
|
||||
+ try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForSubtype(mockTypedArray,
|
||||
+ new MetadataReadBytesTracker())) {
|
||||
+ assertThat(wrapper.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_settingsActivity),
|
||||
+ is(longStringA));
|
||||
+ assertThat(wrapper.getString(
|
||||
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity),
|
||||
+ is(longStringB));
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testTypedArrayWrapper_closeRecyclesTypedArray() {
|
||||
+ final TypedArray mockTypedArray = mock(TypedArray.class);
|
||||
+ final TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray,
|
||||
+ new MetadataReadBytesTracker());
|
||||
+
|
||||
+ wrapper.close();
|
||||
+
|
||||
+ verify(mockTypedArray).recycle();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testTypedArrayWrapper_metadataReadBytesTracker_throwsExceptionWhenLimitExceeded() {
|
||||
+ final TypedArray mockTypedArray = mock(TypedArray.class);
|
||||
+ final String longString = "a".repeat(1000);
|
||||
+ when(mockTypedArray.getString(0)).thenReturn(longString);
|
||||
+
|
||||
+ try (TypedArrayWrapper wrapper = TypedArrayWrapper.createForMethod(mockTypedArray,
|
||||
+ new MetadataReadBytesTracker())) {
|
||||
+ assertThrows(XmlPullParserException.class, () -> {
|
||||
+ // Each character is 2 bytes. 1000 chars * 2 = 2000 bytes per call.
|
||||
+ // Limit is 200 * 1024 = 204800 bytes.
|
||||
+ // 204800 / 2000 = 102.4. So 103 calls will exceed the limit.
|
||||
+ for (int i = 0; i < 103; ++i) {
|
||||
+ wrapper.getString(0);
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes)
|
||||
throws Exception {
|
||||
final Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,838 @@
|
|||
From a438ce172b441c8297eadde8d990ab292f5aa7d1 Mon Sep 17 00:00:00 2001
|
||||
From: Hiroki Sato <hirokisato@google.com>
|
||||
Date: Tue, 4 Nov 2025 17:29:42 +0900
|
||||
Subject: [PATCH] Introduce InputMethodSubtypeSafeList
|
||||
|
||||
IMM#getEnabledInputMethodSubtypeList() can return a large list of
|
||||
subtypes, which may cause a TransactionTooLargeException.
|
||||
|
||||
This patch introduces InputMethodSubtypeSafeList to wrap the list as a
|
||||
byte array, avoiding the exception. This mirrors the existing
|
||||
InputMethodInfoSafeList pattern introduced in [1].
|
||||
|
||||
Additionally, this change extracts the common marshalling logic from
|
||||
InputMethodInfoSafeList into a new AbstractSafeList and refactors both
|
||||
SafeList classes to extend it.
|
||||
|
||||
[1] I0a7667070fcdf17d34b248a5988c38064588718a
|
||||
|
||||
Bug: 449416164
|
||||
Bug: 449181366
|
||||
Bug: 449393786
|
||||
Bug: 449227003
|
||||
Test: CtsInputMethodTestCases:{InputMethodRegistrationTest,InputMethodInfoTest}
|
||||
Test: InputMethodCoreTests
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit 1d68a1099be2b99e8410dad01822851287994682)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:e99cb1a4f240988380e43592d845c64f78e1a6d7
|
||||
Merged-In: Ied64a9f018fd3e79cfc51ccd82d361b43e5f29dc
|
||||
Change-Id: Ied64a9f018fd3e79cfc51ccd82d361b43e5f29dc
|
||||
---
|
||||
.../IInputMethodManagerGlobalInvoker.java | 6 +-
|
||||
.../inputmethod/AbstractSafeList.java | 127 +++++++++++++++++
|
||||
.../inputmethod/InputMethodInfoSafeList.java | 105 +++-----------
|
||||
.../InputMethodSubtypeSafeList.aidl | 19 +++
|
||||
.../InputMethodSubtypeSafeList.java | 87 ++++++++++++
|
||||
.../internal/view/IInputMethodManager.aidl | 3 +-
|
||||
.../inputmethod/AbstractSafeListTest.java | 98 ++++++++++++++
|
||||
.../InputMethodSubtypeSafeListTest.java | 128 ++++++++++++++++++
|
||||
.../inputmethod/IInputMethodManagerImpl.java | 7 +-
|
||||
.../InputMethodManagerService.java | 13 +-
|
||||
.../server/inputmethod/ZeroJankProxy.java | 4 +-
|
||||
11 files changed, 497 insertions(+), 100 deletions(-)
|
||||
create mode 100644 core/java/com/android/internal/inputmethod/AbstractSafeList.java
|
||||
create mode 100644 core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl
|
||||
create mode 100644 core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java
|
||||
create mode 100644 core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java
|
||||
create mode 100644 core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java
|
||||
|
||||
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
|
||||
index 290885593ee6..6fbb76577fb8 100644
|
||||
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
|
||||
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
|
||||
@@ -43,6 +43,7 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.inputmethod.InputMethodInfoSafeList;
|
||||
+import com.android.internal.inputmethod.InputMethodSubtypeSafeList;
|
||||
import com.android.internal.inputmethod.SoftInputShowHideReason;
|
||||
import com.android.internal.inputmethod.StartInputFlags;
|
||||
import com.android.internal.inputmethod.StartInputReason;
|
||||
@@ -297,8 +298,9 @@ final class IInputMethodManagerGlobalInvoker {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
- return service.getEnabledInputMethodSubtypeList(imiId,
|
||||
- allowsImplicitlyEnabledSubtypes, userId);
|
||||
+ return InputMethodSubtypeSafeList.extractFrom(
|
||||
+ service.getEnabledInputMethodSubtypeList(imiId,
|
||||
+ allowsImplicitlyEnabledSubtypes, userId));
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
diff --git a/core/java/com/android/internal/inputmethod/AbstractSafeList.java b/core/java/com/android/internal/inputmethod/AbstractSafeList.java
|
||||
new file mode 100644
|
||||
index 000000000000..697b153afecf
|
||||
--- /dev/null
|
||||
+++ b/core/java/com/android/internal/inputmethod/AbstractSafeList.java
|
||||
@@ -0,0 +1,127 @@
|
||||
+/*
|
||||
+ * Copyright 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.internal.inputmethod;
|
||||
+
|
||||
+import android.annotation.NonNull;
|
||||
+import android.annotation.Nullable;
|
||||
+import android.os.Parcel;
|
||||
+import android.os.Parcelable;
|
||||
+
|
||||
+import com.android.internal.annotations.VisibleForTesting;
|
||||
+
|
||||
+import java.util.ArrayList;
|
||||
+import java.util.List;
|
||||
+
|
||||
+/**
|
||||
+ * An abstract base class for creating a {@link Parcelable} container that can hold an arbitrary
|
||||
+ * number of {@link Parcelable} objects without worrying about
|
||||
+ * {@link android.os.TransactionTooLargeException}.
|
||||
+ *
|
||||
+ * @see Parcel#readBlob()
|
||||
+ * @see Parcel#writeBlob(byte[])
|
||||
+ *
|
||||
+ * @param <T> The type of the {@link Parcelable} objects.
|
||||
+ */
|
||||
+public abstract class AbstractSafeList<T extends Parcelable> implements Parcelable {
|
||||
+ @Nullable
|
||||
+ private byte[] mBuffer;
|
||||
+
|
||||
+ protected AbstractSafeList(@Nullable List<T> list) {
|
||||
+ if (list != null && !list.isEmpty()) {
|
||||
+ mBuffer = marshall(list);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ protected AbstractSafeList(@Nullable byte[] buffer) {
|
||||
+ mBuffer = buffer;
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Extracts the list of {@link Parcelable} objects from a {@link AbstractSafeList}, and
|
||||
+ * clears the internal buffer of the list.
|
||||
+ *
|
||||
+ * @param from The {@link AbstractSafeList} to extract from.
|
||||
+ * @param creator The {@link Parcelable.Creator} for the {@link Parcelable} objects.
|
||||
+ * @param <T> The type of the {@link Parcelable} objects.
|
||||
+ * @return The list of {@link Parcelable} objects.
|
||||
+ */
|
||||
+ @NonNull
|
||||
+ protected static <T extends Parcelable> List<T> extractFrom(
|
||||
+ @Nullable AbstractSafeList<T> from, @NonNull Parcelable.Creator<T> creator) {
|
||||
+ if (from == null) {
|
||||
+ return new ArrayList<>();
|
||||
+ }
|
||||
+ final byte[] buf = from.mBuffer;
|
||||
+ from.mBuffer = null;
|
||||
+ if (buf != null) {
|
||||
+ final List<T> list = unmarshall(buf, creator);
|
||||
+ if (list != null) {
|
||||
+ return list;
|
||||
+ }
|
||||
+ }
|
||||
+ return new ArrayList<>();
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public int describeContents() {
|
||||
+ // As long as the parcelled classes return 0, we can also return 0 here.
|
||||
+ return 0;
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
+ dest.writeBlob(mBuffer);
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Marshalls a list of {@link Parcelable} objects into a byte array.
|
||||
+ */
|
||||
+ @Nullable
|
||||
+ @VisibleForTesting
|
||||
+ public static <T extends Parcelable> byte[] marshall(@NonNull List<T> list) {
|
||||
+ Parcel parcel = null;
|
||||
+ try {
|
||||
+ parcel = Parcel.obtain();
|
||||
+ parcel.writeTypedList(list);
|
||||
+ return parcel.marshall();
|
||||
+ } finally {
|
||||
+ if (parcel != null) {
|
||||
+ parcel.recycle();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Unmarshalls a byte array into a list of {@link Parcelable} objects.
|
||||
+ */
|
||||
+ @Nullable
|
||||
+ @VisibleForTesting
|
||||
+ public static <T extends Parcelable> List<T> unmarshall(
|
||||
+ @NonNull byte[] data, @NonNull Parcelable.Creator<T> creator) {
|
||||
+ Parcel parcel = null;
|
||||
+ try {
|
||||
+ parcel = Parcel.obtain();
|
||||
+ parcel.unmarshall(data, 0, data.length);
|
||||
+ parcel.setDataPosition(0);
|
||||
+ return parcel.createTypedArrayList(creator);
|
||||
+ } finally {
|
||||
+ if (parcel != null) {
|
||||
+ parcel.recycle();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java b/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java
|
||||
index 9e720fb6ccee..a2ea5b08f13f 100644
|
||||
--- a/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java
|
||||
+++ b/core/java/com/android/internal/inputmethod/InputMethodInfoSafeList.java
|
||||
@@ -19,24 +19,24 @@ package com.android.internal.inputmethod;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Parcel;
|
||||
-import android.os.Parcelable;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
|
||||
-import java.util.ArrayList;
|
||||
-import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
- * A {@link Parcelable} container that can holds an arbitrary number of {@link InputMethodInfo}
|
||||
- * without worrying about {@link android.os.TransactionTooLargeException} when passing across
|
||||
- * process boundary.
|
||||
- *
|
||||
- * @see Parcel#readBlob()
|
||||
- * @see Parcel#writeBlob(byte[])
|
||||
+ * A {@link android.os.Parcelable} container that can hold an arbitrary number of
|
||||
+ * {@link InputMethodInfo} without worrying about
|
||||
+ * {@link android.os.TransactionTooLargeException} when passing across process boundary.
|
||||
*/
|
||||
-public final class InputMethodInfoSafeList implements Parcelable {
|
||||
- @Nullable
|
||||
- private byte[] mBuffer;
|
||||
+public final class InputMethodInfoSafeList extends AbstractSafeList<InputMethodInfo> {
|
||||
+
|
||||
+ private InputMethodInfoSafeList(@Nullable byte[] buffer) {
|
||||
+ super(buffer);
|
||||
+ }
|
||||
+
|
||||
+ private InputMethodInfoSafeList(@Nullable List<InputMethodInfo> list) {
|
||||
+ super(list);
|
||||
+ }
|
||||
|
||||
/**
|
||||
* Instantiates a list of {@link InputMethodInfo} from the given {@link InputMethodInfoSafeList}
|
||||
@@ -53,81 +53,20 @@ public final class InputMethodInfoSafeList implements Parcelable {
|
||||
*/
|
||||
@NonNull
|
||||
public static List<InputMethodInfo> extractFrom(@Nullable InputMethodInfoSafeList from) {
|
||||
- final byte[] buf = from.mBuffer;
|
||||
- from.mBuffer = null;
|
||||
- if (buf != null) {
|
||||
- final InputMethodInfo[] array = unmarshall(buf);
|
||||
- if (array != null) {
|
||||
- return new ArrayList<>(Arrays.asList(array));
|
||||
- }
|
||||
- }
|
||||
- return new ArrayList<>();
|
||||
- }
|
||||
-
|
||||
- @NonNull
|
||||
- private static InputMethodInfo[] toArray(@Nullable List<InputMethodInfo> original) {
|
||||
- if (original == null) {
|
||||
- return new InputMethodInfo[0];
|
||||
- }
|
||||
- return original.toArray(new InputMethodInfo[0]);
|
||||
- }
|
||||
-
|
||||
- @Nullable
|
||||
- private static byte[] marshall(@NonNull InputMethodInfo[] array) {
|
||||
- Parcel parcel = null;
|
||||
- try {
|
||||
- parcel = Parcel.obtain();
|
||||
- parcel.writeTypedArray(array, 0);
|
||||
- return parcel.marshall();
|
||||
- } finally {
|
||||
- if (parcel != null) {
|
||||
- parcel.recycle();
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- @Nullable
|
||||
- private static InputMethodInfo[] unmarshall(byte[] data) {
|
||||
- Parcel parcel = null;
|
||||
- try {
|
||||
- parcel = Parcel.obtain();
|
||||
- parcel.unmarshall(data, 0, data.length);
|
||||
- parcel.setDataPosition(0);
|
||||
- return parcel.createTypedArray(InputMethodInfo.CREATOR);
|
||||
- } finally {
|
||||
- if (parcel != null) {
|
||||
- parcel.recycle();
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- private InputMethodInfoSafeList(@Nullable byte[] blob) {
|
||||
- mBuffer = blob;
|
||||
+ return AbstractSafeList.extractFrom(from, InputMethodInfo.CREATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates {@link InputMethodInfoSafeList} from the given list of {@link InputMethodInfo}.
|
||||
*
|
||||
* @param list list of {@link InputMethodInfo} from which {@link InputMethodInfoSafeList} will
|
||||
- * be created
|
||||
+ * be created. Giving {@code null} will result in an empty
|
||||
+ * {@link InputMethodInfoSafeList}.
|
||||
* @return {@link InputMethodInfoSafeList} that stores the given list of {@link InputMethodInfo}
|
||||
*/
|
||||
@NonNull
|
||||
public static InputMethodInfoSafeList create(@Nullable List<InputMethodInfo> list) {
|
||||
- if (list == null || list.isEmpty()) {
|
||||
- return empty();
|
||||
- }
|
||||
- return new InputMethodInfoSafeList(marshall(toArray(list)));
|
||||
- }
|
||||
-
|
||||
- /**
|
||||
- * Creates an empty {@link InputMethodInfoSafeList}.
|
||||
- *
|
||||
- * @return {@link InputMethodInfoSafeList} that is empty
|
||||
- */
|
||||
- @NonNull
|
||||
- public static InputMethodInfoSafeList empty() {
|
||||
- return new InputMethodInfoSafeList(null);
|
||||
+ return new InputMethodInfoSafeList(list);
|
||||
}
|
||||
|
||||
public static final Creator<InputMethodInfoSafeList> CREATOR = new Creator<>() {
|
||||
@@ -141,16 +80,4 @@ public final class InputMethodInfoSafeList implements Parcelable {
|
||||
return new InputMethodInfoSafeList[size];
|
||||
}
|
||||
};
|
||||
-
|
||||
- @Override
|
||||
- public int describeContents() {
|
||||
- // As long as InputMethodInfo#describeContents() is guaranteed to return 0, we can always
|
||||
- // return 0 here.
|
||||
- return 0;
|
||||
- }
|
||||
-
|
||||
- @Override
|
||||
- public void writeToParcel(Parcel dest, int flags) {
|
||||
- dest.writeBlob(mBuffer);
|
||||
- }
|
||||
}
|
||||
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl
|
||||
new file mode 100644
|
||||
index 000000000000..11000632eba5
|
||||
--- /dev/null
|
||||
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.aidl
|
||||
@@ -0,0 +1,19 @@
|
||||
+/*
|
||||
+ * Copyright 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.internal.inputmethod;
|
||||
+
|
||||
+parcelable InputMethodSubtypeSafeList;
|
||||
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java
|
||||
new file mode 100644
|
||||
index 000000000000..cd95088f5cf0
|
||||
--- /dev/null
|
||||
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSafeList.java
|
||||
@@ -0,0 +1,87 @@
|
||||
+/*
|
||||
+ * Copyright 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.internal.inputmethod;
|
||||
+
|
||||
+import android.annotation.NonNull;
|
||||
+import android.annotation.Nullable;
|
||||
+import android.os.Parcel;
|
||||
+import android.view.inputmethod.InputMethodSubtype;
|
||||
+
|
||||
+import java.util.List;
|
||||
+
|
||||
+/**
|
||||
+ * A {@link android.os.Parcelable} container that can hold an arbitrary number of
|
||||
+ * {@link InputMethodSubtype} without worrying about
|
||||
+ * {@link android.os.TransactionTooLargeException} when passing across process boundary.
|
||||
+ */
|
||||
+public final class InputMethodSubtypeSafeList extends AbstractSafeList<InputMethodSubtype> {
|
||||
+
|
||||
+ private InputMethodSubtypeSafeList(@Nullable byte[] buffer) {
|
||||
+ super(buffer);
|
||||
+ }
|
||||
+
|
||||
+ private InputMethodSubtypeSafeList(@Nullable List<InputMethodSubtype> list) {
|
||||
+ super(list);
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Instantiates a list of {@link InputMethodSubtype} from the given
|
||||
+ * {@link InputMethodSubtypeSafeList} then clears the internal buffer of
|
||||
+ * {@link InputMethodSubtypeSafeList}.
|
||||
+ *
|
||||
+ * <p>Note that each {@link InputMethodSubtype} item is guaranteed to be a copy of the original
|
||||
+ * {@link InputMethodSubtype} object.</p>
|
||||
+ *
|
||||
+ * <p>Any subsequent call will return an empty list.</p>
|
||||
+ *
|
||||
+ * @param from {@link InputMethodSubtypeSafeList} from which the list of
|
||||
+ * {@link InputMethodSubtype} will be extracted
|
||||
+ * @return list of {@link InputMethodSubtype} stored in the given
|
||||
+ * {@link InputMethodSubtypeSafeList}
|
||||
+ */
|
||||
+ @NonNull
|
||||
+ public static List<InputMethodSubtype> extractFrom(@Nullable InputMethodSubtypeSafeList from) {
|
||||
+ return AbstractSafeList.extractFrom(from, InputMethodSubtype.CREATOR);
|
||||
+ }
|
||||
+
|
||||
+ /**
|
||||
+ * Instantiates {@link InputMethodSubtypeSafeList} from the given list of
|
||||
+ * {@link InputMethodSubtype}.
|
||||
+ *
|
||||
+ * @param list list of {@link InputMethodSubtype} from which
|
||||
+ * {@link InputMethodSubtypeSafeList} will be created. Giving {@code null} will
|
||||
+ * result in an empty {@link InputMethodSubtypeSafeList}.
|
||||
+ * @return {@link InputMethodSubtypeSafeList} that stores the given list of
|
||||
+ * {@link InputMethodSubtype}
|
||||
+ */
|
||||
+ @NonNull
|
||||
+ public static InputMethodSubtypeSafeList create(@Nullable List<InputMethodSubtype> list) {
|
||||
+ return new InputMethodSubtypeSafeList(list);
|
||||
+ }
|
||||
+
|
||||
+ public static final Creator<InputMethodSubtypeSafeList> CREATOR = new Creator<>() {
|
||||
+ @Override
|
||||
+ public InputMethodSubtypeSafeList createFromParcel(Parcel in) {
|
||||
+ return new InputMethodSubtypeSafeList(in.readBlob());
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public InputMethodSubtypeSafeList[] newArray(int size) {
|
||||
+ return new InputMethodSubtypeSafeList[size];
|
||||
+ }
|
||||
+ };
|
||||
+}
|
||||
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
|
||||
index 0791612fa0e8..d9b52404724e 100644
|
||||
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
|
||||
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
|
||||
@@ -32,6 +32,7 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.inputmethod.InputMethodInfoSafeList;
|
||||
+import com.android.internal.inputmethod.InputMethodSubtypeSafeList;
|
||||
|
||||
/**
|
||||
* Public interface to the global input method manager, used by all client applications.
|
||||
@@ -67,7 +68,7 @@ interface IInputMethodManager {
|
||||
|
||||
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
|
||||
+ "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
|
||||
- List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in @nullable String imiId,
|
||||
+ InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(in @nullable String imiId,
|
||||
boolean allowsImplicitlyEnabledSubtypes, int userId);
|
||||
|
||||
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
|
||||
diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java
|
||||
new file mode 100644
|
||||
index 000000000000..0f72f095dbe3
|
||||
--- /dev/null
|
||||
+++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/AbstractSafeListTest.java
|
||||
@@ -0,0 +1,98 @@
|
||||
+/*
|
||||
+ * Copyright 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.internal.inputmethod;
|
||||
+
|
||||
+import static org.junit.Assert.assertEquals;
|
||||
+import static org.junit.Assert.assertNotNull;
|
||||
+
|
||||
+import android.os.Parcel;
|
||||
+import android.os.Parcelable;
|
||||
+import android.platform.test.annotations.Presubmit;
|
||||
+
|
||||
+import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
+import androidx.test.filters.SmallTest;
|
||||
+
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+
|
||||
+import java.util.List;
|
||||
+
|
||||
+@SmallTest
|
||||
+@Presubmit
|
||||
+@RunWith(AndroidJUnit4.class)
|
||||
+public class AbstractSafeListTest {
|
||||
+
|
||||
+ private static class TestParcelable implements Parcelable {
|
||||
+ final int mData;
|
||||
+
|
||||
+ TestParcelable(int data) {
|
||||
+ mData = data;
|
||||
+ }
|
||||
+
|
||||
+ TestParcelable(Parcel parcel) {
|
||||
+ mData = parcel.readInt();
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public void writeToParcel(Parcel parcel, int flags) {
|
||||
+ parcel.writeInt(mData);
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public int describeContents() {
|
||||
+ return 0;
|
||||
+ }
|
||||
+
|
||||
+ @SuppressWarnings("EffectivelyPrivate") // Parcelable must have CREATOR.
|
||||
+ public static final Creator<TestParcelable> CREATOR = new Creator<TestParcelable>() {
|
||||
+ @Override
|
||||
+ public TestParcelable createFromParcel(Parcel parcel) {
|
||||
+ return new TestParcelable(parcel);
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public TestParcelable[] newArray(int size) {
|
||||
+ return new TestParcelable[size];
|
||||
+ }
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testMarshallThenUnmarshall() {
|
||||
+ List<TestParcelable> originalArray = List.of(new TestParcelable(1), new TestParcelable(2));
|
||||
+ byte[] marshalled = AbstractSafeList.marshall(originalArray);
|
||||
+ assertNotNull(marshalled);
|
||||
+ List<TestParcelable> unmarshalled =
|
||||
+ AbstractSafeList.unmarshall(marshalled, TestParcelable.CREATOR);
|
||||
+ assertNotNull(unmarshalled);
|
||||
+ assertEquals(originalArray.size(), unmarshalled.size());
|
||||
+ for (int i = 0; i < originalArray.size(); i++) {
|
||||
+ assertEquals(originalArray.get(i).mData, unmarshalled.get(i).mData);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testMarshallEmptyArray() {
|
||||
+ List<TestParcelable> originalArray = List.of();
|
||||
+ byte[] marshalled = AbstractSafeList.marshall(originalArray);
|
||||
+ assertNotNull(marshalled);
|
||||
+ List<TestParcelable> unmarshalled =
|
||||
+ AbstractSafeList.unmarshall(marshalled, TestParcelable.CREATOR);
|
||||
+ assertNotNull(unmarshalled);
|
||||
+ assertEquals(0, unmarshalled.size());
|
||||
+ }
|
||||
+}
|
||||
diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java
|
||||
new file mode 100644
|
||||
index 000000000000..089ffb80d7a9
|
||||
--- /dev/null
|
||||
+++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeSafeListTest.java
|
||||
@@ -0,0 +1,128 @@
|
||||
+/*
|
||||
+ * Copyright 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.internal.inputmethod;
|
||||
+
|
||||
+import static org.junit.Assert.assertEquals;
|
||||
+import static org.junit.Assert.assertNotNull;
|
||||
+import static org.junit.Assert.assertNotSame;
|
||||
+import static org.junit.Assert.assertTrue;
|
||||
+
|
||||
+import android.os.Parcel;
|
||||
+import android.platform.test.annotations.Presubmit;
|
||||
+import android.view.inputmethod.InputMethodSubtype;
|
||||
+
|
||||
+import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
+import androidx.test.filters.SmallTest;
|
||||
+
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+
|
||||
+import java.util.ArrayList;
|
||||
+import java.util.Collections;
|
||||
+import java.util.List;
|
||||
+import java.util.function.Function;
|
||||
+
|
||||
+@SmallTest
|
||||
+@Presubmit
|
||||
+@RunWith(AndroidJUnit4.class)
|
||||
+public class InputMethodSubtypeSafeListTest {
|
||||
+
|
||||
+ private static InputMethodSubtype createFakeInputMethodSubtype(String locale, String mode) {
|
||||
+ return new InputMethodSubtype.InputMethodSubtypeBuilder()
|
||||
+ .setSubtypeLocale(locale)
|
||||
+ .setSubtypeMode(mode)
|
||||
+ .build();
|
||||
+ }
|
||||
+
|
||||
+ private static List<InputMethodSubtype> createTestInputMethodSubtypeList() {
|
||||
+ List<InputMethodSubtype> list = new ArrayList<>();
|
||||
+ list.add(createFakeInputMethodSubtype("en_US", "keyboard"));
|
||||
+ list.add(createFakeInputMethodSubtype("ja_JP", "keyboard"));
|
||||
+ list.add(createFakeInputMethodSubtype("en_GB", "voice"));
|
||||
+ return list;
|
||||
+ }
|
||||
+
|
||||
+ private static void assertItemsAfterExtract(
|
||||
+ List<InputMethodSubtype> originals,
|
||||
+ Function<List<InputMethodSubtype>, InputMethodSubtypeSafeList> factory) {
|
||||
+ InputMethodSubtypeSafeList list = factory.apply(originals);
|
||||
+ List<InputMethodSubtype> extracted = InputMethodSubtypeSafeList.extractFrom(list);
|
||||
+ assertEquals(originals.size(), extracted.size());
|
||||
+ for (int i = 0; i < originals.size(); i++) {
|
||||
+ assertNotSame(
|
||||
+ "InputMethodSubtypeSafeList.extractFrom() must clone each instance",
|
||||
+ originals.get(i), extracted.get(i));
|
||||
+ assertEquals(
|
||||
+ "Verify the cloned instances have the equal locale",
|
||||
+ originals.get(i).getLocale(), extracted.get(i).getLocale());
|
||||
+ assertEquals(
|
||||
+ "Verify the cloned instances have the equal mode",
|
||||
+ originals.get(i).getMode(), extracted.get(i).getMode());
|
||||
+ }
|
||||
+
|
||||
+ // Subsequent calls of InputMethodSubtypeSafeList.extractFrom() return an empty list.
|
||||
+ List<InputMethodSubtype> extracted2 = InputMethodSubtypeSafeList.extractFrom(list);
|
||||
+ assertTrue(extracted2.isEmpty());
|
||||
+ }
|
||||
+
|
||||
+ private static InputMethodSubtypeSafeList cloneViaParcel(InputMethodSubtypeSafeList original) {
|
||||
+ Parcel parcel = null;
|
||||
+ try {
|
||||
+ parcel = Parcel.obtain();
|
||||
+ original.writeToParcel(parcel, 0);
|
||||
+ parcel.setDataPosition(0);
|
||||
+ InputMethodSubtypeSafeList newInstance =
|
||||
+ InputMethodSubtypeSafeList.CREATOR.createFromParcel(parcel);
|
||||
+ assertNotNull(newInstance);
|
||||
+ return newInstance;
|
||||
+ } finally {
|
||||
+ if (parcel != null) {
|
||||
+ parcel.recycle();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testCreate() {
|
||||
+ assertNotNull(InputMethodSubtypeSafeList.create(createTestInputMethodSubtypeList()));
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testExtract() {
|
||||
+ assertItemsAfterExtract(
|
||||
+ createTestInputMethodSubtypeList(),
|
||||
+ InputMethodSubtypeSafeList::create);
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testExtractAfterParceling() {
|
||||
+ assertItemsAfterExtract(
|
||||
+ createTestInputMethodSubtypeList(),
|
||||
+ originals -> cloneViaParcel(InputMethodSubtypeSafeList.create(originals)));
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testExtractEmptyList() {
|
||||
+ assertItemsAfterExtract(Collections.emptyList(), InputMethodSubtypeSafeList::create);
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void testExtractAfterParcelingEmptyList() {
|
||||
+ assertItemsAfterExtract(Collections.emptyList(),
|
||||
+ originals -> cloneViaParcel(InputMethodSubtypeSafeList.create(originals)));
|
||||
+ }
|
||||
+}
|
||||
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
|
||||
index 15f186b047f2..7845a73111d6 100644
|
||||
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
|
||||
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
|
||||
@@ -48,6 +48,7 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.inputmethod.InputMethodInfoSafeList;
|
||||
+import com.android.internal.inputmethod.InputMethodSubtypeSafeList;
|
||||
import com.android.internal.inputmethod.SoftInputShowHideReason;
|
||||
import com.android.internal.inputmethod.StartInputFlags;
|
||||
import com.android.internal.inputmethod.StartInputReason;
|
||||
@@ -108,7 +109,8 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub {
|
||||
@NonNull
|
||||
List<InputMethodInfo> getEnabledInputMethodListLegacy(@UserIdInt int userId);
|
||||
|
||||
- List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
|
||||
+ @NonNull
|
||||
+ InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId,
|
||||
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId);
|
||||
|
||||
InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId);
|
||||
@@ -278,8 +280,9 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub {
|
||||
return mCallback.getEnabledInputMethodListLegacy(userId);
|
||||
}
|
||||
|
||||
+ @NonNull
|
||||
@Override
|
||||
- public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
|
||||
+ public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId,
|
||||
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
|
||||
return mCallback.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes,
|
||||
userId);
|
||||
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
|
||||
index 5e3224d1012e..7df43d114906 100644
|
||||
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
|
||||
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
|
||||
@@ -172,6 +172,7 @@ import com.android.internal.inputmethod.InputMethodDebug;
|
||||
import com.android.internal.inputmethod.InputMethodInfoSafeList;
|
||||
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
|
||||
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
|
||||
+import com.android.internal.inputmethod.InputMethodSubtypeSafeList;
|
||||
import com.android.internal.inputmethod.SoftInputShowHideReason;
|
||||
import com.android.internal.inputmethod.StartInputFlags;
|
||||
import com.android.internal.inputmethod.StartInputReason;
|
||||
@@ -1590,7 +1591,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
|
||||
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
|
||||
}
|
||||
if (!mUserManagerInternal.exists(userId)) {
|
||||
- return InputMethodInfoSafeList.empty();
|
||||
+ return InputMethodInfoSafeList.create(null);
|
||||
}
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
@@ -1611,7 +1612,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
|
||||
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
|
||||
}
|
||||
if (!mUserManagerInternal.exists(userId)) {
|
||||
- return InputMethodInfoSafeList.empty();
|
||||
+ return InputMethodInfoSafeList.create(null);
|
||||
}
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
@@ -1738,8 +1739,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
|
||||
* subtypes
|
||||
* @param userId the user ID to be queried about
|
||||
*/
|
||||
+ @NonNull
|
||||
@Override
|
||||
- public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
|
||||
+ public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId,
|
||||
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
|
||||
if (UserHandle.getCallingUserId() != userId) {
|
||||
mContext.enforceCallingOrSelfPermission(
|
||||
@@ -1749,8 +1751,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
try {
|
||||
- return getEnabledInputMethodSubtypeListInternal(imiId,
|
||||
- allowsImplicitlyEnabledSubtypes, userId, callingUid);
|
||||
+ return InputMethodSubtypeSafeList.create(
|
||||
+ getEnabledInputMethodSubtypeListInternal(imiId,
|
||||
+ allowsImplicitlyEnabledSubtypes, userId, callingUid));
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(ident);
|
||||
}
|
||||
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
|
||||
index 12c1d9cbb2a1..6a83475fb774 100644
|
||||
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
|
||||
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
|
||||
@@ -65,6 +65,7 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.inputmethod.InputMethodInfoSafeList;
|
||||
+import com.android.internal.inputmethod.InputMethodSubtypeSafeList;
|
||||
import com.android.internal.inputmethod.SoftInputShowHideReason;
|
||||
import com.android.internal.inputmethod.StartInputFlags;
|
||||
import com.android.internal.inputmethod.StartInputReason;
|
||||
@@ -160,8 +161,9 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback {
|
||||
return mInner.getEnabledInputMethodListLegacy(userId);
|
||||
}
|
||||
|
||||
+ @NonNull
|
||||
@Override
|
||||
- public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
|
||||
+ public InputMethodSubtypeSafeList getEnabledInputMethodSubtypeList(String imiId,
|
||||
boolean allowsImplicitlyEnabledSubtypes, int userId) {
|
||||
return mInner.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes,
|
||||
userId);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
From cee45869c491d4e39877918ee881eb60dec7d6e5 Mon Sep 17 00:00:00 2001
|
||||
From: Iustin Ventaniuc <iustiniv@google.com>
|
||||
Date: Thu, 13 Nov 2025 09:48:16 +0000
|
||||
Subject: [PATCH] Handle loadDescription OutOfMemoryError in DeviceAdminInfo
|
||||
|
||||
loadDescription was potentially vulnerable to an attack which causes a
|
||||
DoS exploit by injecting a maliciously large string into the Receiver's
|
||||
label.
|
||||
|
||||
Bug: 443062265
|
||||
Test: manual
|
||||
Flag: EXEMPT BUGFIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:06a5b2327caa3aa8843496458e98b9bb070df6e5
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:660101e3cdd2f8e8bc627517691dfb885a5c8302
|
||||
Merged-In: Icab26c4b77e73f0fcb9a560e3211482ebe2f37bf
|
||||
Change-Id: Icab26c4b77e73f0fcb9a560e3211482ebe2f37bf
|
||||
---
|
||||
core/java/android/app/admin/DeviceAdminInfo.java | 6 +++++-
|
||||
1 file changed, 5 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
|
||||
index 7b46db16f80a..7616d805c31c 100644
|
||||
--- a/core/java/android/app/admin/DeviceAdminInfo.java
|
||||
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
|
||||
@@ -473,8 +473,12 @@ public final class DeviceAdminInfo implements Parcelable {
|
||||
*/
|
||||
public CharSequence loadDescription(PackageManager pm) throws NotFoundException {
|
||||
if (mActivityInfo.descriptionRes != 0) {
|
||||
- return pm.getText(mActivityInfo.packageName,
|
||||
+ try {
|
||||
+ return pm.getText(mActivityInfo.packageName,
|
||||
mActivityInfo.descriptionRes, mActivityInfo.applicationInfo);
|
||||
+ } catch (OutOfMemoryError e) {
|
||||
+ throw new NotFoundException();
|
||||
+ }
|
||||
}
|
||||
throw new NotFoundException();
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
From c148b4fae6347652231d1c4a633f5cc9a8f057f8 Mon Sep 17 00:00:00 2001
|
||||
From: Annie Lin <theannielin@google.com>
|
||||
Date: Wed, 19 Nov 2025 20:16:52 +0000
|
||||
Subject: [PATCH] Prevent launchedFromPackage spoofing via
|
||||
FLAG_ACTIVITY_FORWARD_RESULT.
|
||||
|
||||
Bug: 457742426
|
||||
Test: atest ActivityStarterTests
|
||||
Test: Verified via test app
|
||||
Flag: EXEMPT CVE_FIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:3bb240273822e41f3c6911c60d15983a600308f7
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:79fe04dbdf31ab80311069a4d0a7b518d47c31ac
|
||||
Merged-In: Ic9637c56803b00acc9fca59f8092ed02dd46a4fb
|
||||
Change-Id: Ic9637c56803b00acc9fca59f8092ed02dd46a4fb
|
||||
---
|
||||
.../android/server/wm/ActivityStarter.java | 20 +++--
|
||||
.../server/wm/ActivityStarterTests.java | 82 +++++++++++++++++++
|
||||
2 files changed, 97 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
|
||||
index 92f51bed419f..222698ce24db 100644
|
||||
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
|
||||
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
|
||||
@@ -1142,14 +1142,24 @@ class ActivityStarter {
|
||||
// in the flow, and asking to forward its result back to the previous. In this
|
||||
// case the activity is serving as a trampoline between the two, so we also want
|
||||
// to update its launchedFromPackage to be the same as the previous activity.
|
||||
- // Note that this is safe, since we know these two packages come from the same
|
||||
- // uid; the caller could just as well have supplied that same package name itself
|
||||
- // . This specifially deals with the case of an intent picker/chooser being
|
||||
+ // This specifically deals with the case of an intent picker/chooser being
|
||||
// launched in the app flow to redirect to an activity picked by the user, where
|
||||
// we want the final activity to consider it to have been launched by the
|
||||
// previous app activity.
|
||||
- callingPackage = sourceRecord.launchedFromPackage;
|
||||
- callingFeatureId = sourceRecord.launchedFromFeatureId;
|
||||
+ final String launchedFromPackage = sourceRecord.launchedFromPackage;
|
||||
+ if (launchedFromPackage != null) {
|
||||
+ final PackageManagerInternal pmInternal =
|
||||
+ mService.getPackageManagerInternalLocked();
|
||||
+ final int packageUid = pmInternal.getPackageUid(
|
||||
+ launchedFromPackage, 0 /* flags */,
|
||||
+ UserHandle.getUserId(callingUid));
|
||||
+ // Only override callingPackage and callingFeatureId based on package UID check.
|
||||
+ // This is to prevent spoofing. See b/457742426.
|
||||
+ if (UserHandle.isSameApp(packageUid, callingUid)) {
|
||||
+ callingPackage = launchedFromPackage;
|
||||
+ callingFeatureId = sourceRecord.launchedFromFeatureId;
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
|
||||
index 08b0077c49b3..2948b3da7db8 100644
|
||||
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
|
||||
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
|
||||
@@ -1882,6 +1882,88 @@ public class ActivityStarterTests extends WindowTestsBase {
|
||||
assertEquals(bubbledActivity.getTask(), targetRecord.getTask());
|
||||
}
|
||||
|
||||
+ /**
|
||||
+ * This test simulates the following scenario:
|
||||
+ * 1. Privileged app (P) starts malicious app's activity (M1).
|
||||
+ * 2. M1 starts M2 (also in malicious app) using startNextMatchingActivity().
|
||||
+ * This causes M2's launchedFromPackage to be P.
|
||||
+ * 3. M2 starts an activity in P (P2) using startActivity() with
|
||||
+ * FLAG_ACTIVITY_FORWARD_RESULT.
|
||||
+ * The test verifies that P2's launchedFromPackage is M, not P.
|
||||
+ * See b/457742426 for details.
|
||||
+ */
|
||||
+ @Test
|
||||
+ public void testLaunchedFromPackage_nextMatchingActivity_forwardResult() {
|
||||
+ final String privilegedPackage = "com.test.privileged";
|
||||
+ final int privilegedUid = 10001;
|
||||
+ final String maliciousPackage = "com.test.malicious";
|
||||
+ final int maliciousUid = 10002;
|
||||
+
|
||||
+ // Setup P1 activity
|
||||
+ final ActivityRecord p1 = new ActivityBuilder(mAtm)
|
||||
+ .setComponent(new ComponentName(privilegedPackage, "P1Activity"))
|
||||
+ .setUid(privilegedUid)
|
||||
+ .setCreateTask(true)
|
||||
+ .build();
|
||||
+
|
||||
+ // Setup M1 activity, launched by P1
|
||||
+ final ActivityRecord m1 = new ActivityBuilder(mAtm)
|
||||
+ .setComponent(new ComponentName(maliciousPackage, "M1Activity"))
|
||||
+ .setUid(maliciousUid)
|
||||
+ .setCreateTask(true)
|
||||
+ .setLaunchedFromPackage(privilegedPackage)
|
||||
+ .setLaunchedFromUid(privilegedUid)
|
||||
+ .build();
|
||||
+ m1.resultTo = p1;
|
||||
+
|
||||
+ // Setup M2 activity, as if launched from M1 via startNextMatchingActivity()
|
||||
+ final ActivityRecord m2 = new ActivityBuilder(mAtm)
|
||||
+ .setComponent(new ComponentName(maliciousPackage, "M2Activity"))
|
||||
+ .setUid(maliciousUid)
|
||||
+ .setCreateTask(true)
|
||||
+ .setLaunchedFromPackage(privilegedPackage) // Spoofed package name
|
||||
+ .setLaunchedFromUid(maliciousUid)
|
||||
+ .build();
|
||||
+ m2.resultTo = p1; // result is forwarded
|
||||
+
|
||||
+ // M2 starts P2
|
||||
+ final ActivityStarter starter = prepareStarter(0);
|
||||
+ doReturn(privilegedUid).when(mMockPackageManager).getPackageUid(
|
||||
+ eq(privilegedPackage), anyLong(), anyInt());
|
||||
+ doReturn(maliciousUid).when(mMockPackageManager).getPackageUid(
|
||||
+ eq(maliciousPackage), anyLong(), anyInt());
|
||||
+ starter.setCallingPackage(maliciousPackage);
|
||||
+ starter.setCallingUid(maliciousUid);
|
||||
+
|
||||
+ final Intent p2Intent = new Intent();
|
||||
+ p2Intent.setComponent(new ComponentName(privilegedPackage, "P2Activity"));
|
||||
+ p2Intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
|
||||
+
|
||||
+ final ActivityInfo p2ActivityInfo = new ActivityInfo();
|
||||
+ p2ActivityInfo.applicationInfo = new ApplicationInfo();
|
||||
+ p2ActivityInfo.applicationInfo.packageName = privilegedPackage;
|
||||
+ p2ActivityInfo.applicationInfo.uid = privilegedUid;
|
||||
+ p2ActivityInfo.name = "P2Activity";
|
||||
+
|
||||
+ final ActivityRecord[] outActivity = new ActivityRecord[1];
|
||||
+
|
||||
+ // The request simulates M2 starting P2
|
||||
+ starter.setIntent(p2Intent)
|
||||
+ .setActivityInfo(p2ActivityInfo)
|
||||
+ .setResultTo(m2.token) // sourceRecord is m2
|
||||
+ .setRequestCode(-1) // for startActivity()
|
||||
+ .setOutActivity(outActivity)
|
||||
+ .execute();
|
||||
+
|
||||
+ final ActivityRecord p2 = outActivity[0];
|
||||
+
|
||||
+ assertNotNull(p2);
|
||||
+ assertEquals("launchedFromPackage should be the immediate caller",
|
||||
+ maliciousPackage, p2.launchedFromPackage);
|
||||
+ assertEquals("launchedFromUid should be the immediate caller",
|
||||
+ maliciousUid, p2.launchedFromUid);
|
||||
+ }
|
||||
+
|
||||
private ActivityRecord createBubbledActivity() {
|
||||
final ActivityOptions opts = ActivityOptions.makeBasic();
|
||||
opts.setTaskAlwaysOnTop(true);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
From 09055276288a68cf35b0f84ba32e28822f74ecf9 Mon Sep 17 00:00:00 2001
|
||||
From: Sanjana Sunil <sanjanasunil@google.com>
|
||||
Date: Wed, 26 Nov 2025 14:40:09 +0000
|
||||
Subject: [PATCH] Explicitly unset INSTALL_FROM_MANAGED_USER_OR_PROFILE flag
|
||||
|
||||
If the flag is not explicitly unset, an app could set this flag, leading
|
||||
to unexpected behaviour if the install is not actually from a managed
|
||||
user or profile.
|
||||
|
||||
Bug: 459461121
|
||||
Test: atest PackageManagerShellCommandInstallTest#testSessionCreationWithManagedUserOrProfileFlag_notFromManagedProfile
|
||||
Flag: EXEMPT BUGFIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:990428772d4718853382ec4c5feda2b7bd6f923f
|
||||
Merged-In: I21bbbf628e97244d469eb23ce0558dbf560b7618
|
||||
Change-Id: I21bbbf628e97244d469eb23ce0558dbf560b7618
|
||||
---
|
||||
.../java/com/android/server/pm/PackageInstallerService.java | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
|
||||
index 2d0bb258e89f..a0ad5d1aaa30 100644
|
||||
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
|
||||
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
|
||||
@@ -1027,6 +1027,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
|
||||
}
|
||||
|
||||
final var dpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
|
||||
+ // Only the system should be able to set this flag - so ensure it is unset when not needed.
|
||||
+ params.installFlags &= ~PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE;
|
||||
if (dpmi != null && dpmi.isUserOrganizationManaged(userId)) {
|
||||
params.installFlags |= PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE;
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
From 014dea279c49d532bc4fbbdebbc024133967b6a8 Mon Sep 17 00:00:00 2001
|
||||
From: Julia Reynolds <juliacr@google.com>
|
||||
Date: Wed, 12 Nov 2025 12:09:31 -0500
|
||||
Subject: [PATCH] Be more strict about content types for message array
|
||||
|
||||
Now with fewer class cast exceptions
|
||||
|
||||
Test: ConversationNotification
|
||||
Test: NotificationManagerServiceTest
|
||||
Bug: 433746973
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit 71d4afae00c7d6d9238f8ec82303e1e13da50fbb)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:70fe31848f073029cdb38cd6c5fe47f200dd4c78
|
||||
Merged-In: I3022e010de95f14dcd0d09d123684ee265101e0a
|
||||
Change-Id: I3022e010de95f14dcd0d09d123684ee265101e0a
|
||||
---
|
||||
core/java/android/app/Notification.java | 18 ++--
|
||||
.../NotificationManagerServiceTest.java | 101 +++++++++++++++++-
|
||||
2 files changed, 107 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
|
||||
index 2ed95ab7ad82..b921280da563 100644
|
||||
--- a/core/java/android/app/Notification.java
|
||||
+++ b/core/java/android/app/Notification.java
|
||||
@@ -3152,8 +3152,8 @@ public class Notification implements Parcelable
|
||||
person.visitUris(visitor);
|
||||
}
|
||||
|
||||
- final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES,
|
||||
- Parcelable.class);
|
||||
+ final Bundle[] messages =
|
||||
+ getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class);
|
||||
if (!ArrayUtils.isEmpty(messages)) {
|
||||
for (MessagingStyle.Message message : MessagingStyle.Message
|
||||
.getMessagesFromBundleArray(messages)) {
|
||||
@@ -3161,8 +3161,8 @@ public class Notification implements Parcelable
|
||||
}
|
||||
}
|
||||
|
||||
- final Parcelable[] historic = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES,
|
||||
- Parcelable.class);
|
||||
+ final Parcelable[] historic =
|
||||
+ getParcelableArrayFromBundle(extras, EXTRA_HISTORIC_MESSAGES, Bundle.class);
|
||||
if (!ArrayUtils.isEmpty(historic)) {
|
||||
for (MessagingStyle.Message message : MessagingStyle.Message
|
||||
.getMessagesFromBundleArray(historic)) {
|
||||
@@ -8203,8 +8203,8 @@ public class Notification implements Parcelable
|
||||
*/
|
||||
public boolean hasImage() {
|
||||
if (isStyle(MessagingStyle.class) && extras != null) {
|
||||
- final Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES,
|
||||
- Parcelable.class);
|
||||
+ final Bundle[] messages =
|
||||
+ getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class);
|
||||
if (!ArrayUtils.isEmpty(messages)) {
|
||||
for (MessagingStyle.Message m : MessagingStyle.Message
|
||||
.getMessagesFromBundleArray(messages)) {
|
||||
@@ -9485,10 +9485,10 @@ public class Notification implements Parcelable
|
||||
mUser = user;
|
||||
}
|
||||
mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
|
||||
- Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES, Parcelable.class);
|
||||
+ Bundle[] messages = getParcelableArrayFromBundle(extras, EXTRA_MESSAGES, Bundle.class);
|
||||
mMessages = Message.getMessagesFromBundleArray(messages);
|
||||
- Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES,
|
||||
- Parcelable.class);
|
||||
+ Bundle[] histMessages = getParcelableArrayFromBundle(
|
||||
+ extras, EXTRA_HISTORIC_MESSAGES, Bundle.class);
|
||||
mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
|
||||
mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
|
||||
mUnreadMessageCount = extras.getInt(EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
|
||||
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
index 41cd746e7a04..883c6cc64fbe 100644
|
||||
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
@@ -27,6 +27,8 @@ import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS;
|
||||
import static android.app.Flags.FLAG_NM_SUMMARIZATION;
|
||||
import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME;
|
||||
import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP;
|
||||
+import static android.app.Notification.EXTRA_MESSAGES;
|
||||
+import static android.app.Notification.EXTRA_MESSAGING_PERSON;
|
||||
import static android.app.Notification.EXTRA_PICTURE;
|
||||
import static android.app.Notification.EXTRA_PICTURE_ICON;
|
||||
import static android.app.Notification.EXTRA_TEXT;
|
||||
@@ -81,6 +83,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BA
|
||||
import static android.app.PendingIntent.FLAG_IMMUTABLE;
|
||||
import static android.app.PendingIntent.FLAG_MUTABLE;
|
||||
import static android.app.PendingIntent.FLAG_ONE_SHOT;
|
||||
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
|
||||
import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED;
|
||||
import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG;
|
||||
@@ -235,11 +238,13 @@ import android.companion.ICompanionDeviceManager;
|
||||
import android.compat.testing.PlatformCompatChangeRule;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
+import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.IIntentSender;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
+import android.content.UriPermission;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.IPackageManager;
|
||||
@@ -255,6 +260,7 @@ import android.content.pm.VersionedPackage;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
+import android.graphics.Rect;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
@@ -1503,6 +1509,95 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
|
||||
captor.getValue().getIntent().getPackage());
|
||||
}
|
||||
|
||||
+ public void testNoUriGrantsForBadMessagesList() throws RemoteException {
|
||||
+ Uri targetUri = Uri.parse("content://com.android.contacts/display_photo/1");
|
||||
+
|
||||
+ // create message person
|
||||
+ Person person = new Person.Builder()
|
||||
+ .setName("Name")
|
||||
+ .setIcon(Icon.createWithContentUri(targetUri))
|
||||
+ .setKey("user_123")
|
||||
+ .setBot(false)
|
||||
+ .build();
|
||||
+
|
||||
+ // create MessagingStyle
|
||||
+ Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(person)
|
||||
+ .setConversationTitle("Bug discussion")
|
||||
+ .setGroupConversation(true)
|
||||
+ .addMessage("Hi,look my photo", System.currentTimeMillis() - 60000, person)
|
||||
+ .addMessage("Oho, you used my contacts photo",
|
||||
+ System.currentTimeMillis() - 30000, "Friend");
|
||||
+
|
||||
+ // create Notification
|
||||
+ Notification notification = new Notification.Builder(mContext, TEST_CHANNEL_ID)
|
||||
+ .setSmallIcon(R.drawable.sym_def_app_icon)
|
||||
+ .setContentTitle("")
|
||||
+ .setContentText("")
|
||||
+ .setAutoCancel(true)
|
||||
+ .setStyle(messagingStyle)
|
||||
+ .setCategory(Notification.CATEGORY_MESSAGE)
|
||||
+ .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
|
||||
+ .build();
|
||||
+ notification.contentIntent = createPendingIntent("open");
|
||||
+
|
||||
+ notification.extras.remove(EXTRA_MESSAGING_PERSON);
|
||||
+
|
||||
+ // add BadClipDescription to avoid visitUri check uris in EXTRA_MESSAGES value
|
||||
+ ArrayList<Parcelable> parcelableArray =
|
||||
+ new ArrayList<>(List.of(notification.extras.getParcelableArray(EXTRA_MESSAGES)));
|
||||
+ parcelableArray.add(new MyParceledListSlice());
|
||||
+ notification.extras.putParcelableArray(
|
||||
+ EXTRA_MESSAGES, parcelableArray.toArray(new Parcelable[0]));
|
||||
+ try {
|
||||
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg,
|
||||
+ "testNoUriGrantsForBadMessagesList",
|
||||
+ 1, notification, mContext.getUserId());
|
||||
+ waitForIdle();
|
||||
+ fail("should have failed to parse messages");
|
||||
+ } catch (java.lang.ArrayStoreException e) {
|
||||
+ verify(mUgmInternal, never()).checkGrantUriPermission(
|
||||
+ anyInt(), any(), eq(ContentProvider.getUriWithoutUserId(targetUri)),
|
||||
+ anyInt(), anyInt());
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private class MyParceledListSlice extends Intent {
|
||||
+ @Override
|
||||
+ public void writeToParcel(Parcel dest, int i) {
|
||||
+ Parcel test = Parcel.obtain();
|
||||
+ test.writeString(this.getClass().getName());
|
||||
+ int strLength = test.dataSize();
|
||||
+ test.recycle();
|
||||
+ dest.setDataPosition(dest.dataPosition() - strLength);
|
||||
+ dest.writeString("android.content.pm.ParceledListSlice");
|
||||
+
|
||||
+ dest.writeInt(1);
|
||||
+ dest.writeString(UriPermission.class.getName());
|
||||
+ dest.writeInt(0); // use binder
|
||||
+ dest.writeStrongBinder(new Binder() {
|
||||
+ private int callingPid = -1;
|
||||
+ @Override
|
||||
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
|
||||
+ throws RemoteException {
|
||||
+ if (code == 1) {
|
||||
+ reply.writeNoException();
|
||||
+ reply.writeInt(1);
|
||||
+ if (getCallingUid() == 1000 && callingPid == -1) {
|
||||
+ reply.writeParcelable(new Rect(), 0);
|
||||
+ callingPid = getCallingPid();
|
||||
+ } else {
|
||||
+ reply.writeInt(-1);
|
||||
+ reply.writeInt(-1);
|
||||
+ reply.writeLong(0);
|
||||
+ }
|
||||
+ return true;
|
||||
+ }
|
||||
+ return super.onTransact(code, data, reply, flags);
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@Test
|
||||
public void testDefaultAssistant_overrideDefault() {
|
||||
final int userId = mContext.getUserId();
|
||||
@@ -8086,7 +8181,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(Notification.EXTRA_AUDIO_CONTENTS_URI, audioContents);
|
||||
extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, backgroundImage.toString());
|
||||
- extras.putParcelable(Notification.EXTRA_MESSAGING_PERSON, person1);
|
||||
+ extras.putParcelable(EXTRA_MESSAGING_PERSON, person1);
|
||||
extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST,
|
||||
new ArrayList<>(Arrays.asList(person2, person3)));
|
||||
extras.putParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS,
|
||||
@@ -8224,13 +8319,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
|
||||
.setSmallIcon(android.R.drawable.sym_def_app_icon);
|
||||
|
||||
Bundle messagingExtras = new Bundle();
|
||||
- messagingExtras.putParcelable(Notification.EXTRA_MESSAGING_PERSON,
|
||||
+ messagingExtras.putParcelable(EXTRA_MESSAGING_PERSON,
|
||||
personWithIcon("content://user"));
|
||||
messagingExtras.putParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES,
|
||||
new Bundle[] { new Notification.MessagingStyle.Message("Heyhey!",
|
||||
System.currentTimeMillis() - 100,
|
||||
personWithIcon("content://historicalMessenger")).toBundle()});
|
||||
- messagingExtras.putParcelableArray(Notification.EXTRA_MESSAGES,
|
||||
+ messagingExtras.putParcelableArray(EXTRA_MESSAGES,
|
||||
new Bundle[] { new Notification.MessagingStyle.Message("Are you there?",
|
||||
System.currentTimeMillis(),
|
||||
personWithIcon("content://messenger")).toBundle()});
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
From 924df83d73d9f938fde025c2e793ca12646207e0 Mon Sep 17 00:00:00 2001
|
||||
From: Evan Chen <evanxinchen@google.com>
|
||||
Date: Tue, 18 Nov 2025 22:34:11 +0000
|
||||
Subject: [PATCH] Remove any revoked associations after reboot
|
||||
|
||||
Test: manually
|
||||
Bug: 442392902
|
||||
Flag: EXEMPT bugfix
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:13714bcfaff6ef1c16d0aa3d359b1c8bc1859ac3
|
||||
Merged-In: I94b96d98608d6702e1d3a9581e135280149bf7e1
|
||||
Change-Id: I94b96d98608d6702e1d3a9581e135280149bf7e1
|
||||
---
|
||||
.../server/companion/CompanionDeviceManagerService.java | 6 ++++++
|
||||
1 file changed, 6 insertions(+)
|
||||
|
||||
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
|
||||
index 7ff1ddb7dc79..fdefbc412136 100644
|
||||
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
|
||||
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
|
||||
@@ -33,6 +33,7 @@ import static com.android.internal.util.CollectionUtils.any;
|
||||
import static com.android.internal.util.Preconditions.checkState;
|
||||
import static com.android.server.companion.association.DisassociationProcessor.REASON_API;
|
||||
import static com.android.server.companion.association.DisassociationProcessor.REASON_PKG_DATA_CLEARED;
|
||||
+import static com.android.server.companion.association.DisassociationProcessor.REASON_REVOKED;
|
||||
import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
|
||||
import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
|
||||
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
|
||||
@@ -197,6 +198,11 @@ public class CompanionDeviceManagerService extends SystemService {
|
||||
// Init association stores
|
||||
mAssociationStore.refreshCache();
|
||||
|
||||
+ // Remove any revoked associations after reboot.
|
||||
+ for (AssociationInfo ai : mAssociationStore.getRevokedAssociations()) {
|
||||
+ mDisassociationProcessor.disassociate(ai.getId(), REASON_REVOKED);
|
||||
+ }
|
||||
+
|
||||
// Init UUID store
|
||||
mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
From e363f82104566378b4b9936d6caf27c3ee631d80 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= <matiashe@google.com>
|
||||
Date: Wed, 20 Aug 2025 15:57:28 +0200
|
||||
Subject: [PATCH] Limit the number of services (NLSes, etc) that can be
|
||||
approved per user
|
||||
|
||||
Trying to activate additional packages/components will be silently rejected.
|
||||
|
||||
Bug: 428701593
|
||||
Test: atest ManagedServicesTest NotificationManagerServiceTest
|
||||
Flag: com.android.server.notification.limit_managed_services_count
|
||||
(cherry picked from commit a132684a093d9e1750100b39d4e4168f2d27d349)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:3b41dfeec7ebfcd313ff26b7f27b7e7971af4497
|
||||
Merged-In: Iddd8044997c41f97369b768f4da5e49efc43ad06
|
||||
Change-Id: Iddd8044997c41f97369b768f4da5e49efc43ad06
|
||||
---
|
||||
.../server/notification/ManagedServices.java | 59 ++++++++++++-----
|
||||
.../NotificationManagerService.java | 34 +++++++---
|
||||
.../notification/ManagedServicesTest.java | 63 +++++++++++++++++++
|
||||
.../NotificationManagerServiceTest.java | 27 ++++++++
|
||||
4 files changed, 160 insertions(+), 23 deletions(-)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
|
||||
index 62e26e189a35..159d33999739 100644
|
||||
--- a/services/core/java/com/android/server/notification/ManagedServices.java
|
||||
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
|
||||
@@ -133,6 +133,13 @@ abstract public class ManagedServices {
|
||||
static final int APPROVAL_BY_PACKAGE = 0;
|
||||
static final int APPROVAL_BY_COMPONENT = 1;
|
||||
|
||||
+ /**
|
||||
+ * Maximum number of entries allowed in the lists of packages/components contained in
|
||||
+ * {@link #mApproved} or {@link #mUserSetServices}. For the first, this effectively limits
|
||||
+ * the number of services (e.g. NLSes) that will be bound per user.
|
||||
+ */
|
||||
+ private static final int MAX_SERVICE_ENTRIES = 100;
|
||||
+
|
||||
protected final Context mContext;
|
||||
protected final Object mMutex;
|
||||
private final UserProfiles mUserProfiles;
|
||||
@@ -915,16 +922,22 @@ abstract public class ManagedServices {
|
||||
}
|
||||
}
|
||||
|
||||
- protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
+ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
boolean isPrimary, boolean enabled) {
|
||||
- setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, true);
|
||||
+ return setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, true);
|
||||
}
|
||||
|
||||
- protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
+ /**
|
||||
+ * Changes the enabled state of a managed service.
|
||||
+ *
|
||||
+ * @return true if the change (enabling or disabling) was applied; false otherwise
|
||||
+ */
|
||||
+ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
boolean isPrimary, boolean enabled, boolean userSet) {
|
||||
Slog.i(TAG,
|
||||
(enabled ? " Allowing " : "Disallowing ") + mConfig.caption + " "
|
||||
+ pkgOrComponent + " (userSet: " + userSet + ")");
|
||||
+ boolean changed = false;
|
||||
synchronized (mApproved) {
|
||||
ArrayMap<Boolean, ArraySet<String>> allowedByType = mApproved.get(userId);
|
||||
if (allowedByType == null) {
|
||||
@@ -940,24 +953,42 @@ abstract public class ManagedServices {
|
||||
|
||||
if (approvedItem != null) {
|
||||
if (enabled) {
|
||||
- approved.add(approvedItem);
|
||||
+ if (approved.size() < MAX_SERVICE_ENTRIES) {
|
||||
+ approved.add(approvedItem);
|
||||
+ changed = true;
|
||||
+ } else {
|
||||
+ Slog.w(TAG, TextUtils.formatSimple(
|
||||
+ "Failed to allow %s %s because there are too many already",
|
||||
+ mConfig.caption, pkgOrComponent));
|
||||
+ }
|
||||
} else {
|
||||
approved.remove(approvedItem);
|
||||
+ changed = true;
|
||||
}
|
||||
}
|
||||
- ArraySet<String> userSetServices = mUserSetServices.get(userId);
|
||||
- if (userSetServices == null) {
|
||||
- userSetServices = new ArraySet<>();
|
||||
- mUserSetServices.put(userId, userSetServices);
|
||||
- }
|
||||
- if (userSet) {
|
||||
- userSetServices.add(pkgOrComponent);
|
||||
- } else {
|
||||
- userSetServices.remove(pkgOrComponent);
|
||||
+
|
||||
+ if (changed) {
|
||||
+ ArraySet<String> userSetServices = mUserSetServices.get(userId);
|
||||
+ if (userSetServices == null) {
|
||||
+ userSetServices = new ArraySet<>();
|
||||
+ mUserSetServices.put(userId, userSetServices);
|
||||
+ }
|
||||
+ if (userSet) {
|
||||
+ if (userSetServices.size() < MAX_SERVICE_ENTRIES) {
|
||||
+ userSetServices.add(pkgOrComponent);
|
||||
+ }
|
||||
+ } else {
|
||||
+ userSetServices.remove(pkgOrComponent);
|
||||
+ }
|
||||
+
|
||||
}
|
||||
}
|
||||
|
||||
- rebindServices(false, userId);
|
||||
+ if (changed) {
|
||||
+ rebindServices(false, userId);
|
||||
+ }
|
||||
+
|
||||
+ return changed;
|
||||
}
|
||||
|
||||
private String getApprovedValue(String pkgOrComponent) {
|
||||
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
|
||||
index d7913baf02cd..da76c82e76fc 100644
|
||||
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
|
||||
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
|
||||
@@ -6692,8 +6692,11 @@ public class NotificationManagerService extends SystemService {
|
||||
try {
|
||||
if (mAllowedManagedServicePackages.test(
|
||||
pkg, userId, mConditionProviders.getRequiredPermission())) {
|
||||
- mConditionProviders.setPackageOrComponentEnabled(
|
||||
- pkg, userId, true, granted);
|
||||
+ boolean changed = mConditionProviders.setPackageOrComponentEnabled(pkg, userId,
|
||||
+ /* isPrimary= */ true, granted);
|
||||
+ if (!changed) {
|
||||
+ return;
|
||||
+ }
|
||||
|
||||
getContext().sendBroadcastAsUser(new Intent(
|
||||
ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED)
|
||||
@@ -6963,10 +6966,15 @@ public class NotificationManagerService extends SystemService {
|
||||
try {
|
||||
if (mAllowedManagedServicePackages.test(
|
||||
listener.getPackageName(), userId, mListeners.getRequiredPermission())) {
|
||||
+ boolean changed = mListeners.setPackageOrComponentEnabled(
|
||||
+ listener.flattenToString(), userId, /* isPrimary= */ true, granted,
|
||||
+ userSet);
|
||||
+ if (!changed) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
mConditionProviders.setPackageOrComponentEnabled(listener.flattenToString(),
|
||||
userId, false, granted, userSet);
|
||||
- mListeners.setPackageOrComponentEnabled(listener.flattenToString(),
|
||||
- userId, true, granted, userSet);
|
||||
|
||||
getContext().sendBroadcastAsUser(new Intent(
|
||||
ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED)
|
||||
@@ -12698,19 +12706,20 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
|
||||
@Override
|
||||
- protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
+ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
boolean isPrimary, boolean enabled, boolean userSet) {
|
||||
// Ensures that only one component is enabled at a time
|
||||
if (enabled) {
|
||||
List<ComponentName> allowedComponents = getAllowedComponents(userId);
|
||||
if (!allowedComponents.isEmpty()) {
|
||||
ComponentName currentComponent = CollectionUtils.firstOrNull(allowedComponents);
|
||||
- if (currentComponent.flattenToString().equals(pkgOrComponent)) return;
|
||||
+ if (currentComponent.flattenToString().equals(pkgOrComponent)) return false;
|
||||
setNotificationAssistantAccessGrantedForUserInternal(
|
||||
currentComponent, userId, false, userSet);
|
||||
}
|
||||
}
|
||||
- super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
|
||||
+ return super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled,
|
||||
+ userSet);
|
||||
}
|
||||
|
||||
private boolean isVerboseLogEnabled() {
|
||||
@@ -13057,9 +13066,14 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
|
||||
@Override
|
||||
- protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
+ protected boolean setPackageOrComponentEnabled(String pkgOrComponent, int userId,
|
||||
boolean isPrimary, boolean enabled, boolean userSet) {
|
||||
- super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
|
||||
+ boolean changed = super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary,
|
||||
+ enabled, userSet);
|
||||
+ if (!changed) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
String pkgName = getPackageName(pkgOrComponent);
|
||||
if (redactSensitiveNotificationsFromUntrustedListeners()) {
|
||||
int uid = mPackageManagerInternal.getPackageUid(pkgName, 0, userId);
|
||||
@@ -13079,6 +13093,8 @@ public class NotificationManagerService extends SystemService {
|
||||
new Intent(ACTION_NOTIFICATION_LISTENER_ENABLED_CHANGED)
|
||||
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
|
||||
UserHandle.of(userId), null);
|
||||
+
|
||||
+ return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
|
||||
index 9c85b04fc4ff..cff652c642ee 100644
|
||||
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
|
||||
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
|
||||
@@ -33,6 +33,7 @@ import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAG
|
||||
import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
+import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
@@ -2534,6 +2535,68 @@ public class ManagedServicesTest extends UiServiceTestCase {
|
||||
assertThat(listener.enabledAndUserMatches(visibleBackgroundUserId)).isFalse();
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ public void setPackageOrComponentEnabled_tooManyPackages_stopsAdding() {
|
||||
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
|
||||
+ mIpm, APPROVAL_BY_PACKAGE);
|
||||
+ int userId = 0;
|
||||
+
|
||||
+ for (int i = 1; i <= 100; i++) {
|
||||
+ assertWithMessage("Trying pkg" + i)
|
||||
+ .that(service.setPackageOrComponentEnabled("pkg" + i, userId, true, true))
|
||||
+ .isTrue();
|
||||
+ assertThat(service.isPackageAllowed("pkg" + i, userId)).isTrue();
|
||||
+ }
|
||||
+
|
||||
+ // And finally, monsieur, a wafer-thin mint.
|
||||
+ assertThat(service.setPackageOrComponentEnabled("toomany", userId, true, true)).isFalse();
|
||||
+ assertThat(service.isPackageAllowed("toomany", userId)).isFalse();
|
||||
+
|
||||
+ // We can still DISABLE packages though.
|
||||
+ assertThat(service.isPackageAllowed("pkg33", userId)).isTrue();
|
||||
+ assertThat(service.setPackageOrComponentEnabled("pkg33", userId, true, false)).isTrue();
|
||||
+ assertThat(service.isPackageAllowed("pkg33", userId)).isFalse();
|
||||
+
|
||||
+ // And that allows adding new ones.
|
||||
+ assertThat(service.setPackageOrComponentEnabled("onemore", userId, true, true)).isTrue();
|
||||
+ assertThat(service.isPackageAllowed("onemore", userId)).isTrue();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void setPackageOrComponentEnabled_tooManyChanges_stopsAddingToUserSet() {
|
||||
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
|
||||
+ mIpm, APPROVAL_BY_PACKAGE);
|
||||
+ int userId = 0;
|
||||
+
|
||||
+ for (int i = 1; i <= 100; i++) {
|
||||
+ assertWithMessage("Enabling pkg" + i)
|
||||
+ .that(service.setPackageOrComponentEnabled("pkg" + i, userId, true, true))
|
||||
+ .isTrue();
|
||||
+ assertWithMessage("Disabling pkg" + i)
|
||||
+ .that(service.setPackageOrComponentEnabled("pkg" + i, userId, true, false))
|
||||
+ .isTrue();
|
||||
+ assertThat(service.isPackageAllowed("pkg" + i, userId)).isFalse();
|
||||
+ assertThat(service.isPackageOrComponentUserSet("pkg" + i, userId)).isTrue();
|
||||
+ }
|
||||
+
|
||||
+ // Too many disabled services.
|
||||
+ assertThat(service.setPackageOrComponentEnabled("toomany", userId, true, true)).isTrue();
|
||||
+ assertThat(service.isPackageAllowed("toomany", userId)).isTrue();
|
||||
+ assertThat(service.isPackageOrComponentUserSet("toomany", userId)).isFalse();
|
||||
+ assertThat(service.setPackageOrComponentEnabled("toomany", userId, true, false)).isTrue();
|
||||
+ assertThat(service.isPackageAllowed("toomany", userId)).isFalse();
|
||||
+ assertThat(service.isPackageOrComponentUserSet("toomany", userId)).isFalse();
|
||||
+
|
||||
+ // We make space only when packages are uninstalled.
|
||||
+ service.onPackagesChanged(/* removingPackage= */ true, new String[] { "pkg22" },
|
||||
+ new int[] { 22 });
|
||||
+
|
||||
+ // And that allows tracking new ones.
|
||||
+ assertThat(service.setPackageOrComponentEnabled("onemore", userId, true, true)).isTrue();
|
||||
+ assertThat(service.setPackageOrComponentEnabled("onemore", userId, true, false)).isTrue();
|
||||
+ assertThat(service.isPackageOrComponentUserSet("onemore", userId)).isTrue();
|
||||
+ }
|
||||
+
|
||||
private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
|
||||
ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
|
||||
throws RemoteException {
|
||||
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
index 883c6cc64fbe..3eb35decad73 100644
|
||||
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
|
||||
@@ -713,6 +713,18 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
|
||||
mPolicyFile.finishWrite(fos);
|
||||
|
||||
// Setup managed services
|
||||
+ when(mListeners.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(), anyBoolean()))
|
||||
+ .thenReturn(true);
|
||||
+ when(mListeners.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(), anyBoolean(),
|
||||
+ anyBoolean())).thenReturn(true);
|
||||
+ when(mAssistants.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(), anyBoolean()))
|
||||
+ .thenReturn(true);
|
||||
+ when(mAssistants.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(), anyBoolean(),
|
||||
+ anyBoolean())).thenReturn(true);
|
||||
+ when(mConditionProviders.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(),
|
||||
+ anyBoolean())).thenReturn(true);
|
||||
+ when(mConditionProviders.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(),
|
||||
+ anyBoolean(), anyBoolean())).thenReturn(true);
|
||||
when(mNlf.isTypeAllowed(anyInt())).thenReturn(true);
|
||||
when(mNlf.isPackageAllowed(any())).thenReturn(true);
|
||||
when(mNlf.isPackageAllowed(null)).thenReturn(true);
|
||||
@@ -6110,6 +6122,21 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
|
||||
c.flattenToString(), user.getIdentifier(), true, /* enabled= */ false, true);
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ public void testSetListenerAccessForUser_tooManyListeners_skipsFollowups() throws Exception {
|
||||
+ UserHandle user = UserHandle.of(mContext.getUserId() + 10);
|
||||
+ ComponentName c = ComponentName.unflattenFromString("package/Component");
|
||||
+ when(mListeners.setPackageOrComponentEnabled(any(), anyInt(), anyBoolean(), anyBoolean(),
|
||||
+ anyBoolean())).thenReturn(false);
|
||||
+
|
||||
+ mBinderService.setNotificationListenerAccessGrantedForUser(
|
||||
+ c, user.getIdentifier(), /* enabled= */ true, true);
|
||||
+
|
||||
+ verify(mConditionProviders, never()).setPackageOrComponentEnabled(any(), anyInt(),
|
||||
+ anyBoolean(), anyBoolean(), anyBoolean());
|
||||
+ verify(mContext, never()).sendBroadcastAsUser(any(), any(), any());
|
||||
+ }
|
||||
+
|
||||
@Test
|
||||
public void testSetAssistantAccessForUser() throws Exception {
|
||||
UserInfo ui = new UserInfo();
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
From 51368785bd09cd652ed9851727b16545cb92c4e5 Mon Sep 17 00:00:00 2001
|
||||
From: Song Chun Fan <schfan@google.com>
|
||||
Date: Tue, 11 Nov 2025 00:03:48 +0000
|
||||
Subject: [PATCH] [UidMigration] fix update uninstallation with
|
||||
sharedUserMaxSdkVersion
|
||||
|
||||
When a system app is re-enabled after the update is uninstalled, when
|
||||
the system app has shared uid, the current code doesn't support
|
||||
directly reusing the disabled package setting. Instead, the preloaded
|
||||
version is re-scanned and installed as a new app, which can bring
|
||||
breaking behavior of changed UIDs when the manifest has
|
||||
sharedUserMaxSdkVersion.
|
||||
|
||||
This change fixes the bug where registerExistingAppId fails when it
|
||||
comes to shared uid, therefore directly reuses the disabled package
|
||||
setting, consistent with the behavior for non-shared-uid system apps.
|
||||
|
||||
FLAG: EXEMPT BUGFIX
|
||||
Test: manually with system-app-test.sh
|
||||
BUG: 454062218
|
||||
|
||||
|
||||
|
||||
(cherry picked from commit 6b5ea2f7fbf50313d46e54e0d8f8c18c398e4869)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:740256b41ba113708655f82dc5664291bf79edd0
|
||||
Merged-In: I417cec27697a210416027e862a5e5d207d268b82
|
||||
Change-Id: I417cec27697a210416027e862a5e5d207d268b82
|
||||
---
|
||||
.../core/java/com/android/server/pm/Settings.java | 14 +++++++++-----
|
||||
.../src/com/android/server/pm/MockSystem.kt | 2 +-
|
||||
2 files changed, 10 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
|
||||
index 92257f1ee2dd..ab13d7e766e8 100644
|
||||
--- a/services/core/java/com/android/server/pm/Settings.java
|
||||
+++ b/services/core/java/com/android/server/pm/Settings.java
|
||||
@@ -916,8 +916,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
|
||||
p.getPkgState().setUpdatedSystemApp(false);
|
||||
final AndroidPackageInternal pkg = p.getPkg();
|
||||
PackageSetting ret = addPackageLPw(name, p.getRealName(), p.getPath(), p.getAppId(),
|
||||
- p.getFlags(), p.getPrivateFlags(), mDomainVerificationManager.generateNewId(),
|
||||
- pkg == null ? false : pkg.isSdkLibrary());
|
||||
+ p.getFlags(), p.getPrivateFlags(), mDomainVerificationManager.generateNewId(),
|
||||
+ pkg == null ? false : pkg.isSdkLibrary(), p.hasSharedUser());
|
||||
if (ret != null) {
|
||||
ret.setLegacyNativeLibraryPath(p.getLegacyNativeLibraryPath());
|
||||
ret.setPrimaryCpuAbi(p.getPrimaryCpuAbiLegacy());
|
||||
@@ -937,6 +937,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
|
||||
ret.setRestrictUpdateHash(p.getRestrictUpdateHash());
|
||||
ret.setScannedAsStoppedSystemApp(p.isScannedAsStoppedSystemApp());
|
||||
ret.setInstallSource(p.getInstallSource());
|
||||
+ ret.setSharedUserAppId(p.getSharedUserAppId());
|
||||
}
|
||||
mDisabledSysPackages.remove(name);
|
||||
return ret;
|
||||
@@ -958,7 +959,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
|
||||
}
|
||||
|
||||
PackageSetting addPackageLPw(String name, String realName, File codePath, int uid,
|
||||
- int pkgFlags, int pkgPrivateFlags, @NonNull UUID domainSetId, boolean isSdkLibrary) {
|
||||
+ int pkgFlags, int pkgPrivateFlags, @NonNull UUID domainSetId, boolean isSdkLibrary,
|
||||
+ boolean hasSharedUser) {
|
||||
PackageSetting p = mPackages.get(name);
|
||||
if (p != null) {
|
||||
if (p.getAppId() == uid) {
|
||||
@@ -971,7 +973,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
|
||||
p = new PackageSetting(name, realName, codePath, pkgFlags, pkgPrivateFlags, domainSetId)
|
||||
.setAppId(uid);
|
||||
if ((uid == Process.INVALID_UID && isSdkLibrary && Flags.disallowSdkLibsToBeApps())
|
||||
- || mAppIds.registerExistingAppId(uid, p, name)) {
|
||||
+ || mAppIds.registerExistingAppId(uid, p, name)
|
||||
+ || hasSharedUser) {
|
||||
mPackages.put(name, p);
|
||||
return p;
|
||||
}
|
||||
@@ -4266,7 +4269,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
|
||||
} else if (appId > 0 || (appId == Process.INVALID_UID && isSdkLibrary
|
||||
&& Flags.disallowSdkLibsToBeApps())) {
|
||||
packageSetting = addPackageLPw(name.intern(), realName, new File(codePathStr),
|
||||
- appId, pkgFlags, pkgPrivateFlags, domainSetId, isSdkLibrary);
|
||||
+ appId, pkgFlags, pkgPrivateFlags, domainSetId, isSdkLibrary,
|
||||
+ /* hasSharedUser= */ false);
|
||||
if (PackageManagerService.DEBUG_SETTINGS)
|
||||
Log.i(PackageManagerService.TAG, "Reading package " + name + ": appId="
|
||||
+ appId + " pkg=" + packageSetting);
|
||||
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
|
||||
index 1b2ab2702d49..9a73ba3c155e 100644
|
||||
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
|
||||
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
|
||||
@@ -168,7 +168,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) {
|
||||
null
|
||||
}
|
||||
whenever(mocks.settings.addPackageLPw(nullable(), nullable(), nullable(), nullable(),
|
||||
- nullable(), nullable(), nullable(), nullable())) {
|
||||
+ nullable(), nullable(), nullable(), nullable(), nullable())) {
|
||||
val name: String = getArgument(0)
|
||||
val pendingAdd = mPendingPackageAdds.firstOrNull { it.first == name }
|
||||
?: return@whenever null
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
From c81cf361489e3a3cd764c0a0c85c84958e25d63c Mon Sep 17 00:00:00 2001
|
||||
From: Alec Mouri <alecmouri@google.com>
|
||||
Date: Wed, 5 Nov 2025 21:29:10 +0000
|
||||
Subject: [PATCH] Clip to layer bounds when drawing blur regions
|
||||
|
||||
Otherwise blurs can "escape" layer bounds. This would make 1-2
|
||||
pixel-large layers fill the entire screen if the blur region is
|
||||
appropriately crafted, which is not great.
|
||||
|
||||
Bug: 455563813
|
||||
Flag: EXEMPT CVE_FIX
|
||||
Test: PoC app
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:123f8fec995a3103acbc3a1191b9cef71523e013
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:f3fecb02978030ae4066235cbe638250996b6a9a
|
||||
Merged-In: If59833f2d5060f5f81395d602e2dcb369a10fdbb
|
||||
Change-Id: If59833f2d5060f5f81395d602e2dcb369a10fdbb
|
||||
---
|
||||
libs/renderengine/skia/SkiaRenderEngine.cpp | 4 ++++
|
||||
1 file changed, 4 insertions(+)
|
||||
|
||||
diff --git a/libs/renderengine/skia/SkiaRenderEngine.cpp b/libs/renderengine/skia/SkiaRenderEngine.cpp
|
||||
index 5b6edb4e30..46c58b0c27 100644
|
||||
--- a/libs/renderengine/skia/SkiaRenderEngine.cpp
|
||||
+++ b/libs/renderengine/skia/SkiaRenderEngine.cpp
|
||||
@@ -907,6 +907,10 @@ void SkiaRenderEngine::drawLayersInternal(
|
||||
SkAutoCanvasRestore acr(canvas, true);
|
||||
if (!roundRectClip.isEmpty()) {
|
||||
canvas->clipRRect(roundRectClip, true);
|
||||
+ } else {
|
||||
+ // We need to clip bounds here since otherwise a client sending a bigger blur region
|
||||
+ // enables the blur to "escape" the layer bounds which is very bad for security
|
||||
+ canvas->clipRRect(bounds, true);
|
||||
}
|
||||
|
||||
// TODO(b/182216890): Filter out empty layers earlier
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
From c6da9eeb710c6690d189cb2d1b80b44755860b55 Mon Sep 17 00:00:00 2001
|
||||
From: Kyle Hsiao <kylehsiao@google.com>
|
||||
Date: Wed, 1 Oct 2025 11:57:54 +0000
|
||||
Subject: [PATCH] [NFC] Fix use-after-free in eventCallback
|
||||
|
||||
Transform access to the static shared pointer 'Nfc::mCallback' to be thread-safe to prevent a use-after-free crash.
|
||||
|
||||
The use-after-free occurred when an asynchronous thread (eventCallback) attempted to call a method on 'mCallback' immediately after the main thread Nfc::open had destroyed the underlying object. The simple null check was insufficient due to the race condition.
|
||||
|
||||
This fix implements the correct shared pointer synchronization pattern:
|
||||
1. Protects the read/write of 'mCallback' using 'mCallbackLock'.
|
||||
2. Creates a local 'std::shared_ptr<INfcClientCallback> localCallback' inside the critical section. This local copy holds a temporary strong reference, guaranteeing the object's lifetime for the duration of the subsequent sendEvent() call.
|
||||
|
||||
Bug: 392699284
|
||||
Test: nfc_service_fuzzer
|
||||
Test: atest NfcNciUnitTests
|
||||
Test: atest CtsNfcTestCases
|
||||
Test: atest VtsAidlHalNfcTargetTest
|
||||
Test: atest NfcTestCases
|
||||
Test: atest NfcServiceTest
|
||||
(cherry picked from commit fc619c3348188ff0020cdeb7d4c4728a0246fe1a)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:33b3c128e0f480f6e421761b85c5f00289b62449
|
||||
Merged-In: I8e9e83be7f939bbfc183cb3879808ceacf931450
|
||||
Change-Id: I8e9e83be7f939bbfc183cb3879808ceacf931450
|
||||
---
|
||||
aidl/Nfc.h | 18 ++++++++++++++----
|
||||
1 file changed, 14 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/aidl/Nfc.h b/aidl/Nfc.h
|
||||
index 707bd65..fd23e24 100644
|
||||
--- a/aidl/Nfc.h
|
||||
+++ b/aidl/Nfc.h
|
||||
@@ -32,6 +32,8 @@ using ::aidl::android::hardware::nfc::NfcConfig;
|
||||
using ::aidl::android::hardware::nfc::NfcEvent;
|
||||
using ::aidl::android::hardware::nfc::NfcStatus;
|
||||
|
||||
+static pthread_mutex_t sCallbackLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
+
|
||||
// Default implementation that reports no support NFC.
|
||||
struct Nfc : public BnNfc {
|
||||
public:
|
||||
@@ -52,7 +54,11 @@ struct Nfc : public BnNfc {
|
||||
binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
|
||||
|
||||
static void eventCallback(uint8_t event, uint8_t status) {
|
||||
- if (mCallback != nullptr) {
|
||||
+ std::shared_ptr<INfcClientCallback> localCallback;
|
||||
+ pthread_mutex_lock(&sCallbackLock);
|
||||
+ localCallback = mCallback;
|
||||
+ pthread_mutex_unlock(&sCallbackLock);
|
||||
+ if (localCallback != nullptr) {
|
||||
NfcEvent mEvent;
|
||||
NfcStatus mStatus;
|
||||
switch (event) {
|
||||
@@ -96,7 +102,7 @@ struct Nfc : public BnNfc {
|
||||
default:
|
||||
mStatus = NfcStatus::FAILED;
|
||||
}
|
||||
- auto ret = mCallback->sendEvent(mEvent, mStatus);
|
||||
+ auto ret = localCallback->sendEvent(mEvent, mStatus);
|
||||
if (!ret.isOk()) {
|
||||
LOG(ERROR) << "Failed to send event!";
|
||||
}
|
||||
@@ -104,9 +110,13 @@ struct Nfc : public BnNfc {
|
||||
}
|
||||
|
||||
static void dataCallback(uint16_t data_len, uint8_t* p_data) {
|
||||
+ std::shared_ptr<INfcClientCallback> localCallback;
|
||||
+ pthread_mutex_lock(&sCallbackLock);
|
||||
+ localCallback = mCallback;
|
||||
+ pthread_mutex_unlock(&sCallbackLock);
|
||||
std::vector<uint8_t> data(p_data, p_data + data_len);
|
||||
- if (mCallback != nullptr) {
|
||||
- auto ret = mCallback->sendData(data);
|
||||
+ if (localCallback != nullptr) {
|
||||
+ auto ret = localCallback->sendData(data);
|
||||
if (!ret.isOk()) {
|
||||
LOG(ERROR) << "Failed to send data!";
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
From 48af8a13dd12ecbd0569c328a56d1a7b61a59ca3 Mon Sep 17 00:00:00 2001
|
||||
From: Mill Chen <millchen@google.com>
|
||||
Date: Fri, 26 Sep 2025 09:02:26 +0000
|
||||
Subject: [PATCH] Check permission of the calling package in multi-pane devices
|
||||
|
||||
Bug: 430047417
|
||||
Test: manual test
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit bd4d57ade07792f2a9160acbe480603b30e79917)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:2860bd01810adb2d0f00fba8f327cdae3f20ab9d
|
||||
Merged-In: I91dafa77d07970fdf2628b4d9e89ca1c4b74194c
|
||||
Change-Id: I91dafa77d07970fdf2628b4d9e89ca1c4b74194c
|
||||
---
|
||||
AndroidManifest.xml | 2 +-
|
||||
.../settings/applications/AppInfoBase.java | 20 +++++++++++++++++++
|
||||
2 files changed, 21 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
|
||||
index ed19890456f..e2d249add13 100644
|
||||
--- a/AndroidManifest.xml
|
||||
+++ b/AndroidManifest.xml
|
||||
@@ -2488,7 +2488,7 @@
|
||||
android:name="Settings$AppUsageAccessSettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/usage_access_title">
|
||||
- <intent-filter>
|
||||
+ <intent-filter android:priority="1">
|
||||
<action android:name="android.settings.USAGE_ACCESS_SETTINGS"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="package"/>
|
||||
diff --git a/src/com/android/settings/applications/AppInfoBase.java b/src/com/android/settings/applications/AppInfoBase.java
|
||||
index 02237b886d9..14e54eb5b03 100644
|
||||
--- a/src/com/android/settings/applications/AppInfoBase.java
|
||||
+++ b/src/com/android/settings/applications/AppInfoBase.java
|
||||
@@ -49,6 +49,7 @@ import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
+import com.android.settings.activityembedding.ActivityEmbeddingUtils;
|
||||
import com.android.settings.applications.manageapplications.ManageApplications;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
@@ -178,6 +179,25 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment
|
||||
if (!(activity instanceof SettingsActivity)) {
|
||||
return false;
|
||||
}
|
||||
+ // Check the permission of the calling package if the device supports multi-pane.
|
||||
+ if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(activity)) {
|
||||
+ final String callingPackageName =
|
||||
+ ((SettingsActivity) activity).getInitialCallingPackage();
|
||||
+
|
||||
+ if (TextUtils.isEmpty(callingPackageName)) {
|
||||
+ Log.w(TAG, "Not able to get calling package name for permission check");
|
||||
+ return false;
|
||||
+ }
|
||||
+ if (mPm.checkPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL,
|
||||
+ callingPackageName)
|
||||
+ != PackageManager.PERMISSION_GRANTED) {
|
||||
+ Log.w(TAG, "Package " + callingPackageName + " does not have required permission "
|
||||
+ + Manifest.permission.INTERACT_ACROSS_USERS_FULL);
|
||||
+ return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
try {
|
||||
int callerUid = ActivityManager.getService().getLaunchedFromUid(
|
||||
activity.getActivityToken());
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,600 @@
|
|||
From 7d8fbee887fc9577337c2a80513ae4399bf60111 Mon Sep 17 00:00:00 2001
|
||||
From: Shawn Lin <shawnlin@google.com>
|
||||
Date: Tue, 14 Oct 2025 08:08:58 +0000
|
||||
Subject: [PATCH] Fixed "Unlock your phone" unexpectedlly turned ON after OTA
|
||||
|
||||
If the new settings key is not set, we should use the values of the old
|
||||
keys as default value.
|
||||
|
||||
Bug: 444673089
|
||||
Test: atest FaceSettingsAppsPreferenceControllerTest
|
||||
FaceSettingsKeyguardUnlockPreferenceControllerTest
|
||||
FingerprintSettingsAppsPreferenceControllerTest
|
||||
FingerprintSettingsKeyguardUnlockPreferenceControllerTest
|
||||
Flag: EXEMPT BUGFIX
|
||||
(cherry picked from commit 05f0884146a093e6311d7a30232d6850a28368ef)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:feb931006ee2d56c146156d5bb1491117841ccf3
|
||||
Merged-In: I345defc78500c244e29e8595f5fbc705b95f4ba6
|
||||
Change-Id: I345defc78500c244e29e8595f5fbc705b95f4ba6
|
||||
---
|
||||
.../FaceSettingsAppsPreferenceController.java | 17 ++-
|
||||
...ngsKeyguardUnlockPreferenceController.java | 13 ++
|
||||
...printSettingsAppsPreferenceController.java | 17 ++-
|
||||
...ngsKeyguardUnlockPreferenceController.java | 13 ++
|
||||
...eSettingsAppsPreferenceControllerTest.java | 116 ++++++++++++++++++
|
||||
...eyguardUnlockPreferenceControllerTest.java | 90 ++++++++++++++
|
||||
...tSettingsAppsPreferenceControllerTest.java | 76 ++++++++++++
|
||||
...eyguardUnlockPreferenceControllerTest.java | 90 ++++++++++++++
|
||||
8 files changed, 428 insertions(+), 4 deletions(-)
|
||||
create mode 100644 tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceControllerTest.java
|
||||
create mode 100644 tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
create mode 100644 tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceControllerTest.java
|
||||
create mode 100644 tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
|
||||
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceController.java
|
||||
index 9500c25a8ec..78d9ffaf7c4 100644
|
||||
--- a/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceController.java
|
||||
+++ b/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceController.java
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.biometrics.face;
|
||||
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED;
|
||||
import static android.provider.Settings.Secure.FACE_APP_ENABLED;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
@@ -31,6 +32,7 @@ import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
|
||||
|
||||
public class FaceSettingsAppsPreferenceController extends
|
||||
FaceSettingsPreferenceController {
|
||||
+ private static final int NOT_SET = -1;
|
||||
private static final int ON = 1;
|
||||
private static final int OFF = 0;
|
||||
private static final int DEFAULT = ON;
|
||||
@@ -40,12 +42,23 @@ public class FaceSettingsAppsPreferenceController extends
|
||||
public FaceSettingsAppsPreferenceController(@NonNull Context context, @NonNull String key) {
|
||||
super(context, key);
|
||||
mFaceManager = Utils.getFaceManagerOrNull(context);
|
||||
+
|
||||
+ // For OTA case: if FACE_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is set, set the
|
||||
+ // default value of the former to that of the latter.
|
||||
+ final int defValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, NOT_SET, getUserId());
|
||||
+ final int oldDefValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, NOT_SET, getUserId());
|
||||
+ if (defValue == NOT_SET && oldDefValue != NOT_SET) {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, oldDefValue, getUserId());
|
||||
+ }
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
- return Settings.Secure.getIntForUser(mContext.getContentResolver(), FACE_APP_ENABLED,
|
||||
- DEFAULT, getUserId()) == ON;
|
||||
+ return Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, DEFAULT, getUserId()) == ON;
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceController.java
|
||||
index 5ff15232a02..46731b94451 100644
|
||||
--- a/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceController.java
|
||||
+++ b/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceController.java
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.biometrics.face;
|
||||
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED;
|
||||
import static android.provider.Settings.Secure.FACE_KEYGUARD_ENABLED;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
@@ -31,6 +32,7 @@ import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
|
||||
|
||||
public class FaceSettingsKeyguardUnlockPreferenceController extends
|
||||
FaceSettingsPreferenceController {
|
||||
+ private static final int NOT_SET = -1;
|
||||
private static final int ON = 1;
|
||||
private static final int OFF = 0;
|
||||
private static final int DEFAULT = ON;
|
||||
@@ -41,6 +43,17 @@ public class FaceSettingsKeyguardUnlockPreferenceController extends
|
||||
@NonNull Context context, @NonNull String key) {
|
||||
super(context, key);
|
||||
mFaceManager = Utils.getFaceManagerOrNull(context);
|
||||
+
|
||||
+ // For OTA case: if FACE_KEYGUARD_ENABLED is not set and BIOMETRIC_KEYGUARD_ENABLED is set,
|
||||
+ // set the default value of the former to that of the latter.
|
||||
+ final int defValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_KEYGUARD_ENABLED, NOT_SET, getUserId());
|
||||
+ final int oldDefValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, NOT_SET, getUserId());
|
||||
+ if (defValue == NOT_SET && oldDefValue != NOT_SET) {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_KEYGUARD_ENABLED, oldDefValue, getUserId());
|
||||
+ }
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java
|
||||
index 63fc3dcef23..574d406e382 100644
|
||||
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java
|
||||
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceController.java
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.biometrics.fingerprint;
|
||||
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED;
|
||||
import static android.provider.Settings.Secure.FINGERPRINT_APP_ENABLED;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
@@ -31,6 +32,7 @@ import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
|
||||
|
||||
public class FingerprintSettingsAppsPreferenceController
|
||||
extends FingerprintSettingsPreferenceController {
|
||||
+ private static final int NOT_SET = -1;
|
||||
private static final int ON = 1;
|
||||
private static final int OFF = 0;
|
||||
private static final int DEFAULT = ON;
|
||||
@@ -41,12 +43,23 @@ public class FingerprintSettingsAppsPreferenceController
|
||||
@NonNull Context context, @NonNull String key) {
|
||||
super(context, key);
|
||||
mFingerprintManager = Utils.getFingerprintManagerOrNull(context);
|
||||
+
|
||||
+ // For OTA case: if FINGERPRINT_APP_ENABLED is not set and BIOMETRIC_APP_ENABLED is set,
|
||||
+ // set the default value of the former to that of the latter.
|
||||
+ final int defValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, NOT_SET, getUserId());
|
||||
+ final int oldDefValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, NOT_SET, getUserId());
|
||||
+ if (defValue == NOT_SET && oldDefValue != NOT_SET) {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, oldDefValue, getUserId());
|
||||
+ }
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
- return Settings.Secure.getIntForUser(mContext.getContentResolver(), FINGERPRINT_APP_ENABLED,
|
||||
- DEFAULT, getUserId()) == ON;
|
||||
+ return Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, DEFAULT, getUserId()) == ON;
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java
|
||||
index 56ef2c3db8b..89bea9b5fde 100644
|
||||
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java
|
||||
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceController.java
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.biometrics.fingerprint;
|
||||
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED;
|
||||
import static android.provider.Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
@@ -32,6 +33,7 @@ import com.android.settings.biometrics.activeunlock.ActiveUnlockStatusUtils;
|
||||
public class FingerprintSettingsKeyguardUnlockPreferenceController
|
||||
extends FingerprintSettingsPreferenceController {
|
||||
|
||||
+ private static final int NOT_SET = -1;
|
||||
private static final int ON = 1;
|
||||
private static final int OFF = 0;
|
||||
private static final int DEFAULT = ON;
|
||||
@@ -42,6 +44,17 @@ public class FingerprintSettingsKeyguardUnlockPreferenceController
|
||||
@NonNull Context context, @NonNull String key) {
|
||||
super(context, key);
|
||||
mFingerprintManager = Utils.getFingerprintManagerOrNull(context);
|
||||
+
|
||||
+ // For OTA case: if FINGERPRINT_KEYGUARD_ENABLED is not set and BIOMETRIC_KEYGUARD_ENABLED
|
||||
+ // is set, set the default value of the former to that of the latter.
|
||||
+ final int defValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_KEYGUARD_ENABLED, NOT_SET, getUserId());
|
||||
+ final int oldDefValue = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, NOT_SET, getUserId());
|
||||
+ if (defValue == NOT_SET && oldDefValue != NOT_SET) {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_KEYGUARD_ENABLED, oldDefValue, getUserId());
|
||||
+ }
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceControllerTest.java
|
||||
new file mode 100644
|
||||
index 00000000000..676031c4951
|
||||
--- /dev/null
|
||||
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsAppsPreferenceControllerTest.java
|
||||
@@ -0,0 +1,116 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.settings.biometrics.face;
|
||||
+
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FACE_APP_ENABLED;
|
||||
+
|
||||
+import static com.google.common.truth.Truth.assertThat;
|
||||
+
|
||||
+import static org.mockito.Mockito.when;
|
||||
+
|
||||
+import android.content.Context;
|
||||
+import android.hardware.biometrics.ComponentInfoInternal;
|
||||
+import android.hardware.biometrics.SensorProperties;
|
||||
+import android.hardware.face.FaceManager;
|
||||
+import android.hardware.face.FaceSensorProperties;
|
||||
+import android.hardware.face.FaceSensorPropertiesInternal;
|
||||
+import android.provider.Settings;
|
||||
+
|
||||
+import androidx.test.core.app.ApplicationProvider;
|
||||
+
|
||||
+import com.android.settings.testutils.FakeFeatureFactory;
|
||||
+import com.android.settings.testutils.shadow.ShadowSecureSettings;
|
||||
+import com.android.settings.testutils.shadow.ShadowUtils;
|
||||
+
|
||||
+import org.junit.After;
|
||||
+import org.junit.Before;
|
||||
+import org.junit.Rule;
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+import org.mockito.Mock;
|
||||
+import org.mockito.Spy;
|
||||
+import org.mockito.junit.MockitoJUnit;
|
||||
+import org.mockito.junit.MockitoRule;
|
||||
+import org.robolectric.RobolectricTestRunner;
|
||||
+import org.robolectric.annotation.Config;
|
||||
+
|
||||
+import java.util.ArrayList;
|
||||
+
|
||||
+@RunWith(RobolectricTestRunner.class)
|
||||
+@Config(shadows = {ShadowSecureSettings.class})
|
||||
+public class FaceSettingsAppsPreferenceControllerTest {
|
||||
+ @Rule
|
||||
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
+ @Spy
|
||||
+ private Context mContext = ApplicationProvider.getApplicationContext();
|
||||
+ @Mock
|
||||
+ private FaceManager mFaceManager;
|
||||
+ private FaceSettingsAppsPreferenceController mController;
|
||||
+
|
||||
+ private FaceSensorPropertiesInternal mConvenienceSensorProperty =
|
||||
+ new FaceSensorPropertiesInternal(
|
||||
+ 0 /* sensorId */,
|
||||
+ SensorProperties.STRENGTH_CONVENIENCE,
|
||||
+ 1 /* maxEnrollmentsPerUser */,
|
||||
+ new ArrayList<ComponentInfoInternal>(),
|
||||
+ FaceSensorProperties.TYPE_UNKNOWN,
|
||||
+ true /* supportsFaceDetection */,
|
||||
+ true /* supportsSelfIllumination */,
|
||||
+ true /* resetLockoutRequiresChallenge */);
|
||||
+ private FakeFeatureFactory mFeatureFactory;
|
||||
+
|
||||
+ @Before
|
||||
+ public void setUp() {
|
||||
+ when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(mFaceManager);
|
||||
+ final ArrayList<FaceSensorPropertiesInternal> list = new ArrayList<>();
|
||||
+ list.add(mConvenienceSensorProperty);
|
||||
+ when(mFaceManager.getSensorPropertiesInternal()).thenReturn(list);
|
||||
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
+ }
|
||||
+
|
||||
+ @After
|
||||
+ public void tearDown() {
|
||||
+ ShadowUtils.reset();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricAppEnableOff_FaceAppEnabledNotSet_returnFalse() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, -1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FaceSettingsAppsPreferenceController(
|
||||
+ mContext, "biometric_settings_face_app");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isFalse();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricAppEnableOff_FaceAppEnabledOn_returnTrue() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_APP_ENABLED, 1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FaceSettingsAppsPreferenceController(
|
||||
+ mContext, "biometric_settings_face_app");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isTrue();
|
||||
+ }
|
||||
+}
|
||||
diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
new file mode 100644
|
||||
index 00000000000..837f31e3bd6
|
||||
--- /dev/null
|
||||
+++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
@@ -0,0 +1,90 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.settings.biometrics.face;
|
||||
+
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FACE_KEYGUARD_ENABLED;
|
||||
+
|
||||
+import static com.google.common.truth.Truth.assertThat;
|
||||
+
|
||||
+import static org.mockito.Mockito.when;
|
||||
+
|
||||
+import android.content.Context;
|
||||
+import android.os.UserManager;
|
||||
+import android.provider.Settings;
|
||||
+
|
||||
+import androidx.test.core.app.ApplicationProvider;
|
||||
+
|
||||
+import com.android.settings.testutils.FakeFeatureFactory;
|
||||
+import com.android.settings.testutils.shadow.ShadowSecureSettings;
|
||||
+
|
||||
+import org.junit.Before;
|
||||
+import org.junit.Rule;
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+import org.mockito.Mock;
|
||||
+import org.mockito.Spy;
|
||||
+import org.mockito.junit.MockitoJUnit;
|
||||
+import org.mockito.junit.MockitoRule;
|
||||
+import org.robolectric.RobolectricTestRunner;
|
||||
+import org.robolectric.annotation.Config;
|
||||
+
|
||||
+@RunWith(RobolectricTestRunner.class)
|
||||
+@Config(shadows = {ShadowSecureSettings.class})
|
||||
+public class FaceSettingsKeyguardUnlockPreferenceControllerTest {
|
||||
+ @Rule
|
||||
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
+ @Spy
|
||||
+ Context mContext = ApplicationProvider.getApplicationContext();
|
||||
+ private FaceSettingsKeyguardUnlockPreferenceController mController;
|
||||
+ private FakeFeatureFactory mFeatureFactory;
|
||||
+ @Mock
|
||||
+ private UserManager mUserManager;
|
||||
+
|
||||
+ @Before
|
||||
+ public void setUp() {
|
||||
+
|
||||
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricKeyguardEnabledOff_FaceKeyguardEnabledNotSet_returnFalse() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_KEYGUARD_ENABLED, -1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FaceSettingsKeyguardUnlockPreferenceController(
|
||||
+ mContext, "biometric_settings_face_keyguard");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isFalse();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricKeyguardEnabledOff_FaceKeyguardEnabledOn_returnTrue() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FACE_KEYGUARD_ENABLED, 1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FaceSettingsKeyguardUnlockPreferenceController(
|
||||
+ mContext, "biometric_settings_face_keyguard");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isTrue();
|
||||
+ }
|
||||
+}
|
||||
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceControllerTest.java
|
||||
new file mode 100644
|
||||
index 00000000000..841cec2f3de
|
||||
--- /dev/null
|
||||
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsAppsPreferenceControllerTest.java
|
||||
@@ -0,0 +1,76 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.settings.biometrics.fingerprint;
|
||||
+
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_APP_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FINGERPRINT_APP_ENABLED;
|
||||
+
|
||||
+import static com.google.common.truth.Truth.assertThat;
|
||||
+
|
||||
+import android.content.Context;
|
||||
+import android.provider.Settings;
|
||||
+
|
||||
+import androidx.test.core.app.ApplicationProvider;
|
||||
+
|
||||
+import com.android.settings.testutils.FakeFeatureFactory;
|
||||
+import com.android.settings.testutils.shadow.ShadowSecureSettings;
|
||||
+
|
||||
+import org.junit.Before;
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+import org.robolectric.RobolectricTestRunner;
|
||||
+import org.robolectric.annotation.Config;
|
||||
+
|
||||
+@RunWith(RobolectricTestRunner.class)
|
||||
+@Config(shadows = {ShadowSecureSettings.class})
|
||||
+public class FingerprintSettingsAppsPreferenceControllerTest {
|
||||
+ private Context mContext;
|
||||
+ private FingerprintSettingsAppsPreferenceController mController;
|
||||
+ private FakeFeatureFactory mFeatureFactory;
|
||||
+
|
||||
+ @Before
|
||||
+ public void setUp() {
|
||||
+ mContext = ApplicationProvider.getApplicationContext();
|
||||
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricAppEnableOff_FingerprintAppEnabledNotSet_returnFalse() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, -1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FingerprintSettingsAppsPreferenceController(
|
||||
+ mContext, "biometric_settings_fingerprint_app");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isFalse();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricAppEnableOff_FingerprintAppEnabledOn_returnTrue() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_APP_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_APP_ENABLED, 1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FingerprintSettingsAppsPreferenceController(
|
||||
+ mContext, "biometric_settings_fingerprint_app");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isTrue();
|
||||
+ }
|
||||
+}
|
||||
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
new file mode 100644
|
||||
index 00000000000..119fda8bbf2
|
||||
--- /dev/null
|
||||
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsKeyguardUnlockPreferenceControllerTest.java
|
||||
@@ -0,0 +1,90 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2025 The Android Open Source Project
|
||||
+ *
|
||||
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
+ * you may not use this file except in compliance with the License.
|
||||
+ * You may obtain a copy of the License at
|
||||
+ *
|
||||
+ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
+ *
|
||||
+ * Unless required by applicable law or agreed to in writing, software
|
||||
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
+ * See the License for the specific language governing permissions and
|
||||
+ * limitations under the License.
|
||||
+ */
|
||||
+
|
||||
+package com.android.settings.biometrics.fingerprint;
|
||||
+
|
||||
+import static android.provider.Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED;
|
||||
+import static android.provider.Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED;
|
||||
+
|
||||
+import static com.google.common.truth.Truth.assertThat;
|
||||
+
|
||||
+import static org.mockito.Mockito.when;
|
||||
+
|
||||
+import android.content.Context;
|
||||
+import android.os.UserManager;
|
||||
+import android.provider.Settings;
|
||||
+
|
||||
+import androidx.test.core.app.ApplicationProvider;
|
||||
+
|
||||
+import com.android.settings.testutils.FakeFeatureFactory;
|
||||
+import com.android.settings.testutils.shadow.ShadowSecureSettings;
|
||||
+
|
||||
+import org.junit.Before;
|
||||
+import org.junit.Rule;
|
||||
+import org.junit.Test;
|
||||
+import org.junit.runner.RunWith;
|
||||
+import org.mockito.Mock;
|
||||
+import org.mockito.Spy;
|
||||
+import org.mockito.junit.MockitoJUnit;
|
||||
+import org.mockito.junit.MockitoRule;
|
||||
+import org.robolectric.RobolectricTestRunner;
|
||||
+import org.robolectric.annotation.Config;
|
||||
+
|
||||
+@RunWith(RobolectricTestRunner.class)
|
||||
+@Config(shadows = {ShadowSecureSettings.class})
|
||||
+public class FingerprintSettingsKeyguardUnlockPreferenceControllerTest {
|
||||
+ @Rule
|
||||
+ public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
+ @Spy
|
||||
+ Context mContext = ApplicationProvider.getApplicationContext();
|
||||
+ private FingerprintSettingsKeyguardUnlockPreferenceController mController;
|
||||
+ private FakeFeatureFactory mFeatureFactory;
|
||||
+ @Mock
|
||||
+ private UserManager mUserManager;
|
||||
+
|
||||
+ @Before
|
||||
+ public void setUp() {
|
||||
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void
|
||||
+ isChecked_BiometricKeyguardEnabledOff_FingerprintKeyguardEnabledNotSet_returnFalse() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_KEYGUARD_ENABLED, -1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FingerprintSettingsKeyguardUnlockPreferenceController(
|
||||
+ mContext, "biometric_settings_fingerprint_keyguard");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isFalse();
|
||||
+ }
|
||||
+
|
||||
+ @Test
|
||||
+ public void isChecked_BiometricKeyguardEnabledOff_FingerprintKeyguardEnabledOn_returnTrue() {
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ BIOMETRIC_KEYGUARD_ENABLED, 0, mContext.getUserId());
|
||||
+ Settings.Secure.putIntForUser(mContext.getContentResolver(),
|
||||
+ FINGERPRINT_KEYGUARD_ENABLED, 1, mContext.getUserId());
|
||||
+
|
||||
+ mController = new FingerprintSettingsKeyguardUnlockPreferenceController(
|
||||
+ mContext, "biometric_settings_fingerprint_keyguard");
|
||||
+
|
||||
+ assertThat(mController.isChecked()).isTrue();
|
||||
+ }
|
||||
+}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
From f0271f36388ec9630d89ff8b3ee4cb22e2ca3eaf Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= <ptosi@google.com>
|
||||
Date: Tue, 28 Oct 2025 09:46:06 +0000
|
||||
Subject: [PATCH] vmbase,pvmfw: aarch64: Clean dcache to PoC not PoU
|
||||
|
||||
Some SoCs (with a unified cache before the PoC) might not flush the data
|
||||
to main memory when performing CMOs to PoU so perform them to PoC.
|
||||
|
||||
Test: b/434562039#comment35
|
||||
Bug: 434562039
|
||||
Bug: 455777515
|
||||
Flag: EXEMPT CVE_FIX
|
||||
(cherry picked from commit a6f64040dba5a3aae3f67c68e2a754a5c946610d)
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:ab5f74693d42e114af5ac238cca0bcb4b17698ac
|
||||
Merged-In: Id2398e9bcf8dcf7f7a10d254d8eb411d39e109db
|
||||
Change-Id: Id2398e9bcf8dcf7f7a10d254d8eb411d39e109db
|
||||
---
|
||||
guest/pvmfw/src/arch/aarch64/payload.rs | 6 +++---
|
||||
libs/libvmbase/src/arch.rs | 2 +-
|
||||
2 files changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/guest/pvmfw/src/arch/aarch64/payload.rs b/guest/pvmfw/src/arch/aarch64/payload.rs
|
||||
index 77e9a31..8c2242e 100644
|
||||
--- a/guest/pvmfw/src/arch/aarch64/payload.rs
|
||||
+++ b/guest/pvmfw/src/arch/aarch64/payload.rs
|
||||
@@ -91,7 +91,7 @@ pub fn jump_to_payload(entrypoint: usize, slices: &MemorySlices) -> ! {
|
||||
"b.lo 0b",
|
||||
|
||||
// Flush d-cache over .data & .bss (including skipped region).
|
||||
- "0: dc cvau, {cache_line}",
|
||||
+ "0: dc cvac, {cache_line}",
|
||||
"add {cache_line}, {cache_line}, {dcache_line_size}",
|
||||
"cmp {cache_line}, {scratch_end}",
|
||||
"b.lo 0b",
|
||||
@@ -103,7 +103,7 @@ pub fn jump_to_payload(entrypoint: usize, slices: &MemorySlices) -> ! {
|
||||
"b.lo 0b",
|
||||
|
||||
// Flush d-cache over stack region.
|
||||
- "0: dc cvau, {cache_line}",
|
||||
+ "0: dc cvac, {cache_line}",
|
||||
"add {cache_line}, {cache_line}, {dcache_line_size}",
|
||||
"cmp {cache_line}, {stack_end}",
|
||||
"b.lo 0b",
|
||||
@@ -115,7 +115,7 @@ pub fn jump_to_payload(entrypoint: usize, slices: &MemorySlices) -> ! {
|
||||
"b.lo 0b",
|
||||
|
||||
// Flush d-cache over EH stack region.
|
||||
- "0: dc cvau, {cache_line}",
|
||||
+ "0: dc cvac, {cache_line}",
|
||||
"add {cache_line}, {cache_line}, {dcache_line_size}",
|
||||
"cmp {cache_line}, {eh_stack_end}",
|
||||
"b.lo 0b",
|
||||
diff --git a/libs/libvmbase/src/arch.rs b/libs/libvmbase/src/arch.rs
|
||||
index 29d3a32..e25745b 100644
|
||||
--- a/libs/libvmbase/src/arch.rs
|
||||
+++ b/libs/libvmbase/src/arch.rs
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn flush_region(start: usize, size: usize) {
|
||||
let end = start + size;
|
||||
let start = crate::util::unchecked_align_down(start, line_size);
|
||||
for line in (start..end).step_by(line_size) {
|
||||
- crate::dc!("cvau", line);
|
||||
+ crate::dc!("cvac", line);
|
||||
}
|
||||
} else {
|
||||
compile_error!("Unsupported target_arch")
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
From 69a25763cdb46c8f23fe9eb976132acbe2af82d6 Mon Sep 17 00:00:00 2001
|
||||
From: Himanshu Arora <hmarora@google.com>
|
||||
Date: Fri, 24 Oct 2025 16:47:37 +0000
|
||||
Subject: [PATCH] Fix ACCESS_MEDIA_LOCATION bypass via SAF picker
|
||||
|
||||
When an app uses a picker to access media files, the picker should respect the app's permissions. Currently, it is possible to bypass the ACCESS_MEDIA_LOCATION permission and get unredacted location data.
|
||||
|
||||
This change fixes this by having MediaProvider check the permissions of the app on whose behalf the media is being opened. When a `mediaCapabilitiesUid` is passed, MediaProvider now checks if that UID has the necessary permissions.
|
||||
|
||||
Bug: 326211886
|
||||
Test: manual
|
||||
Flag: EXEMPT BUGFIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:e5e47f93838e1e9a3a3a520f7c89229fc041a8c3
|
||||
Merged-In: I7ad51535d5ae8a3803162f688c4794edbbcfb167
|
||||
Change-Id: I7ad51535d5ae8a3803162f688c4794edbbcfb167
|
||||
---
|
||||
.../providers/media/MediaProvider.java | 20 +++++++++++++++++--
|
||||
1 file changed, 18 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
|
||||
index 0d75628c5..4a09b7500 100644
|
||||
--- a/src/com/android/providers/media/MediaProvider.java
|
||||
+++ b/src/com/android/providers/media/MediaProvider.java
|
||||
@@ -10253,7 +10253,7 @@ public class MediaProvider extends ContentProvider {
|
||||
|
||||
// Figure out if we need to redact contents
|
||||
final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri,
|
||||
- ownerPackageName, file);
|
||||
+ ownerPackageName, file, opts);
|
||||
long[] redactionRanges;
|
||||
try {
|
||||
redactionRanges = redactionNeeded ? RedactionUtils.getRedactionRanges(file)
|
||||
@@ -10372,12 +10372,28 @@ public class MediaProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri,
|
||||
- String ownerPackageName, File file) {
|
||||
+ String ownerPackageName, File file, Bundle opts) {
|
||||
// Redacted Uris should always redact information
|
||||
if (redactedUri != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+ // If the caller provides a media capabilities UID, we check if that UID has the
|
||||
+ // PERMISSION_IS_REDACTION_NEEDED permission. If so, we redact the data. This is
|
||||
+ // used for cases where an app is acting on behalf of another app, and we need
|
||||
+ // to respect the capabilities of the app for which the action is being performed.
|
||||
+ if (opts != null) {
|
||||
+ final int mediaCapabilitiesUid = opts.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID);
|
||||
+ if (mediaCapabilitiesUid > 0) {
|
||||
+ final LocalCallingIdentity identity = LocalCallingIdentity.fromExternal(
|
||||
+ getContext(),
|
||||
+ mUserCache, mediaCapabilitiesUid, null, null);
|
||||
+ if (identity.hasPermission(PERMISSION_IS_REDACTION_NEEDED)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
|
||||
if (callerIsOwner) {
|
||||
return false;
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
From 119013a3d7e8f1eab671bce4c6a85748752081ed Mon Sep 17 00:00:00 2001
|
||||
From: Garvita Jain <garvitajain@google.com>
|
||||
Date: Mon, 1 Dec 2025 11:02:37 +0000
|
||||
Subject: [PATCH] Throw exception on MediaStore createRequest for non-existent
|
||||
uri
|
||||
|
||||
Throw IllegalArgument exception if a caller requests
|
||||
CREATE_WRITE/DELETE/.._REQUEST for a uri that does not exist in the
|
||||
Files table at the time of request.
|
||||
|
||||
BUG: 418773439
|
||||
Test: atest MediaProviderTests
|
||||
Flag: EXEMPT bugfix
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:268fc3fb0a438abbc710687a9590cb80b3c0e8bc
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:973f11ad05506303f6bbb3fd6275c3a2b824b2e8
|
||||
Merged-In: I00a3b15d8dbcd19afaec3980f7d7a07122bef640
|
||||
Change-Id: I00a3b15d8dbcd19afaec3980f7d7a07122bef640
|
||||
---
|
||||
.../android/providers/media/MediaProvider.java | 16 ++++++++++++++++
|
||||
.../providers/media/MediaProviderTest.java | 18 ++++++++++++++++--
|
||||
2 files changed, 32 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
|
||||
index 4a09b7500..b9fc95667 100644
|
||||
--- a/src/com/android/providers/media/MediaProvider.java
|
||||
+++ b/src/com/android/providers/media/MediaProvider.java
|
||||
@@ -8383,6 +8383,22 @@ public class MediaProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
+ // Do not allow to create request if the list contains a uri which does not exist
|
||||
+ final LocalCallingIdentity token = clearLocalCallingIdentity();
|
||||
+ try {
|
||||
+ for (Uri uri : uris) {
|
||||
+ try (Cursor c = queryForSingleItem(uri, new String[]{FileColumns._ID}, null, null,
|
||||
+ null)) {
|
||||
+ // queryForSingleItem method throws FileNotFoundException if no items were
|
||||
+ // found, or multiple items were found, or there was trouble reading the data.
|
||||
+ } catch (FileNotFoundException e) {
|
||||
+ throw new IllegalArgumentException("Invalid Uri: " + uri, e);
|
||||
+ }
|
||||
+ }
|
||||
+ } finally {
|
||||
+ restoreLocalCallingIdentity(token);
|
||||
+ }
|
||||
+
|
||||
final Context context = getContext();
|
||||
final Intent intent = new Intent(method, null, context, PermissionActivity.class);
|
||||
extras.putInt(EXTRA_CALLING_PACKAGE_UID, getCallingUidOrSelf());
|
||||
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
|
||||
index 21cb56e48..660276ce3 100644
|
||||
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
|
||||
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
|
||||
@@ -39,6 +39,7 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
+import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentInterface;
|
||||
@@ -334,11 +335,24 @@ public class MediaProviderTest {
|
||||
*/
|
||||
@Test
|
||||
public void testCreateRequest() throws Exception {
|
||||
- final Collection<Uri> uris = Arrays.asList(
|
||||
- MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42));
|
||||
+ final ContentValues values = new ContentValues();
|
||||
+ values.put(MediaColumns.DISPLAY_NAME, "test.mp3");
|
||||
+ values.put(MediaColumns.MIME_TYPE, "audio/mpeg");
|
||||
+ final Uri uri = sIsolatedResolver.insert(
|
||||
+ MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
|
||||
+ assumeTrue(uri != null);
|
||||
+ final Collection<Uri> uris = List.of(uri);
|
||||
assertNotNull(MediaStore.createWriteRequest(sIsolatedResolver, uris));
|
||||
}
|
||||
|
||||
+ @Test
|
||||
+ public void testCreateRequest_invalidUri_throwsException() throws Exception {
|
||||
+ final Collection<Uri> uris = List.of(
|
||||
+ MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42));
|
||||
+ assertThrows(IllegalArgumentException.class,
|
||||
+ () -> MediaStore.createWriteRequest(sIsolatedResolver, uris));
|
||||
+ }
|
||||
+
|
||||
@Test
|
||||
public void testRequestThumbnail_noAccess_throwsSecurityException() throws Exception {
|
||||
final File dir = Environment
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
From 80a267ed1ac714acff455e85ae28c1732777d5b6 Mon Sep 17 00:00:00 2001
|
||||
From: John Reck <jreck@google.com>
|
||||
Date: Thu, 13 Nov 2025 16:02:23 -0500
|
||||
Subject: [PATCH] Handle underflows in dng_opcode_MapTable
|
||||
|
||||
Bug: 456471290
|
||||
Test: sample image in bug && atest ImageDecoderTest
|
||||
Flag: EXEMPT BUGFIX
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:90c04eb8818273d4df0773ec38cafceba504b151
|
||||
Cherrypick-From: https://googleplex-android-review.googlesource.com/q/commit:c40bd5325d326d5cc4f6a5944e0047542361dd58
|
||||
Merged-In: Icf182db2288952f189e8f6a6baf9cfb0eff4a5e9
|
||||
Change-Id: Icf182db2288952f189e8f6a6baf9cfb0eff4a5e9
|
||||
---
|
||||
source/dng_misc_opcodes.cpp | 16 +++++++++++-----
|
||||
1 file changed, 11 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/source/dng_misc_opcodes.cpp b/source/dng_misc_opcodes.cpp
|
||||
index 38ee103..49e63a8 100644
|
||||
--- a/source/dng_misc_opcodes.cpp
|
||||
+++ b/source/dng_misc_opcodes.cpp
|
||||
@@ -254,6 +254,7 @@ dng_rect dng_area_spec::Overlap (const dng_rect &tile) const
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
+DNG_ATTRIB_NO_SANITIZE("unsigned-integer-overflow")
|
||||
dng_rect dng_area_spec::ScaledOverlap (const dng_rect &tile) const
|
||||
{
|
||||
|
||||
@@ -560,12 +561,17 @@ void dng_opcode_MapTable::ProcessArea (dng_negative & /* negative */,
|
||||
|
||||
const uint16 *table = fBlackAdjustedTable.Get () ? fBlackAdjustedTable->Buffer_uint16 ()
|
||||
: fTable ->Buffer_uint16 ();
|
||||
-
|
||||
- for (uint32 plane = fAreaSpec.Plane ();
|
||||
- plane < fAreaSpec.Plane () + fAreaSpec.Planes () &&
|
||||
- plane < buffer.Planes ();
|
||||
- plane++)
|
||||
+// BEGIN GOOGLE MODIFICATION
|
||||
+ const uint32 planeStart = fAreaSpec.Plane ();
|
||||
+ const uint32 planeCount = fAreaSpec.Planes ();
|
||||
+ const uint32 bufferPlanes = buffer.Planes ();
|
||||
+
|
||||
+ for (uint32 plane = planeStart;
|
||||
+ plane < bufferPlanes &&
|
||||
+ plane - planeStart < planeCount;
|
||||
+ ++plane)
|
||||
{
|
||||
+// END GOOGLE MODIFICATION
|
||||
|
||||
DoMapArea16 (buffer.DirtyPixel_uint16 (overlap.t, overlap.l, plane),
|
||||
1,
|
||||
--
|
||||
2.53.0
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +1,3 @@
|
|||
requests>=2.28.0
|
||||
beautifulsoup4>=4.11.0
|
||||
tqdm>=4.65.0
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Scrape Android Security Bulletins for the past year."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from tqdm import tqdm
|
||||
|
||||
from utils import parse_android_version
|
||||
from utils.logging_config import logger, setup_logging
|
||||
|
||||
|
||||
def get_bulletin_links():
|
||||
def get_bulletin_links() -> List[str]:
|
||||
"""Get all monthly bulletin links from the ASB overview page."""
|
||||
url = "https://source.android.com/docs/security/bulletin/asb-overview"
|
||||
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
|
|
@ -22,9 +28,7 @@ def get_bulletin_links():
|
|||
|
||||
for a in soup.find_all("a", href=True):
|
||||
href = a["href"]
|
||||
# Match bulletins from 2020 onwards (ASB started in 2020)
|
||||
if "/docs/security/bulletin/202" in href and "-01" in href:
|
||||
# Match both /YYYY-MM-DD and /YYYY/YYYY-MM-DD formats
|
||||
if ".html" not in href and "?hl=" not in href:
|
||||
link_url = f"https://source.android.com{href.split('?')[0]}"
|
||||
links.add(link_url)
|
||||
|
|
@ -32,7 +36,7 @@ def get_bulletin_links():
|
|||
return sorted(links)
|
||||
|
||||
|
||||
def is_kernel_section(section_id, section_text):
|
||||
def is_kernel_section(section_id: Optional[str], section_text: Optional[str]) -> bool:
|
||||
"""Check if a section is for kernel vulnerabilities."""
|
||||
if not section_id and not section_text:
|
||||
return False
|
||||
|
|
@ -40,7 +44,7 @@ def is_kernel_section(section_id, section_text):
|
|||
return "kernel" in section_lower
|
||||
|
||||
|
||||
def is_vendor_section(section_id, section_text):
|
||||
def is_vendor_section(section_id: Optional[str], section_text: Optional[str]) -> bool:
|
||||
"""Check if a section is for vendor-specific vulnerabilities."""
|
||||
if not section_id and not section_text:
|
||||
return False
|
||||
|
|
@ -56,7 +60,7 @@ def is_vendor_section(section_id, section_text):
|
|||
return any(indicator in section_lower for indicator in vendor_indicators)
|
||||
|
||||
|
||||
def parse_vulnerability_table(table, section_header):
|
||||
def parse_vulnerability_table(table, section_header: Optional[Tag]) -> List[dict]:
|
||||
"""Parse a vulnerability table and extract vulnerability data."""
|
||||
vulnerabilities = []
|
||||
|
||||
|
|
@ -106,16 +110,21 @@ def parse_vulnerability_table(table, section_header):
|
|||
"affected_android_versions": versions_list
|
||||
}
|
||||
vulnerabilities.append(vuln)
|
||||
except Exception:
|
||||
except ValueError as e:
|
||||
logger.debug(f"Skipping malformed row: {e}")
|
||||
continue
|
||||
|
||||
return vulnerabilities
|
||||
|
||||
|
||||
def scrape_bulletin(url):
|
||||
def scrape_bulletin(url: str) -> List[dict]:
|
||||
"""Scrape a single monthly bulletin."""
|
||||
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
response.raise_for_status()
|
||||
try:
|
||||
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"HTTP error fetching {url}: {e}")
|
||||
raise
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
vulnerabilities = []
|
||||
|
|
@ -125,13 +134,13 @@ def scrape_bulletin(url):
|
|||
if element.name in ["h2", "h3"]:
|
||||
current_section = element
|
||||
elif element.name == "table" and current_section:
|
||||
vulns = parse_vulnerability_table(element, current_section)
|
||||
vulns = parse_vulnerability_table(element, current_section) # type: ignore
|
||||
vulnerabilities.extend(vulns)
|
||||
|
||||
return vulnerabilities
|
||||
|
||||
|
||||
def get_bulletin_date(url):
|
||||
def get_bulletin_date(url: str) -> Optional[str]:
|
||||
"""Extract date from bulletin URL."""
|
||||
match = re.search(r"/(\d{4}-\d{2}-\d{2})", url)
|
||||
if match:
|
||||
|
|
@ -140,14 +149,30 @@ def get_bulletin_date(url):
|
|||
|
||||
|
||||
def main():
|
||||
"""Main function to scrape all bulletins for the past year."""
|
||||
print("Fetching bulletin links...")
|
||||
parser = argparse.ArgumentParser(description='Scrape Android Security Bulletins')
|
||||
parser.add_argument('--months', type=int, default=12,
|
||||
help='Number of months to scrape (default: 12)')
|
||||
parser.add_argument('--output-dir', '-o', default='asb_data',
|
||||
help='Output directory for JSON files')
|
||||
parser.add_argument('--log-level', default='INFO',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||||
help='Logging level (default: INFO)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
setup_logging(args.log_level)
|
||||
|
||||
logger.info("Fetching bulletin links...")
|
||||
bulletin_links = get_bulletin_links()
|
||||
|
||||
one_year_ago = datetime.now() - timedelta(days=365)
|
||||
one_year_ago = datetime.now() - timedelta(days=365 * args.months // 12)
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
monthly_data = {}
|
||||
for url in bulletin_links:
|
||||
|
||||
for url in tqdm(bulletin_links, desc="Fetching bulletins"):
|
||||
date_str = get_bulletin_date(url)
|
||||
if not date_str:
|
||||
continue
|
||||
|
|
@ -158,7 +183,6 @@ def main():
|
|||
if bulletin_date < one_year_ago:
|
||||
continue
|
||||
|
||||
print(f"Scraping {date_str}...")
|
||||
vulnerabilities = scrape_bulletin(url)
|
||||
|
||||
if vulnerabilities:
|
||||
|
|
@ -168,21 +192,22 @@ def main():
|
|||
"vulnerabilities": vulnerabilities,
|
||||
"vulnerability_count": len(vulnerabilities)
|
||||
}
|
||||
logger.info(f"Scraped {date_str} ({len(vulnerabilities)} vulnerabilities)")
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"HTTP error processing {url}: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Error processing {url}: {e}")
|
||||
logger.warning(f"Failed to parse {url}: {e}")
|
||||
continue
|
||||
|
||||
output_dir = Path("asb_data")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
for month, data in monthly_data.items():
|
||||
for month, data in tqdm(monthly_data.items(), desc="Saving JSON files"):
|
||||
output_file = output_dir / f"{month}.json"
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
print(f"Saved {month}.json ({data['vulnerability_count']} vulnerabilities)")
|
||||
logger.info(f"Saved {month}.json ({data['vulnerability_count']} vulnerabilities)")
|
||||
|
||||
print(f"\nTotal: {len(monthly_data)} months of data saved to {output_dir}/")
|
||||
logger.info(f"\nTotal: {len(monthly_data)} months of data saved to {output_dir}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
63
utils/__init__.py
Normal file
63
utils/__init__.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Shared utilities for Android ASB scraper."""
|
||||
|
||||
import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def parse_patch_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Parse a patch URL to extract component and commit hash.
|
||||
|
||||
Args:
|
||||
url: The patch URL from AOSP
|
||||
|
||||
Returns:
|
||||
Tuple of (component, commit_hash) or (None, None) if parsing fails
|
||||
"""
|
||||
match = re.search(r'/([a-zA-Z0-9_-]+)/\+/[a-f0-9]{40}', url)
|
||||
if match:
|
||||
component = match.group(1)
|
||||
commit_hash = url.split('/')[-1]
|
||||
return (component, commit_hash)
|
||||
|
||||
match = re.search(r'/\+/[a-f0-9]{40}', url)
|
||||
if match:
|
||||
commit_hash = match.group(0).split('/')[-1]
|
||||
return (None, commit_hash)
|
||||
|
||||
return (None, None)
|
||||
|
||||
|
||||
def get_repo_url_from_patch_url(url: str) -> Optional[str]:
|
||||
"""Get AOSP repo URL from a patch URL.
|
||||
|
||||
Args:
|
||||
url: The patch URL
|
||||
|
||||
Returns:
|
||||
AOSP repo URL or None if parsing fails
|
||||
"""
|
||||
match = re.search(r'https://android\.googlesource\.com/([a-zA-Z0-9_-]+)', url)
|
||||
if match:
|
||||
component = match.group(1)
|
||||
return f"https://android.googlesource.com/{component}"
|
||||
return None
|
||||
|
||||
|
||||
def parse_android_version(version_str: str) -> Optional[int]:
|
||||
"""Extract major version number from Android version string.
|
||||
|
||||
Args:
|
||||
version_str: Android version string (e.g., "Android 14", "13")
|
||||
|
||||
Returns:
|
||||
Major version number or None if parsing fails
|
||||
"""
|
||||
match = re.search(r'Android\s+(\d+)', version_str)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
match = re.search(r'^(\d+)', version_str)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
return None
|
||||
29
utils/logging_config.py
Normal file
29
utils/logging_config.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Logging configuration for Android ASB scraper."""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def setup_logging(log_level: str = "INFO"):
|
||||
"""Setup terminal-only logging with StreamHandler.
|
||||
|
||||
Args:
|
||||
log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
handlers = [logging.StreamHandler()]
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, log_level.upper()),
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=handlers
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
__all__ = ["setup_logging", "logger"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue