diff options
190 files changed, 10188 insertions, 2269 deletions
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb index 6b3278fc6925..4f5b62060856 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "8572644" + build_id: "9653376" target: "CtsShim" source_file: "aosp_arm64/CtsShimPriv.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "tm-dev" + git_branch: "master" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb index 34c9c4d4fe08..404bcacde167 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "8572644" + build_id: "9653376" target: "CtsShim" source_file: "aosp_arm64/CtsShim.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "tm-dev" + git_branch: "master" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShimPriv_apk.asciipb new file mode 100644 index 000000000000..e89809138548 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShimPriv_apk.asciipb @@ -0,0 +1,15 @@ +drops { + android_build_drop { + build_id: "9653376" + target: "CtsShim" + source_file: "aosp_riscv64/CtsShimPriv.apk" + } + dest_file: "packages/CtsShim/apk//riscv64/CtsShimPriv.apk" + version: "" + version_group: "" + git_project: "platform/frameworks/base" + git_branch: "master" + transform: TRANSFORM_NONE + transform_options { + } +} diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShim_apk.asciipb new file mode 100644 index 000000000000..04092366c257 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__riscv64_CtsShim_apk.asciipb @@ -0,0 +1,15 @@ +drops { + android_build_drop { + build_id: "9653376" + target: "CtsShim" + source_file: "aosp_riscv64/CtsShim.apk" + } + dest_file: "packages/CtsShim/apk//riscv64/CtsShim.apk" + version: "" + version_group: "" + git_project: "platform/frameworks/base" + git_branch: "master" + transform: TRANSFORM_NONE + transform_options { + } +} diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb index 6897a027705f..045af028a668 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "8572644" + build_id: "9653376" target: "CtsShim" source_file: "aosp_x86_64/CtsShimPriv.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "tm-dev" + git_branch: "master" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb index 6dfa7810f22d..483b2f1764ae 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "8572644" + build_id: "9653376" target: "CtsShim" source_file: "aosp_x86_64/CtsShim.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "tm-dev" + git_branch: "master" transform: TRANSFORM_NONE transform_options { } diff --git a/TEST_MAPPING b/TEST_MAPPING index a48ce0ce1b6c..0ba24d112b63 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -73,14 +73,6 @@ ] }, { - "name": "TestablesTests", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - } - ] - }, - { "name": "FrameworksCoreTests", "options": [ { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 60afdc76d249..c6fd0bffb95e 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -906,6 +906,10 @@ public class JobSchedulerService extends com.android.server.SystemService synchronized (mLock) { mUidToPackageCache.remove(uid); } + } else { + synchronized (mJobSchedulerStub.mPersistCache) { + mJobSchedulerStub.mPersistCache.remove(pkgUid); + } } } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { if (DEBUG) { diff --git a/api/Android.bp b/api/Android.bp index 4cb52bc2d29a..9d20eca75f8f 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -41,23 +41,6 @@ bootstrap_go_package { } python_binary_host { - name: "api_versions_trimmer", - srcs: ["api_versions_trimmer.py"], -} - -python_test_host { - name: "api_versions_trimmer_unittests", - main: "api_versions_trimmer_unittests.py", - srcs: [ - "api_versions_trimmer_unittests.py", - "api_versions_trimmer.py", - ], - test_options: { - unit_test: true, - }, -} - -python_binary_host { name: "merge_annotation_zips", srcs: ["merge_annotation_zips.py"], } diff --git a/api/api.go b/api/api.go index 9876abb8ce36..09c238336a39 100644 --- a/api/api.go +++ b/api/api.go @@ -194,55 +194,6 @@ func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, sys } } -func createFilteredApiVersions(ctx android.LoadHookContext, modules []string) { - // For the filtered api versions, we prune all APIs except art module's APIs. because - // 1) ART apis are available by default to all modules, while other module-to-module deps are - // explicit and probably receive more scrutiny anyway - // 2) The number of ART/libcore APIs is large, so not linting them would create a large gap - // 3) It's a compromise. Ideally we wouldn't be filtering out any module APIs, and have - // per-module lint databases that excludes just that module's APIs. Alas, that's more - // difficult to achieve. - modules = remove(modules, art) - - for _, i := range []struct{ - name string - out string - in string - }{ - { - // We shouldn't need public-filtered or system-filtered. - // public-filtered is currently used to lint things that - // use the module sdk or the system server sdk, but those - // should be switched over to module-filtered and - // system-server-filtered, and then public-filtered can - // be removed. - name: "api-versions-xml-public-filtered", - out: "api-versions-public-filtered.xml", - in: ":api_versions_public{.api_versions.xml}", - }, { - name: "api-versions-xml-module-lib-filtered", - out: "api-versions-module-lib-filtered.xml", - in: ":api_versions_module_lib{.api_versions.xml}", - }, { - name: "api-versions-xml-system-server-filtered", - out: "api-versions-system-server-filtered.xml", - in: ":api_versions_system_server{.api_versions.xml}", - }, - } { - props := genruleProps{} - props.Name = proptools.StringPtr(i.name) - props.Out = []string{i.out} - // Note: order matters: first parameter is the full api-versions.xml - // after that the stubs files in any order - // stubs files are all modules that export API surfaces EXCEPT ART - props.Srcs = append([]string{i.in}, createSrcs(modules, ".stubs{.jar}")...) - props.Tools = []string{"api_versions_trimmer"} - props.Cmd = proptools.StringPtr("$(location api_versions_trimmer) $(out) $(in)") - props.Dists = []android.Dist{{Targets: []string{"sdk"}}} - ctx.CreateModule(genrule.GenRuleFactory, &props, &bp2buildNotAvailable) - } -} - func createMergedPublicStubs(ctx android.LoadHookContext, modules []string) { props := libraryProps{} props.Name = proptools.StringPtr("all-modules-public-stubs") @@ -395,8 +346,6 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath) - createFilteredApiVersions(ctx, bootclasspath) - createPublicStubsSourceFilegroup(ctx, bootclasspath) } diff --git a/api/api_versions_trimmer.py b/api/api_versions_trimmer.py deleted file mode 100755 index 9afd95a3003a..000000000000 --- a/api/api_versions_trimmer.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 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. - -"""Script to remove mainline APIs from the api-versions.xml.""" - -import argparse -import re -import xml.etree.ElementTree as ET -import zipfile - - -def read_classes(stubs): - """Read classes from the stubs file. - - Args: - stubs: argument can be a path to a file (a string), a file-like object or a - path-like object - - Returns: - a set of the classes found in the file (set of strings) - """ - classes = set() - with zipfile.ZipFile(stubs) as z: - for info in z.infolist(): - if (not info.is_dir() - and info.filename.endswith(".class") - and not info.filename.startswith("META-INF")): - # drop ".class" extension - classes.add(info.filename[:-6]) - return classes - - -def filter_method_tag(method, classes_to_remove): - """Updates the signature of this method by calling filter_method_signature. - - Updates the method passed into this function. - - Args: - method: xml element that represents a method - classes_to_remove: set of classes you to remove - """ - filtered = filter_method_signature(method.get("name"), classes_to_remove) - method.set("name", filtered) - - -def filter_method_signature(signature, classes_to_remove): - """Removes mentions of certain classes from this method signature. - - Replaces any existing classes that need to be removed, with java/lang/Object - - Args: - signature: string that is a java representation of a method signature - classes_to_remove: set of classes you to remove - """ - regex = re.compile("L.*?;") - start = signature.find("(") - matches = set(regex.findall(signature[start:])) - for m in matches: - # m[1:-1] to drop the leading `L` and `;` ending - if m[1:-1] in classes_to_remove: - signature = signature.replace(m, "Ljava/lang/Object;") - return signature - - -def filter_lint_database(database, classes_to_remove, output): - """Reads a lint database and writes a filtered version without some classes. - - Reads database from api-versions.xml and removes any references to classes - in the second argument. Writes the result (another xml with the same format - of the database) to output. - - Args: - database: path to xml with lint database to read - classes_to_remove: iterable (ideally a set or similar for quick - lookups) that enumerates the classes that should be removed - output: path to write the filtered database - """ - xml = ET.parse(database) - root = xml.getroot() - for c in xml.findall("class"): - cname = c.get("name") - if cname in classes_to_remove: - root.remove(c) - else: - # find the <extends /> tag inside this class to see if the parent - # has been removed from the known classes (attribute called name) - super_classes = c.findall("extends") - for super_class in super_classes: - super_class_name = super_class.get("name") - if super_class_name in classes_to_remove: - super_class.set("name", "java/lang/Object") - interfaces = c.findall("implements") - for interface in interfaces: - interface_name = interface.get("name") - if interface_name in classes_to_remove: - c.remove(interface) - for method in c.findall("method"): - filter_method_tag(method, classes_to_remove) - xml.write(output) - - -def main(): - """Run the program.""" - parser = argparse.ArgumentParser( - description= - ("Read a lint database (api-versions.xml) and many stubs jar files. " - "Produce another database file that doesn't include the classes present " - "in the stubs file(s).")) - parser.add_argument("output", help="Destination of the result (xml file).") - parser.add_argument( - "api_versions", - help="The lint database (api-versions.xml file) to read data from" - ) - parser.add_argument("stubs", nargs="+", help="The stubs jar file(s)") - parsed = parser.parse_args() - classes = set() - for stub in parsed.stubs: - classes.update(read_classes(stub)) - filter_lint_database(parsed.api_versions, classes, parsed.output) - - -if __name__ == "__main__": - main() diff --git a/api/api_versions_trimmer_unittests.py b/api/api_versions_trimmer_unittests.py deleted file mode 100644 index d2e5b7d1a07e..000000000000 --- a/api/api_versions_trimmer_unittests.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 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 io -import re -import unittest -import xml.etree.ElementTree as ET -import zipfile - -import api_versions_trimmer - - -def create_in_memory_zip_file(files): - f = io.BytesIO() - with zipfile.ZipFile(f, "w") as z: - for fname in files: - with z.open(fname, mode="w") as class_file: - class_file.write(b"") - return f - - -def indent(elem, level=0): - i = "\n" + level * " " - j = "\n" + (level - 1) * " " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for subelem in elem: - indent(subelem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = j - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = j - return elem - - -def pretty_print(s): - tree = ET.parse(io.StringIO(s)) - el = indent(tree.getroot()) - res = ET.tostring(el).decode("utf-8") - # remove empty lines inside the result because this still breaks some - # comparisons - return re.sub(r"\n\s*\n", "\n", res, re.MULTILINE) - - -class ApiVersionsTrimmerUnittests(unittest.TestCase): - - def setUp(self): - # so it prints diffs in long strings (xml files) - self.maxDiff = None - - def test_read_classes(self): - f = create_in_memory_zip_file( - ["a/b/C.class", - "a/b/D.class", - ] - ) - res = api_versions_trimmer.read_classes(f) - self.assertEqual({"a/b/C", "a/b/D"}, res) - - def test_read_classes_ignore_dex(self): - f = create_in_memory_zip_file( - ["a/b/C.class", - "a/b/D.class", - "a/b/E.dex", - "f.dex", - ] - ) - res = api_versions_trimmer.read_classes(f) - self.assertEqual({"a/b/C", "a/b/D"}, res) - - def test_read_classes_ignore_manifest(self): - f = create_in_memory_zip_file( - ["a/b/C.class", - "a/b/D.class", - "META-INFO/G.class" - ] - ) - res = api_versions_trimmer.read_classes(f) - self.assertEqual({"a/b/C", "a/b/D"}, res) - - def test_filter_method_signature(self): - xml = """ - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" since="24"/> - """ - method = ET.fromstring(xml) - classes_to_remove = {"android/accessibilityservice/GestureDescription"} - expected = "dispatchGesture(Ljava/lang/Object;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" - api_versions_trimmer.filter_method_tag(method, classes_to_remove) - self.assertEqual(expected, method.get("name")) - - def test_filter_method_signature_with_L_in_method(self): - xml = """ - <method name="dispatchLeftGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" since="24"/> - """ - method = ET.fromstring(xml) - classes_to_remove = {"android/accessibilityservice/GestureDescription"} - expected = "dispatchLeftGesture(Ljava/lang/Object;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" - api_versions_trimmer.filter_method_tag(method, classes_to_remove) - self.assertEqual(expected, method.get("name")) - - def test_filter_method_signature_with_L_in_class(self): - xml = """ - <method name="dispatchGesture(Landroid/accessibilityservice/LeftGestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" since="24"/> - """ - method = ET.fromstring(xml) - classes_to_remove = {"android/accessibilityservice/LeftGestureDescription"} - expected = "dispatchGesture(Ljava/lang/Object;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" - api_versions_trimmer.filter_method_tag(method, classes_to_remove) - self.assertEqual(expected, method.get("name")) - - def test_filter_method_signature_with_inner_class(self): - xml = """ - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription$Inner;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" since="24"/> - """ - method = ET.fromstring(xml) - classes_to_remove = {"android/accessibilityservice/GestureDescription$Inner"} - expected = "dispatchGesture(Ljava/lang/Object;Landroid/accessibilityservice/AccessibilityService$GestureResultCallback;Landroid/os/Handler;)Z" - api_versions_trimmer.filter_method_tag(method, classes_to_remove) - self.assertEqual(expected, method.get("name")) - - def _run_filter_db_test(self, database_str, expected): - """Performs the pattern of testing the filter_lint_database method. - - Filters instances of the class "a/b/C" (hard-coded) from the database string - and compares the result with the expected result (performs formatting of - the xml of both inputs) - - Args: - database_str: string, the contents of the lint database (api-versions.xml) - expected: string, the expected result after filtering the original - database - """ - database = io.StringIO(database_str) - classes_to_remove = {"a/b/C"} - output = io.BytesIO() - api_versions_trimmer.filter_lint_database( - database, - classes_to_remove, - output - ) - expected = pretty_print(expected) - res = pretty_print(output.getvalue().decode("utf-8")) - self.assertEqual(expected, res) - - def test_filter_lint_database_updates_method_signature_params(self): - self._run_filter_db_test( - database_str=""" - <api version="2"> - <!-- will be removed --> - <class name="a/b/C" since="1"> - <extends name="java/lang/Object"/> - </class> - - <class name="a/b/E" since="1"> - <!-- extends will be modified --> - <extends name="a/b/C"/> - <!-- first parameter will be modified --> - <method name="dispatchGesture(La/b/C;Landroid/os/Handler;)Z" since="24"/> - <!-- second should remain untouched --> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """, - expected=""" - <api version="2"> - <class name="a/b/E" since="1"> - <extends name="java/lang/Object"/> - <method name="dispatchGesture(Ljava/lang/Object;Landroid/os/Handler;)Z" since="24"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """) - - def test_filter_lint_database_updates_method_signature_return(self): - self._run_filter_db_test( - database_str=""" - <api version="2"> - <!-- will be removed --> - <class name="a/b/C" since="1"> - <extends name="java/lang/Object"/> - </class> - - <class name="a/b/E" since="1"> - <!-- extends will be modified --> - <extends name="a/b/C"/> - <!-- return type should be changed --> - <method name="gestureIdToString(I)La/b/C;" since="24"/> - </class> - </api> - """, - expected=""" - <api version="2"> - <class name="a/b/E" since="1"> - - <extends name="java/lang/Object"/> - - <method name="gestureIdToString(I)Ljava/lang/Object;" since="24"/> - </class> - </api> - """) - - def test_filter_lint_database_removes_implements(self): - self._run_filter_db_test( - database_str=""" - <api version="2"> - <!-- will be removed --> - <class name="a/b/C" since="1"> - <extends name="java/lang/Object"/> - </class> - - <class name="a/b/D" since="1"> - <extends name="java/lang/Object"/> - <implements name="a/b/C"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """, - expected=""" - <api version="2"> - - <class name="a/b/D" since="1"> - <extends name="java/lang/Object"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """) - - def test_filter_lint_database_updates_extends(self): - self._run_filter_db_test( - database_str=""" - <api version="2"> - <!-- will be removed --> - <class name="a/b/C" since="1"> - <extends name="java/lang/Object"/> - </class> - - <class name="a/b/E" since="1"> - <!-- extends will be modified --> - <extends name="a/b/C"/> - <method name="dispatchGesture(Ljava/lang/Object;Landroid/os/Handler;)Z" since="24"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """, - expected=""" - <api version="2"> - <class name="a/b/E" since="1"> - <extends name="java/lang/Object"/> - <method name="dispatchGesture(Ljava/lang/Object;Landroid/os/Handler;)Z" since="24"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """) - - def test_filter_lint_database_removes_class(self): - self._run_filter_db_test( - database_str=""" - <api version="2"> - <!-- will be removed --> - <class name="a/b/C" since="1"> - <extends name="java/lang/Object"/> - </class> - - <class name="a/b/D" since="1"> - <extends name="java/lang/Object"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """, - expected=""" - <api version="2"> - - <class name="a/b/D" since="1"> - <extends name="java/lang/Object"/> - <method name="dispatchGesture(Landroid/accessibilityservice/GestureDescription;Landroid/accessibilityservice/AccessibilityService$GestureRe -sultCallback;Landroid/os/Handler;)Z" since="24"/> - </class> - </api> - """) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index 1efdf7759f32..6b6bc9728b7f 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -460,6 +460,7 @@ public: EGLConfig BootAnimation::getEglConfig(const EGLDisplay& display) { const EGLint attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, diff --git a/core/api/current.txt b/core/api/current.txt index a8552f80e31f..4c1594b19409 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18727,8 +18727,8 @@ package android.hardware.usb { method public int bulkTransfer(android.hardware.usb.UsbEndpoint, byte[], int, int, int); method public boolean claimInterface(android.hardware.usb.UsbInterface, boolean); method public void close(); - method public int controlTransfer(int, int, int, int, byte[], int, int); - method public int controlTransfer(int, int, int, int, byte[], int, int, int); + method public int controlTransfer(int, int, int, int, @Nullable byte[], int, int); + method public int controlTransfer(int, int, int, int, @Nullable byte[], int, int, int); method public int getFileDescriptor(); method public byte[] getRawDescriptors(); method public String getSerial(); @@ -27503,24 +27503,15 @@ package android.nfc { public final class NfcAdapter { method public void disableForegroundDispatch(android.app.Activity); - method @Deprecated public void disableForegroundNdefPush(android.app.Activity); method public void disableReaderMode(android.app.Activity); method public void enableForegroundDispatch(android.app.Activity, android.app.PendingIntent, android.content.IntentFilter[], String[][]); - method @Deprecated public void enableForegroundNdefPush(android.app.Activity, android.nfc.NdefMessage); method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle); method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context); method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo(); method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler); - method @Deprecated public boolean invokeBeam(android.app.Activity); method public boolean isEnabled(); - method @Deprecated public boolean isNdefPushEnabled(); method public boolean isSecureNfcEnabled(); method public boolean isSecureNfcSupported(); - method @Deprecated public void setBeamPushUris(android.net.Uri[], android.app.Activity); - method @Deprecated public void setBeamPushUrisCallback(android.nfc.NfcAdapter.CreateBeamUrisCallback, android.app.Activity); - method @Deprecated public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, android.app.Activity...); - method @Deprecated public void setNdefPushMessageCallback(android.nfc.NfcAdapter.CreateNdefMessageCallback, android.app.Activity, android.app.Activity...); - method @Deprecated public void setOnNdefPushCompleteCallback(android.nfc.NfcAdapter.OnNdefPushCompleteCallback, android.app.Activity, android.app.Activity...); field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; @@ -41371,6 +41362,8 @@ package android.telephony { field public static final String KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY = "carrier_nr_availabilities_int_array"; field public static final String KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL = "carrier_provisions_wifi_merged_networks_bool"; field public static final String KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL = "carrier_rcs_provisioning_required_bool"; + field public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array"; + field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array"; field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string"; field public static final String KEY_CARRIER_SETTINGS_ENABLE_BOOL = "carrier_settings_enable_bool"; field public static final String KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL = "carrier_supports_opp_data_auto_provisioning_bool"; @@ -42752,9 +42745,12 @@ package android.telephony { method public int getDomain(); method @Nullable public String getRegisteredPlmn(); method public int getTransportType(); - method public boolean isRegistered(); - method public boolean isRoaming(); - method public boolean isSearching(); + method public boolean isNetworkRegistered(); + method public boolean isNetworkRoaming(); + method public boolean isNetworkSearching(); + method @Deprecated public boolean isRegistered(); + method @Deprecated public boolean isRoaming(); + method @Deprecated public boolean isSearching(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.telephony.NetworkRegistrationInfo> CREATOR; field public static final int DOMAIN_CS = 1; // 0x1 @@ -43311,6 +43307,7 @@ package android.telephony { method public int getActiveSubscriptionInfoCountMax(); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telephony.SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getActiveSubscriptionInfoList(); + method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public java.util.List<android.telephony.SubscriptionInfo> getAllSubscriptionInfoList(); method @NonNull public java.util.List<android.telephony.SubscriptionInfo> getCompleteActiveSubscriptionInfoList(); method public static int getDefaultDataSubscriptionId(); method public static int getDefaultSmsSubscriptionId(); @@ -43322,7 +43319,8 @@ package android.telephony { method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int, int); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public String getPhoneNumber(int); method public static int getSlotIndex(int); - method @Nullable public int[] getSubscriptionIds(int); + method public static int getSubscriptionId(int); + method @Deprecated @Nullable public int[] getSubscriptionIds(int); method @NonNull public java.util.List<android.telephony.SubscriptionPlan> getSubscriptionPlans(int); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public java.util.List<android.telephony.SubscriptionInfo> getSubscriptionsInGroup(@NonNull android.os.ParcelUuid); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public boolean isActiveSubscriptionId(int); @@ -43506,6 +43504,7 @@ package android.telephony { method public int describeContents(); method public int getNetworkType(); method public int getOverrideNetworkType(); + method public boolean isRoaming(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.telephony.TelephonyDisplayInfo> CREATOR; field public static final int OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO = 2; // 0x2 diff --git a/core/api/removed.txt b/core/api/removed.txt index 1fa1e89fb46e..8b3696a1e6d9 100644 --- a/core/api/removed.txt +++ b/core/api/removed.txt @@ -252,6 +252,22 @@ package android.net { } +package android.nfc { + + public final class NfcAdapter { + method @Deprecated public void disableForegroundNdefPush(android.app.Activity); + method @Deprecated public void enableForegroundNdefPush(android.app.Activity, android.nfc.NdefMessage); + method @Deprecated public boolean invokeBeam(android.app.Activity); + method @Deprecated public boolean isNdefPushEnabled(); + method @Deprecated public void setBeamPushUris(android.net.Uri[], android.app.Activity); + method @Deprecated public void setBeamPushUrisCallback(android.nfc.NfcAdapter.CreateBeamUrisCallback, android.app.Activity); + method @Deprecated public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, android.app.Activity...); + method @Deprecated public void setNdefPushMessageCallback(android.nfc.NfcAdapter.CreateNdefMessageCallback, android.app.Activity, android.app.Activity...); + method @Deprecated public void setOnNdefPushCompleteCallback(android.nfc.NfcAdapter.OnNdefPushCompleteCallback, android.app.Activity, android.app.Activity...); + } + +} + package android.os { public class BatteryManager { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 9882a4f35f99..821944f406cd 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9112,9 +9112,7 @@ package android.nfc { method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean addNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler, String[]); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean); - method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disableNdefPush(); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable(); - method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableNdefPush(); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public java.util.Map<java.lang.String,java.lang.Boolean> getTagIntentAppPreferenceForUser(int); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn(); @@ -9123,10 +9121,8 @@ package android.nfc { method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void registerControllerAlwaysOnListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean); - method public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, int); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); - field public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 1; // 0x1 field public static final int TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND = -1; // 0xffffffff field public static final int TAG_INTENT_APP_PREF_RESULT_SUCCESS = 0; // 0x0 field public static final int TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE = -2; // 0xfffffffe @@ -12832,7 +12828,8 @@ package android.telephony { public final class NetworkRegistrationInfo implements android.os.Parcelable { method @Nullable public android.telephony.DataSpecificRegistrationInfo getDataSpecificInfo(); - method public int getRegistrationState(); + method public int getNetworkRegistrationState(); + method @Deprecated public int getRegistrationState(); method public int getRejectCause(); method public int getRoamingType(); method public boolean isEmergencyEnabled(); diff --git a/core/api/system-removed.txt b/core/api/system-removed.txt index 2c5acf182d51..1c10356c6b03 100644 --- a/core/api/system-removed.txt +++ b/core/api/system-removed.txt @@ -140,6 +140,17 @@ package android.media.tv { } +package android.nfc { + + public final class NfcAdapter { + method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disableNdefPush(); + method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableNdefPush(); + method public void setNdefPushMessage(android.nfc.NdefMessage, android.app.Activity, int); + field public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 1; // 0x1 + } + +} + package android.os { public class Build { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 7cfb66a3dcc6..6a5ef3a67488 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -141,8 +141,9 @@ package android.app { field public static final int PROCESS_CAPABILITY_FOREGROUND_CAMERA = 2; // 0x2 field public static final int PROCESS_CAPABILITY_FOREGROUND_LOCATION = 1; // 0x1 field public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 4; // 0x4 - field public static final int PROCESS_CAPABILITY_NETWORK = 8; // 0x8 + field @Deprecated public static final int PROCESS_CAPABILITY_NETWORK = 8; // 0x8 field public static final int PROCESS_CAPABILITY_NONE = 0; // 0x0 + field public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 8; // 0x8 field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4 field public static final int PROCESS_STATE_TOP = 2; // 0x2 field public static final int STOP_USER_ON_SWITCH_DEFAULT = -1; // 0xffffffff diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index eadf758e2b99..d328b34a2a8f 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -643,9 +643,17 @@ public class ActivityManager { @TestApi public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 1 << 2; - /** @hide Process can access network despite any power saving resrictions */ + /** @hide Process can access network despite any power saving restrictions */ @TestApi - public static final int PROCESS_CAPABILITY_NETWORK = 1 << 3; + public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 1 << 3; + /** + * @hide + * @deprecated Use {@link #PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK} instead. + */ + @TestApi + @Deprecated + public static final int PROCESS_CAPABILITY_NETWORK = + PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; /** @hide all capabilities, the ORing of all flags in {@link ProcessCapability}*/ @TestApi diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 559989368a9f..5ba7a4cb4f54 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -238,6 +238,7 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.net.InetAddress; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -4221,18 +4222,20 @@ public final class ActivityThread extends ClientTransactionHandler static void handleAttachStartupAgents(String dataDir) { try { - Path code_cache = ContextImpl.getCodeCacheDirBeforeBind(new File(dataDir)).toPath(); - if (!Files.exists(code_cache)) { + Path codeCache = ContextImpl.getCodeCacheDirBeforeBind(new File(dataDir)).toPath(); + if (!Files.exists(codeCache)) { return; } - Path startup_path = code_cache.resolve("startup_agents"); - if (Files.exists(startup_path)) { - for (Path p : Files.newDirectoryStream(startup_path)) { - handleAttachAgent( - p.toAbsolutePath().toString() - + "=" - + dataDir, - null); + Path startupPath = codeCache.resolve("startup_agents"); + if (Files.exists(startupPath)) { + try (DirectoryStream<Path> startupFiles = Files.newDirectoryStream(startupPath)) { + for (Path p : startupFiles) { + handleAttachAgent( + p.toAbsolutePath().toString() + + "=" + + dataDir, + null); + } } } } catch (Exception e) { diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index bccbb381bfb1..2a854b2f6513 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -37,8 +37,6 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; -import com.android.internal.util.Preconditions; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; @@ -61,7 +59,8 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private long mIdForResponseEvent; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; - private @Nullable String mDeliveryGroupMatchingKey; + private @Nullable String mDeliveryGroupMatchingNamespaceFragment; + private @Nullable String mDeliveryGroupMatchingKeyFragment; private @Nullable BundleMerger mDeliveryGroupExtrasMerger; private @Nullable IntentFilter mDeliveryGroupMatchingFilter; private @DeferralPolicy int mDeferralPolicy; @@ -209,7 +208,13 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.deliveryGroupPolicy"; /** - * Corresponds to {@link #setDeliveryGroupMatchingKey(String, String)}. + * Corresponds to namespace fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. + */ + private static final String KEY_DELIVERY_GROUP_NAMESPACE = + "android:broadcast.deliveryGroupMatchingNamespace"; + + /** + * Corresponds to key fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. */ private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupMatchingKey"; @@ -350,7 +355,8 @@ public class BroadcastOptions extends ComponentOptions { mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); - mDeliveryGroupMatchingKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupMatchingNamespaceFragment = opts.getString(KEY_DELIVERY_GROUP_NAMESPACE); + mDeliveryGroupMatchingKeyFragment = opts.getString(KEY_DELIVERY_GROUP_KEY); mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, BundleMerger.class); mDeliveryGroupMatchingFilter = opts.getParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, @@ -864,11 +870,8 @@ public class BroadcastOptions extends ComponentOptions { @NonNull public BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String namespace, @NonNull String key) { - Preconditions.checkArgument(!namespace.contains(":"), - "namespace should not contain ':'"); - Preconditions.checkArgument(!key.contains(":"), - "key should not contain ':'"); - mDeliveryGroupMatchingKey = namespace + ":" + key; + mDeliveryGroupMatchingNamespaceFragment = Objects.requireNonNull(namespace); + mDeliveryGroupMatchingKeyFragment = Objects.requireNonNull(key); return this; } @@ -881,7 +884,38 @@ public class BroadcastOptions extends ComponentOptions { */ @Nullable public String getDeliveryGroupMatchingKey() { - return mDeliveryGroupMatchingKey; + if (mDeliveryGroupMatchingNamespaceFragment == null + || mDeliveryGroupMatchingKeyFragment == null) { + return null; + } + return String.join(":", mDeliveryGroupMatchingNamespaceFragment, + mDeliveryGroupMatchingKeyFragment); + } + + /** + * Return the namespace fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group namespace fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingNamespaceFragment() { + return mDeliveryGroupMatchingNamespaceFragment; + } + + /** + * Return the key fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group key fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingKeyFragment() { + return mDeliveryGroupMatchingKeyFragment; } /** @@ -889,7 +923,8 @@ public class BroadcastOptions extends ComponentOptions { * {@link #setDeliveryGroupMatchingKey(String, String)}. */ public void clearDeliveryGroupMatchingKey() { - mDeliveryGroupMatchingKey = null; + mDeliveryGroupMatchingNamespaceFragment = null; + mDeliveryGroupMatchingKeyFragment = null; } /** @@ -1101,8 +1136,11 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); } - if (mDeliveryGroupMatchingKey != null) { - b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKey); + if (mDeliveryGroupMatchingNamespaceFragment != null) { + b.putString(KEY_DELIVERY_GROUP_NAMESPACE, mDeliveryGroupMatchingNamespaceFragment); + } + if (mDeliveryGroupMatchingKeyFragment != null) { + b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKeyFragment); } if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { if (mDeliveryGroupExtrasMerger != null) { diff --git a/core/java/android/hardware/soundtrigger/OWNERS b/core/java/android/hardware/soundtrigger/OWNERS index 01b2cb981bbb..1e41886fe716 100644 --- a/core/java/android/hardware/soundtrigger/OWNERS +++ b/core/java/android/hardware/soundtrigger/OWNERS @@ -1,2 +1 @@ -atneya@google.com -elaurent@google.com +include /media/java/android/media/soundtrigger/OWNERS diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index 7c2e518b8544..44144d92f56a 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -238,7 +238,7 @@ public class UsbDeviceConnection { * or negative value for failure */ public int controlTransfer(int requestType, int request, int value, - int index, byte[] buffer, int length, int timeout) { + int index, @Nullable byte[] buffer, int length, int timeout) { return controlTransfer(requestType, request, value, index, buffer, 0, length, timeout); } @@ -263,7 +263,7 @@ public class UsbDeviceConnection { * or negative value for failure */ public int controlTransfer(int requestType, int request, int value, int index, - byte[] buffer, int offset, int length, int timeout) { + @Nullable byte[] buffer, int offset, int length, int timeout) { checkBounds(buffer, offset, length); return native_control_request(requestType, request, value, index, buffer, offset, length, timeout); diff --git a/core/java/android/net/Ikev2VpnProfile.java b/core/java/android/net/Ikev2VpnProfile.java index a20191cab774..74775a820620 100644 --- a/core/java/android/net/Ikev2VpnProfile.java +++ b/core/java/android/net/Ikev2VpnProfile.java @@ -769,6 +769,19 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile { } } + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("IkeV2VpnProfile ["); + sb.append(" MaxMtu=" + mMaxMtu); + if (mIsBypassable) sb.append(" Bypassable"); + if (mRequiresInternetValidation) sb.append(" RequiresInternetValidation"); + if (mIsRestrictedToTestNetworks) sb.append(" RestrictedToTestNetworks"); + if (mAutomaticNattKeepaliveTimerEnabled) sb.append(" AutomaticNattKeepaliveTimerEnabled"); + if (mAutomaticIpVersionSelectionEnabled) sb.append(" AutomaticIpVersionSelectionEnabled"); + sb.append("]"); + return sb.toString(); + } + /** A incremental builder for IKEv2 VPN profiles */ public static final class Builder { private int mType = -1; diff --git a/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java b/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java index 38b3174abd4c..46cf0163c0e5 100644 --- a/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java +++ b/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java @@ -354,6 +354,7 @@ public final class VcnCellUnderlyingNetworkTemplate extends VcnUnderlyingNetwork } /** @hide */ + @Override public Map<Integer, Integer> getCapabilitiesMatchCriteria() { return Collections.unmodifiableMap(new HashMap<>(mCapabilitiesMatchCriteria)); } diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java index 6f9c9dd918d1..a27e9230d473 100644 --- a/core/java/android/net/vcn/VcnConfig.java +++ b/core/java/android/net/vcn/VcnConfig.java @@ -16,6 +16,7 @@ package android.net.vcn; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static com.android.internal.annotations.VisibleForTesting.Visibility; @@ -75,6 +76,7 @@ public final class VcnConfig implements Parcelable { static { ALLOWED_TRANSPORTS.add(TRANSPORT_WIFI); ALLOWED_TRANSPORTS.add(TRANSPORT_CELLULAR); + ALLOWED_TRANSPORTS.add(TRANSPORT_TEST); } private static final String PACKAGE_NAME_KEY = "mPackageName"; @@ -155,6 +157,11 @@ public final class VcnConfig implements Parcelable { + transport + " which might be from a new version of VcnConfig"); } + + if (transport == TRANSPORT_TEST && !mIsTestModeProfile) { + throw new IllegalArgumentException( + "Found TRANSPORT_TEST in a non-test-mode profile"); + } } } diff --git a/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java b/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java index 9235d0913295..edf2c093bc8b 100644 --- a/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java +++ b/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java @@ -29,6 +29,7 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Map; import java.util.Objects; /** @@ -307,4 +308,7 @@ public abstract class VcnUnderlyingNetworkTemplate { public int getMinExitDownstreamBandwidthKbps() { return mMinExitDownstreamBandwidthKbps; } + + /** @hide */ + public abstract Map<Integer, Integer> getCapabilitiesMatchCriteria(); } diff --git a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java index 2544a6d63561..2e6b09f032fb 100644 --- a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java +++ b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java @@ -15,6 +15,9 @@ */ package android.net.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_ANY; + import static com.android.internal.annotations.VisibleForTesting.Visibility; import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; import static com.android.server.vcn.util.PersistableBundleUtils.STRING_SERIALIZER; @@ -23,6 +26,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.net.NetworkCapabilities; +import android.net.vcn.VcnUnderlyingNetworkTemplate.MatchCriteria; import android.os.PersistableBundle; import android.util.ArraySet; @@ -32,6 +36,7 @@ import com.android.server.vcn.util.PersistableBundleUtils; import java.util.ArrayList; import java.util.Collections; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -162,6 +167,12 @@ public final class VcnWifiUnderlyingNetworkTemplate extends VcnUnderlyingNetwork return Collections.unmodifiableSet(mSsids); } + /** @hide */ + @Override + public Map<Integer, Integer> getCapabilitiesMatchCriteria() { + return Collections.singletonMap(NET_CAPABILITY_INTERNET, MATCH_REQUIRED); + } + /** This class is used to incrementally build VcnWifiUnderlyingNetworkTemplate objects. */ public static final class Builder { private int mMeteredMatchCriteria = MATCH_ANY; diff --git a/core/java/android/nfc/BeamShareData.java b/core/java/android/nfc/BeamShareData.java deleted file mode 100644 index 6a40f98fe21c..000000000000 --- a/core/java/android/nfc/BeamShareData.java +++ /dev/null @@ -1,67 +0,0 @@ -package android.nfc; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.UserHandle; - -/** - * Class to IPC data to be shared over Android Beam. - * Allows bundling NdefMessage, Uris and flags in a single - * IPC call. This is important as we want to reduce the - * amount of IPC calls at "touch time". - * @hide - */ -public final class BeamShareData implements Parcelable { - public final NdefMessage ndefMessage; - public final Uri[] uris; - public final UserHandle userHandle; - public final int flags; - - public BeamShareData(NdefMessage msg, Uri[] uris, UserHandle userHandle, int flags) { - this.ndefMessage = msg; - this.uris = uris; - this.userHandle = userHandle; - this.flags = flags; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - int urisLength = (uris != null) ? uris.length : 0; - dest.writeParcelable(ndefMessage, 0); - dest.writeInt(urisLength); - if (urisLength > 0) { - dest.writeTypedArray(uris, 0); - } - dest.writeParcelable(userHandle, 0); - dest.writeInt(this.flags); - } - - public static final @android.annotation.NonNull Parcelable.Creator<BeamShareData> CREATOR = - new Parcelable.Creator<BeamShareData>() { - @Override - public BeamShareData createFromParcel(Parcel source) { - Uri[] uris = null; - NdefMessage msg = source.readParcelable(NdefMessage.class.getClassLoader(), android.nfc.NdefMessage.class); - int numUris = source.readInt(); - if (numUris > 0) { - uris = new Uri[numUris]; - source.readTypedArray(uris, Uri.CREATOR); - } - UserHandle userHandle = source.readParcelable(UserHandle.class.getClassLoader(), android.os.UserHandle.class); - int flags = source.readInt(); - - return new BeamShareData(msg, uris, userHandle, flags); - } - - @Override - public BeamShareData[] newArray(int size) { - return new BeamShareData[size]; - } - }; -} diff --git a/core/java/android/nfc/IAppCallback.aidl b/core/java/android/nfc/IAppCallback.aidl index 133146de2aa1..b06bf06d5197 100644 --- a/core/java/android/nfc/IAppCallback.aidl +++ b/core/java/android/nfc/IAppCallback.aidl @@ -16,7 +16,6 @@ package android.nfc; -import android.nfc.BeamShareData; import android.nfc.Tag; /** @@ -24,7 +23,5 @@ import android.nfc.Tag; */ interface IAppCallback { - BeamShareData createBeamShareData(byte peerLlcpVersion); - oneway void onNdefPushComplete(byte peerLlcpVersion); oneway void onTagDiscovered(in Tag tag); } diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 8a30ef4c2121..a6d8cafe8263 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -18,7 +18,6 @@ package android.nfc; import android.app.PendingIntent; import android.content.IntentFilter; -import android.nfc.BeamShareData; import android.nfc.NdefMessage; import android.nfc.Tag; import android.nfc.TechListParcel; @@ -47,24 +46,18 @@ interface INfcAdapter int getState(); boolean disable(boolean saveState); boolean enable(); - boolean enableNdefPush(); - boolean disableNdefPush(); - boolean isNdefPushEnabled(); void pausePolling(int timeoutInMs); void resumePolling(); void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); void setAppCallback(in IAppCallback callback); - oneway void invokeBeam(); - oneway void invokeBeamInternal(in BeamShareData shareData); boolean ignore(int nativeHandle, int debounceMs, ITagRemovedCallback callback); void dispatch(in Tag tag); void setReaderMode (IBinder b, IAppCallback callback, int flags, in Bundle extras); - void setP2pModes(int initatorModes, int targetModes); void addNfcUnlockHandler(INfcUnlockHandler unlockHandler, in int[] techList); void removeNfcUnlockHandler(INfcUnlockHandler unlockHandler); diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 911aaf317e3e..8d75cac531fb 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -19,9 +19,6 @@ package android.nfc; import android.app.Activity; import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; -import android.content.ContentProvider; -import android.content.Intent; -import android.net.Uri; import android.nfc.NfcAdapter.ReaderCallback; import android.os.Binder; import android.os.Bundle; @@ -110,14 +107,8 @@ public final class NfcActivityManager extends IAppCallback.Stub class NfcActivityState { boolean resumed = false; Activity activity; - NdefMessage ndefMessage = null; // static NDEF message - NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null; - NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null; - NfcAdapter.CreateBeamUrisCallback uriCallback = null; - Uri[] uris = null; - int flags = 0; - int readerModeFlags = 0; NfcAdapter.ReaderCallback readerCallback = null; + int readerModeFlags = 0; Bundle readerModeExtras = null; Binder token; @@ -137,24 +128,16 @@ public final class NfcActivityManager extends IAppCallback.Stub unregisterApplication(activity.getApplication()); resumed = false; activity = null; - ndefMessage = null; - ndefMessageCallback = null; - onNdefPushCompleteCallback = null; - uriCallback = null; - uris = null; + readerCallback = null; readerModeFlags = 0; + readerModeExtras = null; token = null; } @Override public String toString() { - StringBuilder s = new StringBuilder("[").append(" "); - s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" "); - s.append(uriCallback).append(" "); - if (uris != null) { - for (Uri uri : uris) { - s.append(onNdefPushCompleteCallback).append(" ").append(uri).append("]"); - } - } + StringBuilder s = new StringBuilder("["); + s.append(readerCallback); + s.append("]"); return s.toString(); } } @@ -245,92 +228,6 @@ public final class NfcActivityManager extends IAppCallback.Stub } } - public void setNdefPushContentUri(Activity activity, Uri[] uris) { - boolean isResumed; - synchronized (NfcActivityManager.this) { - NfcActivityState state = getActivityState(activity); - state.uris = uris; - isResumed = state.resumed; - } - if (isResumed) { - // requestNfcServiceCallback() verifies permission also - requestNfcServiceCallback(); - } else { - // Crash API calls early in case NFC permission is missing - verifyNfcPermission(); - } - } - - - public void setNdefPushContentUriCallback(Activity activity, - NfcAdapter.CreateBeamUrisCallback callback) { - boolean isResumed; - synchronized (NfcActivityManager.this) { - NfcActivityState state = getActivityState(activity); - state.uriCallback = callback; - isResumed = state.resumed; - } - if (isResumed) { - // requestNfcServiceCallback() verifies permission also - requestNfcServiceCallback(); - } else { - // Crash API calls early in case NFC permission is missing - verifyNfcPermission(); - } - } - - public void setNdefPushMessage(Activity activity, NdefMessage message, int flags) { - boolean isResumed; - synchronized (NfcActivityManager.this) { - NfcActivityState state = getActivityState(activity); - state.ndefMessage = message; - state.flags = flags; - isResumed = state.resumed; - } - if (isResumed) { - // requestNfcServiceCallback() verifies permission also - requestNfcServiceCallback(); - } else { - // Crash API calls early in case NFC permission is missing - verifyNfcPermission(); - } - } - - public void setNdefPushMessageCallback(Activity activity, - NfcAdapter.CreateNdefMessageCallback callback, int flags) { - boolean isResumed; - synchronized (NfcActivityManager.this) { - NfcActivityState state = getActivityState(activity); - state.ndefMessageCallback = callback; - state.flags = flags; - isResumed = state.resumed; - } - if (isResumed) { - // requestNfcServiceCallback() verifies permission also - requestNfcServiceCallback(); - } else { - // Crash API calls early in case NFC permission is missing - verifyNfcPermission(); - } - } - - public void setOnNdefPushCompleteCallback(Activity activity, - NfcAdapter.OnNdefPushCompleteCallback callback) { - boolean isResumed; - synchronized (NfcActivityManager.this) { - NfcActivityState state = getActivityState(activity); - state.onNdefPushCompleteCallback = callback; - isResumed = state.resumed; - } - if (isResumed) { - // requestNfcServiceCallback() verifies permission also - requestNfcServiceCallback(); - } else { - // Crash API calls early in case NFC permission is missing - verifyNfcPermission(); - } - } - /** * Request or unrequest NFC service callbacks. * Makes IPC call - do not hold lock. @@ -351,86 +248,6 @@ public final class NfcActivityManager extends IAppCallback.Stub } } - /** Callback from NFC service, usually on binder thread */ - @Override - public BeamShareData createBeamShareData(byte peerLlcpVersion) { - NfcAdapter.CreateNdefMessageCallback ndefCallback; - NfcAdapter.CreateBeamUrisCallback urisCallback; - NdefMessage message; - Activity activity; - Uri[] uris; - int flags; - NfcEvent event = new NfcEvent(mAdapter, peerLlcpVersion); - synchronized (NfcActivityManager.this) { - NfcActivityState state = findResumedActivityState(); - if (state == null) return null; - - ndefCallback = state.ndefMessageCallback; - urisCallback = state.uriCallback; - message = state.ndefMessage; - uris = state.uris; - flags = state.flags; - activity = state.activity; - } - final long ident = Binder.clearCallingIdentity(); - try { - // Make callbacks without lock - if (ndefCallback != null) { - message = ndefCallback.createNdefMessage(event); - } - if (urisCallback != null) { - uris = urisCallback.createBeamUris(event); - if (uris != null) { - ArrayList<Uri> validUris = new ArrayList<Uri>(); - for (Uri uri : uris) { - if (uri == null) { - Log.e(TAG, "Uri not allowed to be null."); - continue; - } - String scheme = uri.getScheme(); - if (scheme == null || (!scheme.equalsIgnoreCase("file") && - !scheme.equalsIgnoreCase("content"))) { - Log.e(TAG, "Uri needs to have " + - "either scheme file or scheme content"); - continue; - } - uri = ContentProvider.maybeAddUserId(uri, activity.getUserId()); - validUris.add(uri); - } - - uris = validUris.toArray(new Uri[validUris.size()]); - } - } - if (uris != null && uris.length > 0) { - for (Uri uri : uris) { - // Grant the NFC process permission to read these URIs - activity.grantUriPermission("com.android.nfc", uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - return new BeamShareData(message, uris, activity.getUser(), flags); - } - - /** Callback from NFC service, usually on binder thread */ - @Override - public void onNdefPushComplete(byte peerLlcpVersion) { - NfcAdapter.OnNdefPushCompleteCallback callback; - synchronized (NfcActivityManager.this) { - NfcActivityState state = findResumedActivityState(); - if (state == null) return; - - callback = state.onNdefPushCompleteCallback; - } - NfcEvent event = new NfcEvent(mAdapter, peerLlcpVersion); - // Make callback without lock - if (callback != null) { - callback.onNdefPushComplete(event); - } - } - @Override public void onTagDiscovered(Tag tag) throws RemoteException { NfcAdapter.ReaderCallback callback; diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 656cd997acbe..cacde7f4a547 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -343,8 +343,12 @@ public final class NfcAdapter { */ public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence"; - /** @hide */ + /** + * @hide + * @removed + */ @SystemApi + @UnsupportedAppUsage public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 0x1; /** @hide */ @@ -418,7 +422,6 @@ public final class NfcAdapter { // Guarded by NfcAdapter.class static boolean sIsInitialized = false; static boolean sHasNfcFeature; - static boolean sHasBeamFeature; static boolean sHasCeFeature; // Final after first constructor, except for @@ -484,7 +487,7 @@ public final class NfcAdapter { * A callback to be invoked when the system successfully delivers your {@link NdefMessage} * to another device. * @see #setOnNdefPushCompleteCallback - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @deprecated this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated @@ -510,7 +513,7 @@ public final class NfcAdapter { * content currently visible to the user. Alternatively, you can call {@link * #setNdefPushMessage setNdefPushMessage()} if the {@link NdefMessage} always contains the * same data. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @deprecated this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated @@ -540,7 +543,7 @@ public final class NfcAdapter { /** - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @deprecated this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated @@ -616,7 +619,6 @@ public final class NfcAdapter { PackageManager pm; pm = context.getPackageManager(); sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC); - sHasBeamFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM); sHasCeFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF) @@ -1114,35 +1116,17 @@ public final class NfcAdapter { * @param uris an array of Uri(s) to push over Android Beam * @param activity activity for which the Uri(s) will be pushed * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public void setBeamPushUris(Uri[] uris, Activity activity) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } } - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - if (uris != null) { - for (Uri uri : uris) { - if (uri == null) throw new NullPointerException("Uri not " + - "allowed to be null"); - String scheme = uri.getScheme(); - if (scheme == null || (!scheme.equalsIgnoreCase("file") && - !scheme.equalsIgnoreCase("content"))) { - throw new IllegalArgumentException("URI needs to have " + - "either scheme file or scheme content"); - } - } - } - mNfcActivityManager.setNdefPushContentUri(activity, uris); } /** @@ -1202,23 +1186,17 @@ public final class NfcAdapter { * @param callback callback, or null to disable * @param activity activity for which the Uri(s) will be pushed * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } } - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushContentUriCallback(activity, callback); } /** @@ -1292,58 +1270,32 @@ public final class NfcAdapter { * to only register one at a time, and to do so in that activity's * {@link Activity#onCreate} * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public void setNdefPushMessage(NdefMessage message, Activity activity, Activity ... activities) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } - } - int targetSdkVersion = getSdkVersion(); - try { - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushMessage(activity, message, 0); - for (Activity a : activities) { - if (a == null) { - throw new NullPointerException("activities cannot contain null"); - } - mNfcActivityManager.setNdefPushMessage(a, message, 0); - } - } catch (IllegalStateException e) { - if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) { - // Less strict on old applications - just log the error - Log.e(TAG, "Cannot call API with Activity that has already " + - "been destroyed", e); - } else { - // Prevent new applications from making this mistake, re-throw - throw(e); - } } } /** * @hide + * @removed */ @SystemApi + @UnsupportedAppUsage public void setNdefPushMessage(NdefMessage message, Activity activity, int flags) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } } - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushMessage(activity, message, flags); } /** @@ -1411,57 +1363,21 @@ public final class NfcAdapter { * to only register one at a time, and to do so in that activity's * {@link Activity#onCreate} * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity, Activity ... activities) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } - } - int targetSdkVersion = getSdkVersion(); - try { - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushMessageCallback(activity, callback, 0); - for (Activity a : activities) { - if (a == null) { - throw new NullPointerException("activities cannot contain null"); - } - mNfcActivityManager.setNdefPushMessageCallback(a, callback, 0); - } - } catch (IllegalStateException e) { - if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) { - // Less strict on old applications - just log the error - Log.e(TAG, "Cannot call API with Activity that has already " + - "been destroyed", e); - } else { - // Prevent new applications from making this mistake, re-throw - throw(e); - } } } /** - * @hide - */ - @UnsupportedAppUsage - public void setNdefPushMessageCallback(CreateNdefMessageCallback callback, Activity activity, - int flags) { - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setNdefPushMessageCallback(activity, callback, flags); - } - - /** * Set a callback on successful Android Beam (TM). * * <p>This method may be called at any time before {@link Activity#onDestroy}, @@ -1498,41 +1414,17 @@ public final class NfcAdapter { * to only register one at a time, and to do so in that activity's * {@link Activity#onCreate} * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public void setOnNdefPushCompleteCallback(OnNdefPushCompleteCallback callback, Activity activity, Activity ... activities) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } - } - int targetSdkVersion = getSdkVersion(); - try { - if (activity == null) { - throw new NullPointerException("activity cannot be null"); - } - mNfcActivityManager.setOnNdefPushCompleteCallback(activity, callback); - for (Activity a : activities) { - if (a == null) { - throw new NullPointerException("activities cannot contain null"); - } - mNfcActivityManager.setOnNdefPushCompleteCallback(a, callback); - } - } catch (IllegalStateException e) { - if (targetSdkVersion < android.os.Build.VERSION_CODES.JELLY_BEAN) { - // Less strict on old applications - just log the error - Log.e(TAG, "Cannot call API with Activity that has already " + - "been destroyed", e); - } else { - // Prevent new applications from making this mistake, re-throw - throw(e); - } } } @@ -1715,46 +1607,18 @@ public final class NfcAdapter { * @param activity the current foreground Activity that has registered data to share * @return whether the Beam animation was successfully invoked * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated + @UnsupportedAppUsage public boolean invokeBeam(Activity activity) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return false; - } - } - if (activity == null) { - throw new NullPointerException("activity may not be null."); - } - enforceResumed(activity); - try { - sService.invokeBeam(); - return true; - } catch (RemoteException e) { - Log.e(TAG, "invokeBeam: NFC process has died."); - attemptDeadServiceRecovery(e); - return false; - } - } - - /** - * @hide - */ - public boolean invokeBeam(BeamShareData shareData) { - try { - Log.e(TAG, "invokeBeamInternal()"); - sService.invokeBeamInternal(shareData); - return true; - } catch (RemoteException e) { - Log.e(TAG, "invokeBeam: NFC process has died."); - attemptDeadServiceRecovery(e); - return false; } + return false; } /** @@ -1780,25 +1644,18 @@ public final class NfcAdapter { * * @param activity foreground activity * @param message a NDEF Message to push over NFC - * @throws IllegalStateException if the activity is not currently in the foreground - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated use {@link #setNdefPushMessage} instead + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. */ @Deprecated + @UnsupportedAppUsage public void enableForegroundNdefPush(Activity activity, NdefMessage message) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } } - if (activity == null || message == null) { - throw new NullPointerException(); - } - enforceResumed(activity); - mNfcActivityManager.setNdefPushMessage(activity, message, 0); } /** @@ -1817,27 +1674,18 @@ public final class NfcAdapter { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param activity the Foreground activity - * @throws IllegalStateException if the Activity has already been paused - * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated use {@link #setNdefPushMessage} instead + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable + * @removed this feature is removed. File sharing can work using other technology like + * Bluetooth. */ @Deprecated + @UnsupportedAppUsage public void disableForegroundNdefPush(Activity activity) { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return; - } } - if (activity == null) { - throw new NullPointerException(); - } - enforceResumed(activity); - mNfcActivityManager.setNdefPushMessage(activity, null, 0); - mNfcActivityManager.setNdefPushMessageCallback(activity, null, 0); - mNfcActivityManager.setOnNdefPushCompleteCallback(activity, null); } /** @@ -1971,40 +1819,26 @@ public final class NfcAdapter { * Enable NDEF Push feature. * <p>This API is for the Settings application. * @hide + * @removed */ @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @UnsupportedAppUsage public boolean enableNdefPush() { - if (!sHasNfcFeature) { - throw new UnsupportedOperationException(); - } - try { - return sService.enableNdefPush(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } + return false; } /** * Disable NDEF Push feature. * <p>This API is for the Settings application. * @hide + * @removed */ @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @UnsupportedAppUsage public boolean disableNdefPush() { - synchronized (NfcAdapter.class) { - if (!sHasNfcFeature) { - throw new UnsupportedOperationException(); - } - } - try { - return sService.disableNdefPush(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; - } + return false; } /** @@ -2030,26 +1864,18 @@ public final class NfcAdapter { * @see android.provider.Settings#ACTION_NFCSHARING_SETTINGS * @return true if NDEF Push feature is enabled * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. - * @deprecated this feature is deprecated. File sharing can work using other technology like + * @removed this feature is removed. File sharing can work using other technology like * Bluetooth. */ @java.lang.Deprecated - + @UnsupportedAppUsage public boolean isNdefPushEnabled() { synchronized (NfcAdapter.class) { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } - if (!sHasBeamFeature) { - return false; - } - } - try { - return sService.isNdefPushEnabled(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; } + return false; } /** @@ -2140,17 +1966,6 @@ public final class NfcAdapter { } /** - * @hide - */ - public void setP2pModes(int initiatorModes, int targetModes) { - try { - sService.setP2pModes(initiatorModes, targetModes); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } - } - - /** * Registers a new NFC unlock handler with the NFC service. * * <p />NFC unlock handlers are intended to unlock the keyguard in the presence of a trusted diff --git a/core/java/android/nfc/tech/IsoDep.java b/core/java/android/nfc/tech/IsoDep.java index 089b1599ec06..0ba0c5a8d13b 100644 --- a/core/java/android/nfc/tech/IsoDep.java +++ b/core/java/android/nfc/tech/IsoDep.java @@ -88,6 +88,7 @@ public final class IsoDep extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void setTimeout(int timeout) { try { @@ -106,6 +107,7 @@ public final class IsoDep extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public int getTimeout() { try { @@ -167,6 +169,7 @@ public final class IsoDep extends BasicTagTechnology { * @return response bytes received, will not be null * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or this operation is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field */ public byte[] transceive(byte[] data) throws IOException { return transceive(data, true); @@ -193,6 +196,7 @@ public final class IsoDep extends BasicTagTechnology { * support. * * @return whether the NFC adapter on this device supports extended length APDUs. + * @throws SecurityException if the tag object is reused after the tag has left the field */ public boolean isExtendedLengthApduSupported() { try { diff --git a/core/java/android/nfc/tech/MifareClassic.java b/core/java/android/nfc/tech/MifareClassic.java index 080e058737b6..26f54e692289 100644 --- a/core/java/android/nfc/tech/MifareClassic.java +++ b/core/java/android/nfc/tech/MifareClassic.java @@ -597,6 +597,7 @@ public final class MifareClassic extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void setTimeout(int timeout) { try { @@ -615,6 +616,7 @@ public final class MifareClassic extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public int getTimeout() { try { diff --git a/core/java/android/nfc/tech/MifareUltralight.java b/core/java/android/nfc/tech/MifareUltralight.java index dec2c6548ab5..c0416a39ba76 100644 --- a/core/java/android/nfc/tech/MifareUltralight.java +++ b/core/java/android/nfc/tech/MifareUltralight.java @@ -236,6 +236,7 @@ public final class MifareUltralight extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void setTimeout(int timeout) { try { @@ -255,6 +256,7 @@ public final class MifareUltralight extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public int getTimeout() { try { diff --git a/core/java/android/nfc/tech/Ndef.java b/core/java/android/nfc/tech/Ndef.java index 225636565480..bb750cfbd82d 100644 --- a/core/java/android/nfc/tech/Ndef.java +++ b/core/java/android/nfc/tech/Ndef.java @@ -261,6 +261,7 @@ public final class Ndef extends BasicTagTechnology { * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or the operation is canceled * @throws FormatException if the NDEF Message on the tag is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field */ public NdefMessage getNdefMessage() throws IOException, FormatException { checkConnected(); @@ -301,6 +302,7 @@ public final class Ndef extends BasicTagTechnology { * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or the operation is canceled * @throws FormatException if the NDEF Message to write is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void writeNdefMessage(NdefMessage msg) throws IOException, FormatException { checkConnected(); @@ -339,6 +341,7 @@ public final class Ndef extends BasicTagTechnology { * <p>Does not cause any RF activity and does not block. * * @return true if it is possible to make this tag read-only + * @throws SecurityException if the tag object is reused after the tag has left the field */ public boolean canMakeReadOnly() { INfcTag tagService = mTag.getTagService(); @@ -370,6 +373,7 @@ public final class Ndef extends BasicTagTechnology { * @return true on success, false if it is not possible to make this tag read-only * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or the operation is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field */ public boolean makeReadOnly() throws IOException { checkConnected(); diff --git a/core/java/android/nfc/tech/NdefFormatable.java b/core/java/android/nfc/tech/NdefFormatable.java index 4175cd08804c..f19d30258393 100644 --- a/core/java/android/nfc/tech/NdefFormatable.java +++ b/core/java/android/nfc/tech/NdefFormatable.java @@ -111,6 +111,7 @@ public final class NdefFormatable extends BasicTagTechnology { * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or the operation is canceled * @throws FormatException if the NDEF Message to write is malformed + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void formatReadOnly(NdefMessage firstMessage) throws IOException, FormatException { format(firstMessage, true); diff --git a/core/java/android/nfc/tech/NfcA.java b/core/java/android/nfc/tech/NfcA.java index 88730f9af3df..7e6648361670 100644 --- a/core/java/android/nfc/tech/NfcA.java +++ b/core/java/android/nfc/tech/NfcA.java @@ -141,6 +141,7 @@ public final class NfcA extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void setTimeout(int timeout) { try { @@ -159,6 +160,7 @@ public final class NfcA extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public int getTimeout() { try { diff --git a/core/java/android/nfc/tech/NfcF.java b/core/java/android/nfc/tech/NfcF.java index 44871212daef..2ccd38875f07 100644 --- a/core/java/android/nfc/tech/NfcF.java +++ b/core/java/android/nfc/tech/NfcF.java @@ -145,6 +145,7 @@ public final class NfcF extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param timeout timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void setTimeout(int timeout) { try { @@ -163,6 +164,7 @@ public final class NfcF extends BasicTagTechnology { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @return timeout value in milliseconds + * @throws SecurityException if the tag object is reused after the tag has left the field */ public int getTimeout() { try { diff --git a/core/java/android/nfc/tech/TagTechnology.java b/core/java/android/nfc/tech/TagTechnology.java index 0e2c7c18c61c..839fe429b338 100644 --- a/core/java/android/nfc/tech/TagTechnology.java +++ b/core/java/android/nfc/tech/TagTechnology.java @@ -176,6 +176,7 @@ public interface TagTechnology extends Closeable { * @see #close() * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or connect is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void connect() throws IOException; @@ -193,6 +194,7 @@ public interface TagTechnology extends Closeable { * @see #close() * @throws TagLostException if the tag leaves the field * @throws IOException if there is an I/O failure, or connect is canceled + * @throws SecurityException if the tag object is reused after the tag has left the field * @hide */ public void reconnect() throws IOException; @@ -205,6 +207,7 @@ public interface TagTechnology extends Closeable { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @see #connect() + * @throws SecurityException if the tag object is reused after the tag has left the field */ public void close() throws IOException; diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 735a0689be5e..244632a87593 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -806,12 +806,9 @@ public class Build { * PackageManager.setComponentEnabledSetting} will now throw an * IllegalArgumentException if the given component class name does not * exist in the application's manifest. - * <li> {@link android.nfc.NfcAdapter#setNdefPushMessage - * NfcAdapter.setNdefPushMessage}, - * {@link android.nfc.NfcAdapter#setNdefPushMessageCallback - * NfcAdapter.setNdefPushMessageCallback} and - * {@link android.nfc.NfcAdapter#setOnNdefPushCompleteCallback - * NfcAdapter.setOnNdefPushCompleteCallback} will throw + * <li> {@code NfcAdapter.setNdefPushMessage}, + * {@code NfcAdapter.setNdefPushMessageCallback} and + * {@code NfcAdapter.setOnNdefPushCompleteCallback} will throw * IllegalStateException if called after the Activity has been destroyed. * <li> Accessibility services must require the new * {@link android.Manifest.permission#BIND_ACCESSIBILITY_SERVICE} permission or diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java index 221e89a6a76f..310ceb3aeb91 100644 --- a/core/java/android/os/PermissionEnforcer.java +++ b/core/java/android/os/PermissionEnforcer.java @@ -18,9 +18,11 @@ package android.os; import android.annotation.NonNull; import android.annotation.SystemService; +import android.app.AppOpsManager; import android.content.AttributionSource; import android.content.Context; import android.content.PermissionChecker; +import android.content.pm.PackageManager; import android.permission.PermissionCheckerManager; /** @@ -40,6 +42,7 @@ import android.permission.PermissionCheckerManager; public class PermissionEnforcer { private final Context mContext; + private static final String ACCESS_DENIED = "Access denied, requires: "; /** Protected constructor. Allows subclasses to instantiate an object * without using a Context. @@ -59,11 +62,42 @@ public class PermissionEnforcer { mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */); } + @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck") + @PermissionCheckerManager.PermissionResult + protected int checkPermission(@NonNull String permission, int pid, int uid) { + if (mContext.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) { + return PermissionCheckerManager.PERMISSION_GRANTED; + } + return PermissionCheckerManager.PERMISSION_HARD_DENIED; + } + + private boolean anyAppOps(@NonNull String[] permissions) { + for (String permission : permissions) { + if (AppOpsManager.permissionToOpCode(permission) != AppOpsManager.OP_NONE) { + return true; + } + } + return false; + } + public void enforcePermission(@NonNull String permission, @NonNull AttributionSource source) throws SecurityException { int result = checkPermission(permission, source); if (result != PermissionCheckerManager.PERMISSION_GRANTED) { - throw new SecurityException("Access denied, requires: " + permission); + throw new SecurityException(ACCESS_DENIED + permission); + } + } + + public void enforcePermission(@NonNull String permission, int pid, int uid) + throws SecurityException { + if (AppOpsManager.permissionToOpCode(permission) != AppOpsManager.OP_NONE) { + AttributionSource source = new AttributionSource(uid, null, null); + enforcePermission(permission, source); + return; + } + int result = checkPermission(permission, pid, uid); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException(ACCESS_DENIED + permission); } } @@ -72,7 +106,23 @@ public class PermissionEnforcer { for (String permission : permissions) { int result = checkPermission(permission, source); if (result != PermissionCheckerManager.PERMISSION_GRANTED) { - throw new SecurityException("Access denied, requires: allOf={" + throw new SecurityException(ACCESS_DENIED + "allOf={" + + String.join(", ", permissions) + "}"); + } + } + } + + public void enforcePermissionAllOf(@NonNull String[] permissions, + int pid, int uid) throws SecurityException { + if (anyAppOps(permissions)) { + AttributionSource source = new AttributionSource(uid, null, null); + enforcePermissionAllOf(permissions, source); + return; + } + for (String permission : permissions) { + int result = checkPermission(permission, pid, uid); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException(ACCESS_DENIED + "allOf={" + String.join(", ", permissions) + "}"); } } @@ -86,7 +136,24 @@ public class PermissionEnforcer { return; } } - throw new SecurityException("Access denied, requires: anyOf={" + throw new SecurityException(ACCESS_DENIED + "anyOf={" + + String.join(", ", permissions) + "}"); + } + + public void enforcePermissionAnyOf(@NonNull String[] permissions, + int pid, int uid) throws SecurityException { + if (anyAppOps(permissions)) { + AttributionSource source = new AttributionSource(uid, null, null); + enforcePermissionAnyOf(permissions, source); + return; + } + for (String permission : permissions) { + int result = checkPermission(permission, pid, uid); + if (result == PermissionCheckerManager.PERMISSION_GRANTED) { + return; + } + } + throw new SecurityException(ACCESS_DENIED + "anyOf={" + String.join(", ", permissions) + "}"); } diff --git a/core/java/android/os/RemoteException.java b/core/java/android/os/RemoteException.java index 878e1413a8c9..970f4192c36c 100644 --- a/core/java/android/os/RemoteException.java +++ b/core/java/android/os/RemoteException.java @@ -21,6 +21,12 @@ import android.util.AndroidException; /** * Parent exception for all Binder remote-invocation errors + * + * Note: not all exceptions from binder services will be subclasses of this. + * For instance, RuntimeException and several subclasses of it may be + * thrown as well as OutOfMemoryException. + * + * One common subclass is {@link DeadObjectException}. */ public class RemoteException extends AndroidException { public RemoteException() { diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 3c1b4ba8f8b5..0012572540ae 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -8410,7 +8410,7 @@ public final class ContactsContract { extras.putString(KEY_ACCOUNT_NAME, accountName); extras.putString(KEY_ACCOUNT_TYPE, accountType); - contentResolver.call(ContactsContract.AUTHORITY_URI, + nullSafeCall(contentResolver, ContactsContract.AUTHORITY_URI, ContactsContract.SimContacts.ADD_SIM_ACCOUNT_METHOD, null, extras); } @@ -8433,7 +8433,7 @@ public final class ContactsContract { Bundle extras = new Bundle(); extras.putInt(KEY_SIM_SLOT_INDEX, simSlotIndex); - contentResolver.call(ContactsContract.AUTHORITY_URI, + nullSafeCall(contentResolver, ContactsContract.AUTHORITY_URI, ContactsContract.SimContacts.REMOVE_SIM_ACCOUNT_METHOD, null, extras); } @@ -8445,7 +8445,7 @@ public final class ContactsContract { */ public static @NonNull List<SimAccount> getSimAccounts( @NonNull ContentResolver contentResolver) { - Bundle response = contentResolver.call(ContactsContract.AUTHORITY_URI, + Bundle response = nullSafeCall(contentResolver, ContactsContract.AUTHORITY_URI, ContactsContract.SimContacts.QUERY_SIM_ACCOUNTS_METHOD, null, null); List<SimAccount> result = response.getParcelableArrayList(KEY_SIM_ACCOUNTS); @@ -9064,7 +9064,8 @@ public final class ContactsContract { * @param contactId the id of the contact to undemote. */ public static void undemote(ContentResolver contentResolver, long contactId) { - contentResolver.call(ContactsContract.AUTHORITY_URI, PinnedPositions.UNDEMOTE_METHOD, + nullSafeCall(contentResolver, ContactsContract.AUTHORITY_URI, + PinnedPositions.UNDEMOTE_METHOD, String.valueOf(contactId), null); } @@ -10276,4 +10277,13 @@ public final class ContactsContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata_sync_state"; } + + private static Bundle nullSafeCall(@NonNull ContentResolver resolver, @NonNull Uri uri, + @NonNull String method, @Nullable String arg, @Nullable Bundle extras) { + try (ContentProviderClient client = resolver.acquireContentProviderClient(uri)) { + return client.call(method, arg, extras); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 85e3aee02b40..fe0c1dd65bef 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1596,7 +1596,6 @@ public final class Settings { * Input: Nothing. * <p> * Output: Nothing - * @see android.nfc.NfcAdapter#isNdefPushEnabled() */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_NFCSHARING_SETTINGS = diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 3dc805e03f9d..bb76b469e96d 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -4842,6 +4842,14 @@ public final class Telephony { */ public static final String COLUMN_USER_HANDLE = "user_handle"; + /** + * TelephonyProvider column name for satellite enabled. + * By default, it's disabled. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENABLED = "satellite_enabled"; + /** All columns in {@link SimInfo} table. */ private static final List<String> ALL_COLUMNS = List.of( COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, @@ -4910,7 +4918,8 @@ public final class Telephony { COLUMN_PORT_INDEX, COLUMN_USAGE_SETTING, COLUMN_TP_MESSAGE_REF, - COLUMN_USER_HANDLE + COLUMN_USER_HANDLE, + COLUMN_SATELLITE_ENABLED ); /** diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java index 13f7e5d4232b..3a254c1d92fc 100644 --- a/core/java/android/security/net/config/SystemCertificateSource.java +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -39,9 +39,13 @@ public final class SystemCertificateSource extends DirectoryCertificateSource { } private static File getDirectory() { - // TODO(miguelaranda): figure out correct code path. + if ((System.getProperty("system.certs.enabled") != null) + && (System.getProperty("system.certs.enabled")).equals("true")) { + return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); + } File updatable_dir = new File("/apex/com.android.conscrypt/cacerts"); - if (updatable_dir.exists() && !(updatable_dir.list().length == 0)) { + if (updatable_dir.exists() + && !(updatable_dir.list().length == 0)) { return updatable_dir; } return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index 6d640229b3e6..a3c278f487fc 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -15,6 +15,10 @@ siyamed@google.com # Autofill per-file ViewStructure.java = file:/core/java/android/service/autofill/OWNERS +# Choreographer +per-file Choreographer.java = file:platform/frameworks/native:/services/surfaceflinger/OWNERS +per-file DisplayEventReceiver.java = file:platform/frameworks/native:/services/surfaceflinger/OWNERS + # Display per-file Display*.java = file:/services/core/java/com/android/server/display/OWNERS per-file Display*.aidl = file:/services/core/java/com/android/server/display/OWNERS @@ -56,6 +60,9 @@ per-file SurfaceView.java = file:/services/core/java/com/android/server/wm/OWNER per-file SurfaceHolder.java = file:/graphics/java/android/graphics/OWNERS per-file SurfaceHolder.java = file:/services/core/java/com/android/server/wm/OWNERS +# Text +per-file HandwritingInitiator.java = file:/core/java/android/text/OWNERS + # View per-file View.java = file:/services/accessibility/OWNERS per-file View.java = file:/core/java/android/service/autofill/OWNERS diff --git a/core/java/android/widget/OWNERS b/core/java/android/widget/OWNERS index 02dafd44d3bd..7f0a651c6420 100644 --- a/core/java/android/widget/OWNERS +++ b/core/java/android/widget/OWNERS @@ -13,3 +13,5 @@ per-file TextView*,Edit*,Selection* = file:../text/OWNERS per-file SpellChecker.java = file:../view/inputmethod/OWNERS per-file RemoteViews* = file:../appwidget/OWNERS + +per-file Toast.java = juliacr@google.com, jeffdq@google.com diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index ca57c84a1631..fceee4e01799 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -189,6 +189,9 @@ public class Toast { /** * Show the view for the specified duration. + * + * <p>Note that toasts being sent from the background are rate limited, so avoid sending such + * toasts in quick succession. */ public void show() { if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) { diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS index 0d02683868c5..a1d571fc4350 100644 --- a/core/java/com/android/internal/app/OWNERS +++ b/core/java/com/android/internal/app/OWNERS @@ -1,4 +1,6 @@ per-file *AppOp* = file:/core/java/android/permission/OWNERS +per-file UnlaunchableAppActivity.java = file:/core/java/android/app/admin/WorkProfile_OWNERS +per-file IntentForwarderActivity.java = file:/core/java/android/app/admin/WorkProfile_OWNERS per-file *Resolver* = file:/packages/SystemUI/OWNERS per-file *Chooser* = file:/packages/SystemUI/OWNERS per-file SimpleIconFactory.java = file:/packages/SystemUI/OWNERS diff --git a/core/java/com/android/internal/expresslog/Counter.java b/core/java/com/android/internal/expresslog/Counter.java index afdbdc8ead73..4a46d91efbf0 100644 --- a/core/java/com/android/internal/expresslog/Counter.java +++ b/core/java/com/android/internal/expresslog/Counter.java @@ -36,6 +36,16 @@ public final class Counter { } /** + * Increments Telemetry Express Counter metric by 1 + * @param metricId to log, no-op if metricId is not defined in the TeX catalog + * @param uid used as a dimension for the count metric + * @hide + */ + public static void logIncrementWithUid(@NonNull String metricId, int uid) { + logIncrementWithUid(metricId, uid, 1); + } + + /** * Increments Telemetry Express Counter metric by arbitrary value * @param metricId to log, no-op if metricId is not defined in the TeX catalog * @param amount to increment counter @@ -45,4 +55,17 @@ public final class Counter { final long metricIdHash = Utils.hashString(metricId); FrameworkStatsLog.write(FrameworkStatsLog.EXPRESS_EVENT_REPORTED, metricIdHash, amount); } + + /** + * Increments Telemetry Express Counter metric by arbitrary value + * @param metricId to log, no-op if metricId is not defined in the TeX catalog + * @param uid used as a dimension for the count metric + * @param amount to increment counter + * @hide + */ + public static void logIncrementWithUid(@NonNull String metricId, int uid, long amount) { + final long metricIdHash = Utils.hashString(metricId); + FrameworkStatsLog.write( + FrameworkStatsLog.EXPRESS_UID_EVENT_REPORTED, metricIdHash, amount, uid); + } } diff --git a/core/java/com/android/internal/expresslog/Histogram.java b/core/java/com/android/internal/expresslog/Histogram.java index 2f3b662af6a7..2fe784a5a855 100644 --- a/core/java/com/android/internal/expresslog/Histogram.java +++ b/core/java/com/android/internal/expresslog/Histogram.java @@ -16,10 +16,14 @@ package com.android.internal.expresslog; +import android.annotation.FloatRange; +import android.annotation.IntRange; import android.annotation.NonNull; import com.android.internal.util.FrameworkStatsLog; +import java.util.Arrays; + /** Histogram encapsulates StatsD write API calls */ public final class Histogram { @@ -28,7 +32,8 @@ public final class Histogram { /** * Creates Histogram metric logging wrapper - * @param metricId to log, logging will be no-op if metricId is not defined in the TeX catalog + * + * @param metricId to log, logging will be no-op if metricId is not defined in the TeX catalog * @param binOptions to calculate bin index for samples * @hide */ @@ -39,6 +44,7 @@ public final class Histogram { /** * Logs increment sample count for automatically calculated bin + * * @param sample value * @hide */ @@ -48,10 +54,24 @@ public final class Histogram { /*count*/ 1, binIndex); } + /** + * Logs increment sample count for automatically calculated bin + * + * @param uid used as a dimension for the count metric + * @param sample value + * @hide + */ + public void logSampleWithUid(int uid, float sample) { + final int binIndex = mBinOptions.getBinForSample(sample); + FrameworkStatsLog.write(FrameworkStatsLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED, + mMetricIdHash, /*count*/ 1, binIndex, uid); + } + /** Used by Histogram to map data sample to corresponding bin */ public interface BinOptions { /** * Returns bins count to be used by a histogram + * * @return bins count used to initialize Options, including overflow & underflow bins * @hide */ @@ -61,6 +81,7 @@ public final class Histogram { * Returns bin index for the input sample value * index == 0 stands for underflow * index == getBinsCount() - 1 stands for overflow + * * @return zero based index * @hide */ @@ -76,17 +97,19 @@ public final class Histogram { private final float mBinSize; /** - * Creates otpions for uniform (linear) sized bins - * @param binCount amount of histogram bins. 2 bin indexes will be calculated - * automatically to represent undeflow & overflow bins - * @param minValue is included in the first bin, values less than minValue - * go to underflow bin + * Creates options for uniform (linear) sized bins + * + * @param binCount amount of histogram bins. 2 bin indexes will be calculated + * automatically to represent underflow & overflow bins + * @param minValue is included in the first bin, values less than minValue + * go to underflow bin * @param exclusiveMaxValue is included in the overflow bucket. For accurate - measure up to kMax, then exclusiveMaxValue + * measure up to kMax, then exclusiveMaxValue * should be set to kMax + 1 * @hide */ - public UniformOptions(int binCount, float minValue, float exclusiveMaxValue) { + public UniformOptions(@IntRange(from = 1) int binCount, float minValue, + float exclusiveMaxValue) { if (binCount < 1) { throw new IllegalArgumentException("Bin count should be positive number"); } @@ -99,7 +122,7 @@ public final class Histogram { mExclusiveMaxValue = exclusiveMaxValue; mBinSize = (mExclusiveMaxValue - minValue) / binCount; - // Implicitly add 2 for the extra undeflow & overflow bins + // Implicitly add 2 for the extra underflow & overflow bins mBinCount = binCount + 2; } @@ -120,4 +143,92 @@ public final class Histogram { return (int) ((sample - mMinValue) / mBinSize + 1); } } + + /** Used by Histogram to map data sample to corresponding bin for scaled bins */ + public static final class ScaledRangeOptions implements BinOptions { + // store minimum value per bin + final long[] mBins; + + /** + * Creates options for scaled range bins + * + * @param binCount amount of histogram bins. 2 bin indexes will be calculated + * automatically to represent underflow & overflow bins + * @param minValue is included in the first bin, values less than minValue + * go to underflow bin + * @param firstBinWidth used to represent first bin width and as a reference to calculate + * width for consecutive bins + * @param scaleFactor used to calculate width for consecutive bins + * @hide + */ + public ScaledRangeOptions(@IntRange(from = 1) int binCount, int minValue, + @FloatRange(from = 1.f) float firstBinWidth, + @FloatRange(from = 1.f) float scaleFactor) { + if (binCount < 1) { + throw new IllegalArgumentException("Bin count should be positive number"); + } + + if (firstBinWidth < 1.f) { + throw new IllegalArgumentException( + "First bin width invalid (should be 1.f at minimum)"); + } + + if (scaleFactor < 1.f) { + throw new IllegalArgumentException( + "Scaled factor invalid (should be 1.f at minimum)"); + } + + // precalculating bins ranges (no need to create a bin for underflow reference value) + mBins = initBins(binCount + 1, minValue, firstBinWidth, scaleFactor); + } + + @Override + public int getBinsCount() { + return mBins.length + 1; + } + + @Override + public int getBinForSample(float sample) { + if (sample < mBins[0]) { + // goes to underflow + return 0; + } else if (sample >= mBins[mBins.length - 1]) { + // goes to overflow + return mBins.length; + } + + return lower_bound(mBins, (long) sample) + 1; + } + + // To find lower bound using binary search implementation of Arrays utility class + private static int lower_bound(long[] array, long sample) { + int index = Arrays.binarySearch(array, sample); + // If key is not present in the array + if (index < 0) { + // Index specify the position of the key when inserted in the sorted array + // so the element currently present at this position will be the lower bound + return Math.abs(index) - 2; + } + return index; + } + + private static long[] initBins(int count, int minValue, float firstBinWidth, + float scaleFactor) { + long[] bins = new long[count]; + bins[0] = minValue; + double lastWidth = firstBinWidth; + for (int i = 1; i < count; i++) { + // current bin minValue = previous bin width * scaleFactor + double currentBinMinValue = bins[i - 1] + lastWidth; + if (currentBinMinValue > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Attempted to create a bucket larger than maxint"); + } + + bins[i] = (long) currentBinMinValue; + lastWidth *= scaleFactor; + } + return bins; + } + } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 1505ccce97a1..85cb15bdd906 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -721,7 +721,8 @@ public class ZygoteInit { } catch (ErrnoException ex) { throw new RuntimeException("Failed to capget()", ex); } - capabilities &= ((long) data[0].effective) | (((long) data[1].effective) << 32); + capabilities &= Integer.toUnsignedLong(data[0].effective) | + (Integer.toUnsignedLong(data[1].effective) << 32); /* Hardcoded command line to start the system server */ String[] args = { diff --git a/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS b/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS new file mode 100644 index 000000000000..77550002e3c6 --- /dev/null +++ b/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS @@ -0,0 +1,3 @@ +# TODO(b/274465475): Migrate LatencyTracker testing to its own module +marcinoc@google.com +ilkos@google.com diff --git a/core/java/com/android/internal/util/OWNERS b/core/java/com/android/internal/util/OWNERS index 1808bd56b562..9be8ea7aadc4 100644 --- a/core/java/com/android/internal/util/OWNERS +++ b/core/java/com/android/internal/util/OWNERS @@ -6,3 +6,4 @@ per-file Protocol* = etancohen@google.com, lorenzo@google.com per-file State* = jchalard@google.com, lorenzo@google.com, satk@google.com per-file *Dump* = file:/core/java/com/android/internal/util/dump/OWNERS per-file *Screenshot* = file:/packages/SystemUI/src/com/android/systemui/screenshot/OWNERS +per-file *LatencyTracker* = file:/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS diff --git a/core/jni/OWNERS b/core/jni/OWNERS index c99a27a6a257..1e7a93c8e812 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -5,6 +5,9 @@ per-file *Camera*,*camera* = shuzhenwang@google.com, yinchiayeh@google.com, zhij # Connectivity per-file android_net_* = codewiz@google.com, jchalard@google.com, lorenzo@google.com, reminv@google.com, satk@google.com +# Choreographer +per-file android_view_DisplayEventReceiver* = file:platform/frameworks/native:/services/surfaceflinger/OWNERS + # CPU per-file *Cpu* = file:/core/java/com/android/internal/os/CPU_OWNERS diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index a7c7d0ba35bc..b24dc8a9e63b 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -413,9 +413,13 @@ protected: if (env->ExceptionCheck()) { ScopedLocalRef<jthrowable> excep(env, env->ExceptionOccurred()); - binder_report_exception(env, excep.get(), - "*** Uncaught remote exception! " - "(Exceptions are not yet supported across processes.)"); + + auto state = IPCThreadState::self(); + String8 msg; + msg.appendFormat("*** Uncaught remote exception! Exceptions are not yet supported " + "across processes. Client PID %d UID %d.", + state->getCallingPid(), state->getCallingUid()); + binder_report_exception(env, excep.get(), msg.c_str()); res = JNI_FALSE; } @@ -431,6 +435,7 @@ protected: ScopedLocalRef<jthrowable> excep(env, env->ExceptionOccurred()); binder_report_exception(env, excep.get(), "*** Uncaught exception in onBinderStrictModePolicyChange"); + // TODO: should turn this to fatal? } // Need to always call through the native implementation of diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 57a196fa3d0c..a5d979ceade6 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -2296,7 +2296,9 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, // region shared with the child process we reduce the number of pages that // transition to the private-dirty state when malloc adjusts the meta-data // on each of the pages it is managing after the fork. - mallopt(M_PURGE, 0); + if (mallopt(M_PURGE_ALL, 0) != 1) { + mallopt(M_PURGE, 0); + } } pid_t pid = fork(); diff --git a/core/proto/android/nfc/nfc_service.proto b/core/proto/android/nfc/nfc_service.proto index 2df1d5d210da..1dcd5cc210d9 100644 --- a/core/proto/android/nfc/nfc_service.proto +++ b/core/proto/android/nfc/nfc_service.proto @@ -60,7 +60,7 @@ message NfcServiceDumpProto { optional bool secure_nfc_capable = 13; optional bool vr_mode_enabled = 14; optional DiscoveryParamsProto discovery_params = 15; - optional P2pLinkManagerProto p2p_link_manager = 16; + reserved 16; optional com.android.nfc.cardemulation.CardEmulationManagerProto card_emulation_manager = 17; optional NfcDispatcherProto nfc_dispatcher = 18; optional string native_crash_logs = 19 [(.android.privacy).dest = DEST_EXPLICIT]; @@ -77,38 +77,6 @@ message DiscoveryParamsProto { optional bool enable_p2p = 5; } -// Debugging information for com.android.nfc.P2pLinkManager -message P2pLinkManagerProto { - option (.android.msg_privacy).dest = DEST_AUTOMATIC; - - enum LinkState { - LINK_STATE_UNKNOWN = 0; - LINK_STATE_DOWN = 1; - LINK_STATE_DEBOUNCE = 2; - LINK_STATE_UP = 3; - } - - enum SendState { - SEND_STATE_UNKNOWN = 0; - SEND_STATE_NOTHING_TO_SEND = 1; - SEND_STATE_NEED_CONFIRMATION = 2; - SEND_STATE_SENDING = 3; - SEND_STATE_COMPLETE = 4; - SEND_STATE_CANCELED = 5; - } - - optional int32 default_miu = 1; - optional int32 default_rw_size = 2; - optional LinkState link_state = 3; - optional SendState send_state = 4; - optional int32 send_flags = 5; - optional bool send_enabled = 6; - optional bool receive_enabled = 7; - optional string callback_ndef = 8 [(.android.privacy).dest = DEST_EXPLICIT]; - optional .android.nfc.NdefMessageProto message_to_send = 9; - repeated string uris_to_send = 10 [(.android.privacy).dest = DEST_EXPLICIT]; -} - // Debugging information for com.android.nfc.NfcDispatcher message NfcDispatcherProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a5e96176ec4c..277bdf388188 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1334,8 +1334,7 @@ android:permissionFlags="hardRestricted" android:protectionLevel="dangerous" /> - <!-- Allows an application to write (but not read) the user's - call log data. + <!-- Allows an application to write and read the user's call log data. <p class="note"><strong>Note:</strong> If your app uses the {@link #WRITE_CONTACTS} permission and <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 94cf1b2ada0e..504492b8ffcf 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -122,4 +122,10 @@ the performance, but sync mode reduces the chance of database/cache out-of-sync. --> <bool name="config_subscription_database_async_update">true</bool> <java-symbol type="bool" name="config_subscription_database_async_update" /> + + <!-- Whether to enable getSubscriptionUserHandle() api. + If the value is true, return user handle associated with the subscription. + If the value is set to false, return null. --> + <bool name="config_enable_get_subscription_user_handle">true</bool> + <java-symbol type="bool" name="config_enable_get_subscription_user_handle" /> </resources> diff --git a/core/tests/coretests/src/com/android/internal/util/OWNERS b/core/tests/coretests/src/com/android/internal/util/OWNERS index d83274560315..dda11fb9a576 100644 --- a/core/tests/coretests/src/com/android/internal/util/OWNERS +++ b/core/tests/coretests/src/com/android/internal/util/OWNERS @@ -1,2 +1,3 @@ per-file *Notification* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *ContrastColor* = file:/services/core/java/com/android/server/notification/OWNERS
\ No newline at end of file +per-file *ContrastColor* = file:/services/core/java/com/android/server/notification/OWNERS +per-file *LatencyTracker* = file:/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS diff --git a/core/tests/coretests/testdoubles/OWNERS b/core/tests/coretests/testdoubles/OWNERS new file mode 100644 index 000000000000..baf92ec067c3 --- /dev/null +++ b/core/tests/coretests/testdoubles/OWNERS @@ -0,0 +1 @@ +per-file *LatencyTracker* = file:/core/java/com/android/internal/util/LATENCY_TRACKER_OWNERS diff --git a/core/tests/expresslog/src/com/android/internal/expresslog/ScaledRangeOptionsTest.java b/core/tests/expresslog/src/com/android/internal/expresslog/ScaledRangeOptionsTest.java new file mode 100644 index 000000000000..ee62d7528818 --- /dev/null +++ b/core/tests/expresslog/src/com/android/internal/expresslog/ScaledRangeOptionsTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 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.expresslog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +@SmallTest +public class ScaledRangeOptionsTest { + private static final String TAG = ScaledRangeOptionsTest.class.getSimpleName(); + + @Test + public void testGetBinsCount() { + Histogram.ScaledRangeOptions options1 = new Histogram.ScaledRangeOptions(1, 100, 100, 2); + assertEquals(3, options1.getBinsCount()); + + Histogram.ScaledRangeOptions options10 = new Histogram.ScaledRangeOptions(10, 100, 100, 2); + assertEquals(12, options10.getBinsCount()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructZeroBinsCount() { + new Histogram.ScaledRangeOptions(0, 100, 100, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructNegativeBinsCount() { + new Histogram.ScaledRangeOptions(-1, 100, 100, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructNegativeFirstBinWidth() { + new Histogram.ScaledRangeOptions(10, 100, -100, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructTooSmallFirstBinWidth() { + new Histogram.ScaledRangeOptions(10, 100, 0.5f, 2); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructNegativeScaleFactor() { + new Histogram.ScaledRangeOptions(10, 100, 100, -2); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructTooSmallScaleFactor() { + new Histogram.ScaledRangeOptions(10, 100, 100, 0.5f); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructTooBigScaleFactor() { + new Histogram.ScaledRangeOptions(10, 100, 100, 500.f); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructTooBigBinRange() { + new Histogram.ScaledRangeOptions(100, 100, 100, 10.f); + } + + @Test + public void testBinIndexForRangeEqual1() { + Histogram.ScaledRangeOptions options = new Histogram.ScaledRangeOptions(10, 1, 1, 1); + assertEquals(12, options.getBinsCount()); + + assertEquals(11, options.getBinForSample(11)); + + for (int i = 0, bins = options.getBinsCount(); i < bins; i++) { + assertEquals(i, options.getBinForSample(i)); + } + } + + @Test + public void testBinIndexForRangeEqual2() { + // this should produce bin otpions similar to linear histogram with bin width 2 + Histogram.ScaledRangeOptions options = new Histogram.ScaledRangeOptions(10, 1, 2, 1); + assertEquals(12, options.getBinsCount()); + + for (int i = 0, bins = options.getBinsCount(); i < bins; i++) { + assertEquals(i, options.getBinForSample(i * 2)); + assertEquals(i, options.getBinForSample(i * 2 - 1)); + } + } + + @Test + public void testBinIndexForRangeEqual5() { + Histogram.ScaledRangeOptions options = new Histogram.ScaledRangeOptions(2, 0, 5, 1); + assertEquals(4, options.getBinsCount()); + for (int i = 0; i < 2; i++) { + for (int sample = 0; sample < 5; sample++) { + assertEquals(i + 1, options.getBinForSample(i * 5 + sample)); + } + } + } + + @Test + public void testBinIndexForRangeEqual10() { + Histogram.ScaledRangeOptions options = new Histogram.ScaledRangeOptions(10, 1, 10, 1); + assertEquals(0, options.getBinForSample(0)); + assertEquals(options.getBinsCount() - 2, options.getBinForSample(100)); + assertEquals(options.getBinsCount() - 1, options.getBinForSample(101)); + + final float binSize = (101 - 1) / 10f; + for (int i = 1, bins = options.getBinsCount() - 1; i < bins; i++) { + assertEquals(i, options.getBinForSample(i * binSize)); + } + } + + @Test + public void testBinIndexForScaleFactor2() { + final int binsCount = 10; + final int minValue = 10; + final int firstBinWidth = 5; + final int scaledFactor = 2; + + Histogram.ScaledRangeOptions options = new Histogram.ScaledRangeOptions( + binsCount, minValue, firstBinWidth, scaledFactor); + assertEquals(binsCount + 2, options.getBinsCount()); + long[] binCounts = new long[10]; + + // precalculate max valid value - start value for the overflow bin + int lastBinStartValue = minValue; //firstBinMin value + int lastBinWidth = firstBinWidth; + for (int binIdx = 2; binIdx <= binsCount + 1; binIdx++) { + lastBinStartValue = lastBinStartValue + lastBinWidth; + lastBinWidth *= scaledFactor; + } + + // underflow bin + for (int i = 1; i < minValue; i++) { + assertEquals(0, options.getBinForSample(i)); + } + + for (int i = 10; i < lastBinStartValue; i++) { + assertTrue(options.getBinForSample(i) > 0); + assertTrue(options.getBinForSample(i) <= binsCount); + binCounts[options.getBinForSample(i) - 1]++; + } + + // overflow bin + assertEquals(binsCount + 1, options.getBinForSample(lastBinStartValue)); + + for (int i = 1; i < binsCount; i++) { + assertEquals(binCounts[i], binCounts[i - 1] * 2L); + } + } +} diff --git a/core/tests/expresslog/src/com/android/internal/expresslog/UniformOptionsTest.java b/core/tests/expresslog/src/com/android/internal/expresslog/UniformOptionsTest.java index 9fa6d0634fbe..037dbb32c2f8 100644 --- a/core/tests/expresslog/src/com/android/internal/expresslog/UniformOptionsTest.java +++ b/core/tests/expresslog/src/com/android/internal/expresslog/UniformOptionsTest.java @@ -24,11 +24,11 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) +@SmallTest public class UniformOptionsTest { private static final String TAG = UniformOptionsTest.class.getSimpleName(); @Test - @SmallTest public void testGetBinsCount() { Histogram.UniformOptions options1 = new Histogram.UniformOptions(1, 100, 1000); assertEquals(3, options1.getBinsCount()); @@ -38,25 +38,21 @@ public class UniformOptionsTest { } @Test(expected = IllegalArgumentException.class) - @SmallTest public void testConstructZeroBinsCount() { new Histogram.UniformOptions(0, 100, 1000); } @Test(expected = IllegalArgumentException.class) - @SmallTest public void testConstructNegativeBinsCount() { new Histogram.UniformOptions(-1, 100, 1000); } @Test(expected = IllegalArgumentException.class) - @SmallTest public void testConstructMaxValueLessThanMinValue() { new Histogram.UniformOptions(10, 1000, 100); } @Test - @SmallTest public void testBinIndexForRangeEqual1() { Histogram.UniformOptions options = new Histogram.UniformOptions(10, 1, 11); for (int i = 0, bins = options.getBinsCount(); i < bins; i++) { @@ -65,7 +61,6 @@ public class UniformOptionsTest { } @Test - @SmallTest public void testBinIndexForRangeEqual2() { Histogram.UniformOptions options = new Histogram.UniformOptions(10, 1, 21); for (int i = 0, bins = options.getBinsCount(); i < bins; i++) { @@ -75,7 +70,6 @@ public class UniformOptionsTest { } @Test - @SmallTest public void testBinIndexForRangeEqual5() { Histogram.UniformOptions options = new Histogram.UniformOptions(2, 0, 10); assertEquals(4, options.getBinsCount()); @@ -87,7 +81,6 @@ public class UniformOptionsTest { } @Test - @SmallTest public void testBinIndexForRangeEqual10() { Histogram.UniformOptions options = new Histogram.UniformOptions(10, 1, 101); assertEquals(0, options.getBinForSample(0)); @@ -101,7 +94,6 @@ public class UniformOptionsTest { } @Test - @SmallTest public void testBinIndexForRangeEqual90() { final int binCount = 10; final int minValue = 100; diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml index c877e8991d9e..c9c1af8b807b 100644 --- a/data/fonts/fonts.xml +++ b/data/fonts/fonts.xml @@ -247,7 +247,7 @@ <alias name="courier new" to="serif-monospace" /> <family name="casual"> - <font weight="400" style="normal">ComingSoon.ttf</font> + <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font> </family> <family name="cursive"> diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java index c2cd6ffc622c..74597c5cd874 100644 --- a/keystore/java/android/security/KeyStore2.java +++ b/keystore/java/android/security/KeyStore2.java @@ -161,6 +161,15 @@ public class KeyStore2 { } /** + * List all entries in the keystore for in the given namespace. + */ + public KeyDescriptor[] listBatch(int domain, long namespace, String startPastAlias) + throws KeyStoreException { + return handleRemoteExceptionWithRetry( + (service) -> service.listEntriesBatched(domain, namespace, startPastAlias)); + } + + /** * Grant string prefix as used by the keystore boringssl engine. Must be kept in sync * with system/security/keystore-engine. Note: The prefix here includes the 0x which * std::stringstream used in keystore-engine needs to identify the number as hex represented. @@ -301,6 +310,13 @@ public class KeyStore2 { }); } + /** + * Returns the number of Keystore entries for a given domain and namespace. + */ + public int getNumberOfEntries(int domain, long namespace) throws KeyStoreException { + return handleRemoteExceptionWithRetry((service) + -> service.getNumberOfEntries(domain, namespace)); + } protected static void interruptedPreservingSleep(long millis) { boolean wasInterrupted = false; Calendar calendar = Calendar.getInstance(); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java index 91f216f1320a..045e318ff513 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java @@ -79,13 +79,11 @@ import java.security.spec.NamedParameterSpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.Enumeration; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Set; +import java.util.NoSuchElementException; import javax.crypto.SecretKey; @@ -1043,26 +1041,22 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { } } - private Set<String> getUniqueAliases() { + private KeyDescriptor[] getAliasesBatch(String startPastAlias) { try { - final KeyDescriptor[] keys = mKeyStore.list( + return mKeyStore.listBatch( getTargetDomain(), - mNamespace + mNamespace, + startPastAlias ); - final Set<String> aliases = new HashSet<>(keys.length); - for (KeyDescriptor d : keys) { - aliases.add(d.alias); - } - return aliases; } catch (android.security.KeyStoreException e) { Log.e(TAG, "Failed to list keystore entries.", e); - return new HashSet<>(); + return new KeyDescriptor[0]; } } @Override public Enumeration<String> engineAliases() { - return Collections.enumeration(getUniqueAliases()); + return new KeyEntriesEnumerator(); } @Override @@ -1073,12 +1067,18 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { return getKeyMetadata(alias) != null; } - @Override public int engineSize() { - return getUniqueAliases().size(); + try { + return mKeyStore.getNumberOfEntries( + getTargetDomain(), + mNamespace + ); + } catch (android.security.KeyStoreException e) { + Log.e(TAG, "Failed to get the number of keystore entries.", e); + return 0; + } } - @Override public boolean engineIsKeyEntry(String alias) { return isKeyEntry(alias); @@ -1251,4 +1251,38 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi { + "or TrustedCertificateEntry; was " + entry); } } + + private class KeyEntriesEnumerator implements Enumeration<String> { + private KeyDescriptor[] mCurrentBatch; + private int mCurrentEntry = 0; + private String mLastAlias = null; + private KeyEntriesEnumerator() { + getAndValidateNextBatch(); + } + + private void getAndValidateNextBatch() { + mCurrentBatch = getAliasesBatch(mLastAlias); + mCurrentEntry = 0; + } + + public boolean hasMoreElements() { + return (mCurrentBatch != null) && (mCurrentBatch.length > 0); + } + + public String nextElement() { + if ((mCurrentBatch == null) || (mCurrentBatch.length == 0)) { + throw new NoSuchElementException("Error while fetching entries."); + } + final KeyDescriptor currentEntry = mCurrentBatch[mCurrentEntry]; + mLastAlias = currentEntry.alias; + + mCurrentEntry++; + // This was the last entry in the batch. + if (mCurrentEntry >= mCurrentBatch.length) { + getAndValidateNextBatch(); + } + + return mLastAlias; + } + } } diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java index 6fa1a694eb67..372e4cb3d72e 100644 --- a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java @@ -40,7 +40,6 @@ import java.security.InvalidKeyException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; -import java.util.Random; /** * Assorted utility methods for implementing crypto operations on top of KeyStore. @@ -50,7 +49,6 @@ import java.util.Random; abstract class KeyStoreCryptoOperationUtils { private static volatile SecureRandom sRng; - private static final Random sRandom = new Random(); private KeyStoreCryptoOperationUtils() {} @@ -213,7 +211,7 @@ abstract class KeyStoreCryptoOperationUtils { } else { // Keystore won't give us an operation challenge if the operation doesn't // need user authorization. So we make our own. - return sRandom.nextLong(); + return getRng().nextLong(); } } } diff --git a/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java index f96c39c879fd..1e1f68aaefc1 100644 --- a/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java +++ b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java @@ -17,9 +17,14 @@ package android.security.keystore2; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.security.KeyStore2; @@ -36,6 +41,12 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; + public class AndroidKeyStoreSpiTest { @Mock @@ -48,14 +59,176 @@ public class AndroidKeyStoreSpiTest { @Test public void testEngineAliasesReturnsEmptySetOnKeyStoreError() throws Exception { - when(mKeystore2.list(anyInt(), anyLong())) + when(mKeystore2.listBatch(anyInt(), anyLong(), isNull())) .thenThrow(new KeyStoreException(6, "Some Error")); AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); spi.initForTesting(mKeystore2); assertThat("Empty collection expected", !spi.engineAliases().hasMoreElements()); - verify(mKeystore2).list(anyInt(), anyLong()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), isNull()); + } + + @Test + public void testEngineAliasesCorrectlyListsZeroEntriesEmptyArray() throws Exception { + when(mKeystore2.listBatch(anyInt(), anyLong(), anyString())) + .thenReturn(new KeyDescriptor[0]); + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + Enumeration<String> aliases = spi.engineAliases(); + assertThat("Should not have any elements", !aliases.hasMoreElements()); + assertThrows("Should have no elements to return", NoSuchElementException.class, + () -> aliases.nextElement()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), isNull()); + } + + @Test + public void testEngineAliasesCorrectlyListsZeroEntriesNullArray() throws Exception { + when(mKeystore2.listBatch(anyInt(), anyLong(), anyString())) + .thenReturn(null); + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + Enumeration<String> aliases = spi.engineAliases(); + assertThat("Should not have any elements", !aliases.hasMoreElements()); + assertThrows("Should have no elements to return", NoSuchElementException.class, + () -> aliases.nextElement()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), isNull()); + } + + private static KeyDescriptor newKeyDescriptor(String alias) { + KeyDescriptor result = new KeyDescriptor(); + result.alias = alias; + return result; + } + + private static KeyDescriptor[] createKeyDescriptorsArray(int numEntries) { + KeyDescriptor[] kds = new KeyDescriptor[numEntries]; + for (int i = 0; i < kds.length; i++) { + kds[i] = newKeyDescriptor(String.format("alias-%d", i)); + } + + return kds; + } + + private static void assertAliasListsEqual( + KeyDescriptor[] keyDescriptors, Enumeration<String> aliasesEnumerator) { + List<String> aliases = Collections.list(aliasesEnumerator); + Assert.assertArrayEquals(Arrays.stream(keyDescriptors).map(kd -> kd.alias).toArray(), + aliases.toArray()); + } + + @Test + public void testEngineAliasesCorrectlyListsEntriesInASingleBatch() throws Exception { + final String alias1 = "testAlias1"; + final String alias2 = "testAlias2"; + final String alias3 = "testAlias3"; + KeyDescriptor[] kds = {newKeyDescriptor(alias1), + newKeyDescriptor(alias2), newKeyDescriptor(alias3)}; + when(mKeystore2.listBatch(anyInt(), anyLong(), eq(null))) + .thenReturn(kds); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("testAlias3"))) + .thenReturn(null); + + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + Enumeration<String> aliases = spi.engineAliases(); + assertThat("Should have more elements before first.", aliases.hasMoreElements()); + Assert.assertEquals(aliases.nextElement(), alias1); + assertThat("Should have more elements before second.", aliases.hasMoreElements()); + Assert.assertEquals(aliases.nextElement(), alias2); + assertThat("Should have more elements before third.", aliases.hasMoreElements()); + Assert.assertEquals(aliases.nextElement(), alias3); + assertThat("Should have no more elements after third.", !aliases.hasMoreElements()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq(null)); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("testAlias3")); + verifyNoMoreInteractions(mKeystore2); + } + + @Test + public void testEngineAliasesCorrectlyListsEntriesInMultipleBatches() throws Exception { + final int numEntries = 2500; + KeyDescriptor[] kds = createKeyDescriptorsArray(numEntries); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq(null))) + .thenReturn(Arrays.copyOfRange(kds, 0, 1000)); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-999"))) + .thenReturn(Arrays.copyOfRange(kds, 1000, 2000)); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-1999"))) + .thenReturn(Arrays.copyOfRange(kds, 2000, 2500)); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-2499"))) + .thenReturn(null); + + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + assertAliasListsEqual(kds, spi.engineAliases()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq(null)); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-999")); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-1999")); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-2499")); + verifyNoMoreInteractions(mKeystore2); + } + + @Test + public void testEngineAliasesCorrectlyListsEntriesWhenNumEntriesIsExactlyOneBatchSize() + throws Exception { + final int numEntries = 1000; + KeyDescriptor[] kds = createKeyDescriptorsArray(numEntries); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq(null))) + .thenReturn(kds); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-999"))) + .thenReturn(null); + + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + assertAliasListsEqual(kds, spi.engineAliases()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq(null)); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-999")); + verifyNoMoreInteractions(mKeystore2); + } + + @Test + public void testEngineAliasesCorrectlyListsEntriesWhenNumEntriesIsAMultiplyOfBatchSize() + throws Exception { + final int numEntries = 2000; + KeyDescriptor[] kds = createKeyDescriptorsArray(numEntries); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq(null))) + .thenReturn(Arrays.copyOfRange(kds, 0, 1000)); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-999"))) + .thenReturn(Arrays.copyOfRange(kds, 1000, 2000)); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-1999"))) + .thenReturn(null); + + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + assertAliasListsEqual(kds, spi.engineAliases()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq(null)); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-999")); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-1999")); + verifyNoMoreInteractions(mKeystore2); + } + + @Test + public void testEngineAliasesCorrectlyListsEntriesWhenReturningLessThanBatchSize() + throws Exception { + final int numEntries = 500; + KeyDescriptor[] kds = createKeyDescriptorsArray(numEntries); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq(null))) + .thenReturn(kds); + when(mKeystore2.listBatch(anyInt(), anyLong(), eq("alias-499"))) + .thenReturn(new KeyDescriptor[0]); + + AndroidKeyStoreSpi spi = new AndroidKeyStoreSpi(); + spi.initForTesting(mKeystore2); + + assertAliasListsEqual(kds, spi.engineAliases()); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq(null)); + verify(mKeystore2).listBatch(anyInt(), anyLong(), eq("alias-499")); + verifyNoMoreInteractions(mKeystore2); } @Mock diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 5a67eb9935dd..738f1ab0c18f 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -98,7 +98,7 @@ bool Properties::load() { debugOverdraw = false; std::string debugOverdrawProperty = base::GetProperty(PROPERTY_DEBUG_OVERDRAW, ""); if (debugOverdrawProperty != "") { - INIT_LOGD(" Overdraw debug enabled: %s", debugOverdrawProperty); + INIT_LOGD(" Overdraw debug enabled: %s", debugOverdrawProperty.c_str()); if (debugOverdrawProperty == "show") { debugOverdraw = true; overdrawColorSet = OverdrawColorSet::Default; diff --git a/media/OWNERS b/media/OWNERS index 5f501372666b..4a6648e91af4 100644 --- a/media/OWNERS +++ b/media/OWNERS @@ -1,4 +1,5 @@ # Bug component: 1344 +atneya@google.com elaurent@google.com essick@google.com etalvala@google.com diff --git a/media/aidl/android/media/soundtrigger_middleware/OWNERS b/media/aidl/android/media/soundtrigger_middleware/OWNERS index 01b2cb981bbb..1e41886fe716 100644 --- a/media/aidl/android/media/soundtrigger_middleware/OWNERS +++ b/media/aidl/android/media/soundtrigger_middleware/OWNERS @@ -1,2 +1 @@ -atneya@google.com -elaurent@google.com +include /media/java/android/media/soundtrigger/OWNERS diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 22033c6d8401..6b29fc319c6b 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -72,7 +72,7 @@ public class AudioSystem throw new UnsupportedOperationException("Trying to instantiate AudioSystem"); } - /* These values must be kept in sync with system/audio.h */ + /* These values must be kept in sync with system/media/audio/include/system/audio-hal-enums.h */ /* * If these are modified, please also update Settings.System.VOLUME_SETTINGS * and attrs.xml and AudioManager.java. @@ -963,7 +963,8 @@ public class AudioSystem */ // - // audio device definitions: must be kept in sync with values in system/core/audio.h + // audio device definitions: must be kept in sync with values + // in system/media/audio/include/system/audio-hal-enums.h // /** @hide */ public static final int DEVICE_NONE = 0x0; diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java index e6d95eb6d5a1..cf3ba872feb5 100644 --- a/media/java/android/media/ThumbnailUtils.java +++ b/media/java/android/media/ThumbnailUtils.java @@ -49,6 +49,7 @@ import com.android.internal.util.ArrayUtils; import libcore.io.IoUtils; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; @@ -255,17 +256,19 @@ public class ThumbnailUtils { // get orientation if (MediaFile.isExifMimeType(mimeType)) { - exif = new ExifInterface(file); - switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) { - case ExifInterface.ORIENTATION_ROTATE_90: - orientation = 90; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - orientation = 180; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - orientation = 270; - break; + try (FileInputStream is = new FileInputStream(file)) { + exif = new ExifInterface(is.getFD()); + switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) { + case ExifInterface.ORIENTATION_ROTATE_90: + orientation = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + orientation = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + orientation = 270; + break; + } } } diff --git a/media/java/android/media/soundtrigger/OWNERS b/media/java/android/media/soundtrigger/OWNERS index 01b2cb981bbb..85f7a4d605e6 100644 --- a/media/java/android/media/soundtrigger/OWNERS +++ b/media/java/android/media/soundtrigger/OWNERS @@ -1,2 +1,3 @@ +# Bug component: 48436 atneya@google.com elaurent@google.com diff --git a/media/java/android/media/tv/OWNERS b/media/java/android/media/tv/OWNERS index fa0429350a25..0b022e514662 100644 --- a/media/java/android/media/tv/OWNERS +++ b/media/java/android/media/tv/OWNERS @@ -1,6 +1,7 @@ quxiangfang@google.com shubang@google.com hgchen@google.com +qingxun@google.com # For android remote service per-file ITvRemoteServiceInput.aidl = file:/media/lib/tvremote/OWNERS diff --git a/packages/CtsShim/Android.bp b/packages/CtsShim/Android.bp index 31cd76079131..baafe7ba570c 100644 --- a/packages/CtsShim/Android.bp +++ b/packages/CtsShim/Android.bp @@ -44,6 +44,9 @@ android_app_import { arm64: { apk: "apk/arm/CtsShimPriv.apk", }, + riscv64: { + apk: "apk/riscv64/CtsShimPriv.apk", + }, x86: { apk: "apk/x86/CtsShimPriv.apk", }, @@ -82,6 +85,9 @@ android_app_import { arm64: { apk: "apk/arm/CtsShim.apk", }, + riscv64: { + apk: "apk/riscv64/CtsShim.apk", + }, x86: { apk: "apk/x86/CtsShim.apk", }, diff --git a/packages/CtsShim/apk/arm/CtsShim.apk b/packages/CtsShim/apk/arm/CtsShim.apk Binary files differindex fb092862b79e..af306a504d30 100644 --- a/packages/CtsShim/apk/arm/CtsShim.apk +++ b/packages/CtsShim/apk/arm/CtsShim.apk diff --git a/packages/CtsShim/apk/arm/CtsShimPriv.apk b/packages/CtsShim/apk/arm/CtsShimPriv.apk Binary files differindex 07915ce76bb5..98c535127e7e 100644 --- a/packages/CtsShim/apk/arm/CtsShimPriv.apk +++ b/packages/CtsShim/apk/arm/CtsShimPriv.apk diff --git a/packages/CtsShim/apk/riscv64/CtsShim.apk b/packages/CtsShim/apk/riscv64/CtsShim.apk Binary files differnew file mode 100644 index 000000000000..af306a504d30 --- /dev/null +++ b/packages/CtsShim/apk/riscv64/CtsShim.apk diff --git a/packages/CtsShim/apk/riscv64/CtsShimPriv.apk b/packages/CtsShim/apk/riscv64/CtsShimPriv.apk Binary files differnew file mode 100644 index 000000000000..9a9997dc4155 --- /dev/null +++ b/packages/CtsShim/apk/riscv64/CtsShimPriv.apk diff --git a/packages/CtsShim/apk/x86/CtsShim.apk b/packages/CtsShim/apk/x86/CtsShim.apk Binary files differindex fb092862b79e..af306a504d30 100644 --- a/packages/CtsShim/apk/x86/CtsShim.apk +++ b/packages/CtsShim/apk/x86/CtsShim.apk diff --git a/packages/CtsShim/apk/x86/CtsShimPriv.apk b/packages/CtsShim/apk/x86/CtsShimPriv.apk Binary files differindex 20e94b6f2dac..29ad4786e3c7 100644 --- a/packages/CtsShim/apk/x86/CtsShimPriv.apk +++ b/packages/CtsShim/apk/x86/CtsShimPriv.apk diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 6669d6ba3085..868867de7b42 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.packageinstaller"> <original-package android:name="com.android.packageinstaller" /> @@ -142,6 +143,9 @@ android:authorities="com.google.android.packageinstaller.wear.provider" android:grantUriPermissions="true" android:exported="true" /> + + <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" + tools:node="remove" /> </application> </manifest> diff --git a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java index f305fd35b7d9..e92157e7c867 100644 --- a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java +++ b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java @@ -47,7 +47,7 @@ import javax.tools.Diagnostic.Kind; * Annotation processor for {@link SearchIndexable} that generates {@link SearchIndexableResources} * subclasses. */ -@SupportedSourceVersion(SourceVersion.RELEASE_11) +@SupportedSourceVersion(SourceVersion.RELEASE_17) @SupportedAnnotationTypes({"com.android.settingslib.search.SearchIndexable"}) public class IndexableProcessor extends AbstractProcessor { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java index c61ebc032fa5..0630a2e515e0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastMetadata.java @@ -261,19 +261,21 @@ public class LocalBluetoothLeBroadcastMetadata { Pattern pattern = Pattern.compile(PATTERN_BT_BROADCAST_METADATA); Matcher match = pattern.matcher(qrCodeString); if (match.find()) { - mSourceAddressType = Integer.parseInt(match.group(MATCH_INDEX_ADDRESS_TYPE)); - mSourceDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice( + try { + mSourceAddressType = Integer.parseInt(match.group(MATCH_INDEX_ADDRESS_TYPE)); + mSourceDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice( match.group(MATCH_INDEX_DEVICE)); - mSourceAdvertisingSid = Integer.parseInt(match.group(MATCH_INDEX_ADVERTISING_SID)); - mBroadcastId = Integer.parseInt(match.group(MATCH_INDEX_BROADCAST_ID)); - mPaSyncInterval = Integer.parseInt(match.group(MATCH_INDEX_SYNC_INTERVAL)); - mIsEncrypted = Boolean.valueOf(match.group(MATCH_INDEX_IS_ENCRYPTED)); - mBroadcastCode = match.group(MATCH_INDEX_BROADCAST_CODE).getBytes(); - mPresentationDelayMicros = - Integer.parseInt(match.group(MATCH_INDEX_PRESENTATION_DELAY)); - - if (DEBUG) { - Log.d(TAG, "Converted qrCodeString result: " + mSourceAdvertisingSid = Integer.parseInt( + match.group(MATCH_INDEX_ADVERTISING_SID)); + mBroadcastId = Integer.parseInt(match.group(MATCH_INDEX_BROADCAST_ID)); + mPaSyncInterval = Integer.parseInt(match.group(MATCH_INDEX_SYNC_INTERVAL)); + mIsEncrypted = Boolean.valueOf(match.group(MATCH_INDEX_IS_ENCRYPTED)); + mBroadcastCode = match.group(MATCH_INDEX_BROADCAST_CODE).getBytes(); + mPresentationDelayMicros = + Integer.parseInt(match.group(MATCH_INDEX_PRESENTATION_DELAY)); + + if (DEBUG) { + Log.d(TAG, "Converted qrCodeString result: " + " ,Type = " + mSourceAddressType + " ,Device = " + mSourceDevice + " ,AdSid = " + mSourceAdvertisingSid @@ -282,11 +284,11 @@ public class LocalBluetoothLeBroadcastMetadata { + " ,encrypted = " + mIsEncrypted + " ,BroadcastCode = " + Arrays.toString(mBroadcastCode) + " ,delay = " + mPresentationDelayMicros); - } + } - mSubgroup = convertToSubgroup(match.group(MATCH_INDEX_SUBGROUPS)); + mSubgroup = convertToSubgroup(match.group(MATCH_INDEX_SUBGROUPS)); - return new BluetoothLeBroadcastMetadata.Builder() + return new BluetoothLeBroadcastMetadata.Builder() .setSourceDevice(mSourceDevice, mSourceAddressType) .setSourceAdvertisingSid(mSourceAdvertisingSid) .setBroadcastId(mBroadcastId) @@ -296,10 +298,13 @@ public class LocalBluetoothLeBroadcastMetadata { .setPresentationDelayMicros(mPresentationDelayMicros) .addSubgroup(mSubgroup) .build(); + } catch (IllegalArgumentException e) { + Log.d(TAG, "IllegalArgumentException when convert : " + e); + return null; + } } else { if (DEBUG) { - Log.d(TAG, - "The match fail, can not convert it to BluetoothLeBroadcastMetadata."); + Log.d(TAG, "The match fail, can not convert it to BluetoothLeBroadcastMetadata."); } return null; } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java index 39b4b8e16708..35e3dd3379f0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileStatusTracker.java @@ -231,7 +231,7 @@ public class MobileStatusTracker { public SignalStrength signalStrength; public TelephonyDisplayInfo telephonyDisplayInfo = new TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN, - TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); + TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false); /** * Empty constructor diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/OWNERS b/packages/SettingsLib/src/com/android/settingslib/mobile/OWNERS index ab9b5dc3ff7a..c01528fb4f18 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/OWNERS +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/OWNERS @@ -1,4 +1,7 @@ # Default reviewers for this and subdirectories. -bonianchen@google.com +songferngwang@google.com +zoeychen@google.com # Emergency approvers in case the above are not available +changbetty@google.com +tomhsu@google.com diff --git a/packages/SettingsLib/src/com/android/settingslib/net/OWNERS b/packages/SettingsLib/src/com/android/settingslib/net/OWNERS index ab9b5dc3ff7a..c01528fb4f18 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/OWNERS +++ b/packages/SettingsLib/src/com/android/settingslib/net/OWNERS @@ -1,4 +1,7 @@ # Default reviewers for this and subdirectories. -bonianchen@google.com +songferngwang@google.com +zoeychen@google.com # Emergency approvers in case the above are not available +changbetty@google.com +tomhsu@google.com diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 2e6ea0e28d86..54946ee4e662 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -144,7 +144,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final TelephonyDisplayInfo DEFAULT_TELEPHONY_DISPLAY_INFO = new TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN, - TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE); + TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false); static final int MAX_WIFI_ENTRY_COUNT = 3; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt index 1fb6a982fdf3..c37b01fff578 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt @@ -49,7 +49,7 @@ internal class MobileState( ) : ConnectivityState() { @JvmField var telephonyDisplayInfo = TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN, - TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE) + TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE, false) @JvmField var serviceState: ServiceState? = null @JvmField var signalStrength: SignalStrength? = null @@ -131,7 +131,7 @@ internal class MobileState( } fun isRoaming(): Boolean { - return serviceState != null && serviceState!!.roaming + return telephonyDisplayInfo != null && telephonyDisplayInfo.isRoaming } /** diff --git a/services/core/java/com/android/server/MmsServiceBroker.java b/services/core/java/com/android/server/MmsServiceBroker.java index 59db68691223..f149fda7a043 100644 --- a/services/core/java/com/android/server/MmsServiceBroker.java +++ b/services/core/java/com/android/server/MmsServiceBroker.java @@ -45,6 +45,7 @@ import android.telephony.TelephonyManager; import android.util.Slog; import com.android.internal.telephony.IMms; +import com.android.internal.telephony.TelephonyPermissions; import com.android.server.uri.NeededUriGrants; import com.android.server.uri.UriGrantsManagerInternal; @@ -337,6 +338,14 @@ public class MmsServiceBroker extends SystemService { throws RemoteException { Slog.d(TAG, "sendMessage() by " + callingPkg); mContext.enforceCallingPermission(Manifest.permission.SEND_SMS, "Send MMS message"); + + // Check if user is associated with the subscription + if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(mContext, subId, + Binder.getCallingUserHandle())) { + // TODO(b/258629881): Display error dialog. + return; + } + if (getAppOpsManager().noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(), callingPkg, attributionTag, null) != AppOpsManager.MODE_ALLOWED) { Slog.e(TAG, callingPkg + " is not allowed to call sendMessage()"); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index ce9c067116ef..2daf04d1f8b9 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -75,6 +75,9 @@ import android.content.pm.ProviderInfo; import android.content.pm.UserInfo; import android.content.res.ObbInfo; import android.database.ContentObserver; +import android.media.MediaCodecList; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; import android.net.Uri; import android.os.BatteryManager; import android.os.Binder; @@ -1006,10 +1009,27 @@ class StorageManagerService extends IStorageManager.Stub } } + private boolean isHevcDecoderSupported() { + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + MediaCodecInfo[] codecInfos = codecList.getCodecInfos(); + for (MediaCodecInfo codecInfo : codecInfos) { + if (codecInfo.isEncoder()) { + continue; + } + String[] supportedTypes = codecInfo.getSupportedTypes(); + for (String type : supportedTypes) { + if (type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + return true; + } + } + } + return false; + } + private void configureTranscoding() { // See MediaProvider TranscodeHelper#getBooleanProperty for more information boolean transcodeEnabled = false; - boolean defaultValue = true; + boolean defaultValue = isHevcDecoderSupported() ? true : false; if (SystemProperties.getBoolean("persist.sys.fuse.transcode_user_control", false)) { transcodeEnabled = SystemProperties.getBoolean("persist.sys.fuse.transcode_enabled", diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index a69d3f0276f3..77a54a568859 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1957,7 +1957,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { && overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED) { overrideNetworkType = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE; } - return new TelephonyDisplayInfo(networkType, overrideNetworkType); + boolean isRoaming = telephonyDisplayInfo.isRoaming(); + return new TelephonyDisplayInfo(networkType, overrideNetworkType, isRoaming); } public void notifyCallForwardingChanged(boolean cfi) { diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index f652cb050cbd..e8c85ce68f22 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -1074,9 +1074,10 @@ public class VcnManagementService extends IVcnManagementService.Stub { subGrp, mLastSnapshot, mConfigs.get(subGrp)); for (int restrictedTransport : restrictedTransports) { if (ncCopy.hasTransport(restrictedTransport)) { - if (restrictedTransport == TRANSPORT_CELLULAR) { - // Only make a cell network as restricted when the VCN is in - // active mode. + if (restrictedTransport == TRANSPORT_CELLULAR + || restrictedTransport == TRANSPORT_TEST) { + // For cell or test network, only mark it as restricted when + // the VCN is in active mode. isRestricted |= (vcn.getStatus() == VCN_STATUS_CODE_ACTIVE); } else { isRestricted = true; @@ -1104,7 +1105,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { final NetworkCapabilities result = ncBuilder.build(); final VcnUnderlyingNetworkPolicy policy = new VcnUnderlyingNetworkPolicy( mTrackingNetworkCallback - .requiresRestartForImmutableCapabilityChanges(result), + .requiresRestartForImmutableCapabilityChanges(result, linkProperties), result); logVdbg("getUnderlyingNetworkPolicy() called for caps: " + networkCapabilities @@ -1354,19 +1355,29 @@ public class VcnManagementService extends IVcnManagementService.Stub { * without requiring a Network restart. */ private class TrackingNetworkCallback extends ConnectivityManager.NetworkCallback { + private final Object mLockObject = new Object(); private final Map<Network, NetworkCapabilities> mCaps = new ArrayMap<>(); + private final Map<Network, LinkProperties> mLinkProperties = new ArrayMap<>(); @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) { - synchronized (mCaps) { + synchronized (mLockObject) { mCaps.put(network, caps); } } @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + synchronized (mLockObject) { + mLinkProperties.put(network, lp); + } + } + + @Override public void onLost(Network network) { - synchronized (mCaps) { + synchronized (mLockObject) { mCaps.remove(network); + mLinkProperties.remove(network); } } @@ -1393,22 +1404,28 @@ public class VcnManagementService extends IVcnManagementService.Stub { return true; } - private boolean requiresRestartForImmutableCapabilityChanges(NetworkCapabilities caps) { + private boolean requiresRestartForImmutableCapabilityChanges( + NetworkCapabilities caps, LinkProperties lp) { if (caps.getSubscriptionIds() == null) { return false; } - synchronized (mCaps) { - for (NetworkCapabilities existing : mCaps.values()) { - if (caps.getSubscriptionIds().equals(existing.getSubscriptionIds()) - && hasSameTransportsAndCapabilities(caps, existing)) { - // Restart if any immutable capabilities have changed - return existing.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + synchronized (mLockObject) { + // Search for an existing network (using interfce names) + // TODO: Get network from NetworkFactory (if exists) for this match. + for (Entry<Network, LinkProperties> lpEntry : mLinkProperties.entrySet()) { + if (lp.getInterfaceName() != null + && !lp.getInterfaceName().isEmpty() + && Objects.equals( + lp.getInterfaceName(), lpEntry.getValue().getInterfaceName())) { + return mCaps.get(lpEntry.getKey()) + .hasCapability(NET_CAPABILITY_NOT_RESTRICTED) != caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED); } } } + // If no network found, by definition does not need restart. return false; } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d48723a4bd04..99f186351f0f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8415,13 +8415,16 @@ public class ActivityManagerService extends IActivityManager.Stub } } + boolean recoverable = eventType.equals("native_recoverable_crash"); + EventLogTags.writeAmCrash(Binder.getCallingPid(), UserHandle.getUserId(Binder.getCallingUid()), processName, r == null ? -1 : r.info.flags, crashInfo.exceptionClassName, crashInfo.exceptionMessage, crashInfo.throwFileName, - crashInfo.throwLineNumber); + crashInfo.throwLineNumber, + recoverable ? 1 : 0); int processClassEnum = processName.equals("system_server") ? ServerProtoEnums.SYSTEM_SERVER : (r != null) ? r.getProcessClassEnum() @@ -8489,7 +8492,13 @@ public class ActivityManagerService extends IActivityManager.Stub eventType, r, processName, null, null, null, null, null, null, crashInfo, new Float(loadingProgress), incrementalMetrics, null); - mAppErrors.crashApplication(r, crashInfo); + // For GWP-ASan recoverable crashes, don't make the app crash (the whole point of + // 'recoverable' is that the app doesn't crash). Normally, for nonrecoreable native crashes, + // debuggerd will terminate the process, but there's a backup where ActivityManager will + // also kill it. Avoid that. + if (!recoverable) { + mAppErrors.crashApplication(r, crashInfo); + } } public void handleApplicationStrictModeViolation( diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index d080036733a5..6ce70a1ee1d4 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -53,7 +53,7 @@ option java_package com.android.server.am 30037 am_process_start_timeout (User|1|5),(PID|1|5),(UID|1|5),(Process Name|3) # Unhandled exception -30039 am_crash (User|1|5),(PID|1|5),(Process Name|3),(Flags|1|5),(Exception|3),(Message|3),(File|3),(Line|1|5) +30039 am_crash (User|1|5),(PID|1|5),(Process Name|3),(Flags|1|5),(Exception|3),(Message|3),(File|3),(Line|1|5),(Recoverable|1|5) # Log.wtf() called 30040 am_wtf (User|1|5),(PID|1|5),(Process Name|3),(Flags|1|5),(Tag|3),(Message|3) diff --git a/services/core/java/com/android/server/am/NativeCrashListener.java b/services/core/java/com/android/server/am/NativeCrashListener.java index 94eb07611037..cd119e7e3fbc 100644 --- a/services/core/java/com/android/server/am/NativeCrashListener.java +++ b/services/core/java/com/android/server/am/NativeCrashListener.java @@ -64,12 +64,15 @@ final class NativeCrashListener extends Thread { class NativeCrashReporter extends Thread { ProcessRecord mApp; int mSignal; + boolean mGwpAsanRecoverableCrash; String mCrashReport; - NativeCrashReporter(ProcessRecord app, int signal, String report) { + NativeCrashReporter(ProcessRecord app, int signal, boolean gwpAsanRecoverableCrash, + String report) { super("NativeCrashReport"); mApp = app; mSignal = signal; + mGwpAsanRecoverableCrash = gwpAsanRecoverableCrash; mCrashReport = report; } @@ -85,7 +88,9 @@ final class NativeCrashListener extends Thread { ci.stackTrace = mCrashReport; if (DEBUG) Slog.v(TAG, "Calling handleApplicationCrash()"); - mAm.handleApplicationCrashInner("native_crash", mApp, mApp.processName, ci); + mAm.handleApplicationCrashInner( + mGwpAsanRecoverableCrash ? "native_recoverable_crash" : "native_crash", + mApp, mApp.processName, ci); if (DEBUG) Slog.v(TAG, "<-- handleApplicationCrash() returned"); } catch (Exception e) { Slog.e(TAG, "Unable to report native crash", e); @@ -207,9 +212,14 @@ final class NativeCrashListener extends Thread { // permits crash_dump to connect to it. This allows us to trust the // received values. - // first, the pid and signal number - int headerBytes = readExactly(fd, buf, 0, 8); - if (headerBytes != 8) { + // Activity Manager protocol: + // - 32-bit network-byte-order: pid + // - 32-bit network-byte-order: signal number + // - byte: gwpAsanRecoverableCrash + // - bytes: raw text of the dump + // - null terminator + int headerBytes = readExactly(fd, buf, 0, 9); + if (headerBytes != 9) { // protocol failure; give up Slog.e(TAG, "Unable to read from debuggerd"); return; @@ -217,69 +227,76 @@ final class NativeCrashListener extends Thread { int pid = unpackInt(buf, 0); int signal = unpackInt(buf, 4); + boolean gwpAsanRecoverableCrash = buf[8] != 0; if (DEBUG) { - Slog.v(TAG, "Read pid=" + pid + " signal=" + signal); + Slog.v(TAG, "Read pid=" + pid + " signal=" + signal + + " recoverable=" + gwpAsanRecoverableCrash); + } + if (pid < 0) { + Slog.e(TAG, "Bogus pid!"); + return; } // now the text of the dump - if (pid > 0) { - final ProcessRecord pr; - synchronized (mAm.mPidsSelfLocked) { - pr = mAm.mPidsSelfLocked.get(pid); + final ProcessRecord pr; + synchronized (mAm.mPidsSelfLocked) { + pr = mAm.mPidsSelfLocked.get(pid); + } + if (pr == null) { + Slog.w(TAG, "Couldn't find ProcessRecord for pid " + pid); + return; + } + + // Don't attempt crash reporting for persistent apps + if (pr.isPersistent()) { + if (DEBUG) { + Slog.v(TAG, "Skipping report for persistent app " + pr); } - if (pr != null) { - // Don't attempt crash reporting for persistent apps - if (pr.isPersistent()) { - if (DEBUG) { - Slog.v(TAG, "Skipping report for persistent app " + pr); - } - return; - } + return; + } - int bytes; - do { - // get some data - bytes = Os.read(fd, buf, 0, buf.length); - if (bytes > 0) { - if (MORE_DEBUG) { - String s = new String(buf, 0, bytes, "UTF-8"); - Slog.v(TAG, "READ=" + bytes + "> " + s); - } - // did we just get the EOD null byte? - if (buf[bytes-1] == 0) { - os.write(buf, 0, bytes-1); // exclude the EOD token - break; - } - // no EOD, so collect it and read more - os.write(buf, 0, bytes); - } - } while (bytes > 0); - - // Okay, we've got the report. - if (DEBUG) Slog.v(TAG, "processing"); - - // Mark the process record as being a native crash so that the - // cleanup mechanism knows we're still submitting the report - // even though the process will vanish as soon as we let - // debuggerd proceed. - synchronized (mAm) { - synchronized (mAm.mProcLock) { - pr.mErrorState.setCrashing(true); - pr.mErrorState.setForceCrashReport(true); - } + int bytes; + do { + // get some data + bytes = Os.read(fd, buf, 0, buf.length); + if (bytes > 0) { + if (MORE_DEBUG) { + String s = new String(buf, 0, bytes, "UTF-8"); + Slog.v(TAG, "READ=" + bytes + "> " + s); + } + // did we just get the EOD null byte? + if (buf[bytes - 1] == 0) { + os.write(buf, 0, bytes - 1); // exclude the EOD token + break; + } + // no EOD, so collect it and read more + os.write(buf, 0, bytes); + } + } while (bytes > 0); + + // Okay, we've got the report. + if (DEBUG) Slog.v(TAG, "processing"); + + // Mark the process record as being a native crash so that the + // cleanup mechanism knows we're still submitting the report even + // though the process will vanish as soon as we let debuggerd + // proceed. This isn't relevant for recoverable crashes, as we don't + // show the user an "app crashed" dialogue because the app (by + // design) didn't crash. + if (!gwpAsanRecoverableCrash) { + synchronized (mAm) { + synchronized (mAm.mProcLock) { + pr.mErrorState.setCrashing(true); + pr.mErrorState.setForceCrashReport(true); } - - // Crash reporting is synchronous but we want to let debuggerd - // go about it business right away, so we spin off the actual - // reporting logic on a thread and let it take it's time. - final String reportString = new String(os.toByteArray(), "UTF-8"); - (new NativeCrashReporter(pr, signal, reportString)).start(); - } else { - Slog.w(TAG, "Couldn't find ProcessRecord for pid " + pid); } - } else { - Slog.e(TAG, "Bogus pid!"); } + + // Crash reporting is synchronous but we want to let debuggerd + // go about it business right away, so we spin off the actual + // reporting logic on a thread and let it take it's time. + final String reportString = new String(os.toByteArray(), "UTF-8"); + (new NativeCrashReporter(pr, signal, gwpAsanRecoverableCrash, reportString)).start(); } catch (Exception e) { Slog.e(TAG, "Exception dealing with report", e); // ugh, fail. diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index abddc4366460..256df98ae760 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2573,7 +2573,10 @@ public final class ProcessList { + ", " + reason); app.setPendingStart(false); killProcessQuiet(pid); - Process.killProcessGroup(app.uid, app.getPid()); + final int appPid = app.getPid(); + if (appPid != 0) { + Process.killProcessGroup(app.uid, appPid); + } noteAppKill(app, ApplicationExitInfo.REASON_OTHER, ApplicationExitInfo.SUBREASON_INVALID_START, reason); return false; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index a74f4154eb85..210fc910ef57 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1209,6 +1209,9 @@ public class AudioDeviceInventory { "LE Audio device addr=" + address + " now available").printLog(TAG)); } + // Reset LEA suspend state each time a new sink is connected + mAudioSystem.setParameters("LeAudioSuspended=false"); + mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); @@ -1254,6 +1257,9 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeLeAudioDeviceUnavailableLater(String address, int device, int delayMs) { + // prevent any activity on the LEA output to avoid unwanted + // reconnection of the sink. + mAudioSystem.setParameters("LeAudioSuspended=true"); // the device will be made unavailable later, so consider it disconnected right away mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); // send the delayed message to make the device unavailable later diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 593acd6578c7..a7c2ddf1b9d3 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -896,7 +896,7 @@ public class AudioService extends IAudioService.Stub // Defines the format for the connection "address" for ALSA devices public static String makeAlsaAddressString(int card, int device) { - return "card=" + card + ";device=" + device + ";"; + return "card=" + card + ";device=" + device; } public static final class Lifecycle extends SystemService { @@ -1014,9 +1014,14 @@ public class AudioService extends IAudioService.Stub mSfxHelper = new SoundEffectsHelper(mContext, playerBase -> ignorePlayerLogs(playerBase)); - final boolean headTrackingDefault = mContext.getResources().getBoolean( + final boolean binauralEnabledDefault = SystemProperties.getBoolean( + "ro.audio.spatializer_binaural_enabled_default", true); + final boolean transauralEnabledDefault = SystemProperties.getBoolean( + "ro.audio.spatializer_transaural_enabled_default", true); + final boolean headTrackingEnabledDefault = mContext.getResources().getBoolean( com.android.internal.R.bool.config_spatial_audio_head_tracking_enabled_default); - mSpatializerHelper = new SpatializerHelper(this, mAudioSystem, headTrackingDefault); + mSpatializerHelper = new SpatializerHelper(this, mAudioSystem, + binauralEnabledDefault, transauralEnabledDefault, headTrackingEnabledDefault); mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator(); @@ -1313,8 +1318,8 @@ public class AudioService extends IAudioService.Stub intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); if (mMonitorRotation) { RotationHelper.init(mContext, mAudioHandler, - rotationParam -> onRotationUpdate(rotationParam), - foldParam -> onFoldUpdate(foldParam)); + rotation -> onRotationUpdate(rotation), + foldState -> onFoldStateUpdate(foldState)); } intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); @@ -1463,16 +1468,20 @@ public class AudioService extends IAudioService.Stub //----------------------------------------------------------------- // rotation/fold updates coming from RotationHelper - void onRotationUpdate(String rotationParameter) { + void onRotationUpdate(Integer rotation) { + mSpatializerHelper.setDisplayOrientation((float) (rotation * Math.PI / 180.)); // use REPLACE as only the last rotation matters + final String rotationParameter = "rotation=" + rotation; sendMsg(mAudioHandler, MSG_ROTATION_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, /*obj*/ rotationParameter, /*delay*/ 0); } - void onFoldUpdate(String foldParameter) { + void onFoldStateUpdate(Boolean foldState) { + mSpatializerHelper.setFoldState(foldState); // use REPLACE as only the last fold state matters + final String foldStateParameter = "device_folded=" + (foldState ? "on" : "off"); sendMsg(mAudioHandler, MSG_FOLD_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, - /*obj*/ foldParameter, /*delay*/ 0); + /*obj*/ foldStateParameter, /*delay*/ 0); } //----------------------------------------------------------------- @@ -1687,6 +1696,11 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.reset(/* featureEnabled */ mHasSpatializerEffect); + // Restore rotation information. + if (mMonitorRotation) { + RotationHelper.forceUpdate(); + } + onIndicateSystemReady(); // indicate the end of reconfiguration phase to audio HAL AudioSystem.setParameters("restarting=false"); @@ -4073,13 +4087,14 @@ public class AudioService extends IAudioService.Stub return; } - // Forcefully set LE audio volume as a workaround, since in some cases - // (like the outgoing call) the value of 'device' is not DEVICE_OUT_BLE_* - // even when BLE is connected. + // In some cases (like the outgoing or rejected call) the value of 'device' is not + // DEVICE_OUT_BLE_* even when BLE is connected. Changing the volume level in such case + // may cuase the other devices volume level leaking into the LeAudio device settings. if (!AudioSystem.isLeAudioDeviceType(device)) { - Log.w(TAG, "setLeAudioVolumeOnModeUpdate got unexpected device=" + device - + ", forcing to device=" + AudioSystem.DEVICE_OUT_BLE_HEADSET); - device = AudioSystem.DEVICE_OUT_BLE_HEADSET; + Log.w(TAG, "setLeAudioVolumeOnModeUpdate ignoring invalid device=" + + device + ", mode=" + mode + ", index=" + index + " maxIndex=" + maxIndex + + " streamType=" + streamType); + return; } if (DEBUG_VOL) { diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 6cd42f87aede..d3b76065f998 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -477,6 +477,7 @@ public class BtHelper { mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); AudioSystem.setParameters("A2dpSuspended=false"); + AudioSystem.setParameters("LeAudioSuspended=false"); mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco"); } diff --git a/services/core/java/com/android/server/audio/RotationHelper.java b/services/core/java/com/android/server/audio/RotationHelper.java index 5cdf58bdd62f..394e4af30a9e 100644 --- a/services/core/java/com/android/server/audio/RotationHelper.java +++ b/services/core/java/com/android/server/audio/RotationHelper.java @@ -55,14 +55,14 @@ class RotationHelper { private static AudioDisplayListener sDisplayListener; private static FoldStateListener sFoldStateListener; /** callback to send rotation updates to AudioSystem */ - private static Consumer<String> sRotationUpdateCb; + private static Consumer<Integer> sRotationCallback; /** callback to send folded state updates to AudioSystem */ - private static Consumer<String> sFoldUpdateCb; + private static Consumer<Boolean> sFoldStateCallback; private static final Object sRotationLock = new Object(); private static final Object sFoldStateLock = new Object(); - private static int sDeviceRotation = Surface.ROTATION_0; // R/W synchronized on sRotationLock - private static boolean sDeviceFold = true; // R/W synchronized on sFoldStateLock + private static Integer sRotation = null; // R/W synchronized on sRotationLock + private static Boolean sFoldState = null; // R/W synchronized on sFoldStateLock private static Context sContext; private static Handler sHandler; @@ -73,15 +73,15 @@ class RotationHelper { * - sContext != null */ static void init(Context context, Handler handler, - Consumer<String> rotationUpdateCb, Consumer<String> foldUpdateCb) { + Consumer<Integer> rotationCallback, Consumer<Boolean> foldStateCallback) { if (context == null) { throw new IllegalArgumentException("Invalid null context"); } sContext = context; sHandler = handler; sDisplayListener = new AudioDisplayListener(); - sRotationUpdateCb = rotationUpdateCb; - sFoldUpdateCb = foldUpdateCb; + sRotationCallback = rotationCallback; + sFoldStateCallback = foldStateCallback; enable(); } @@ -112,9 +112,9 @@ class RotationHelper { int newRotation = DisplayManagerGlobal.getInstance() .getDisplayInfo(Display.DEFAULT_DISPLAY).rotation; synchronized(sRotationLock) { - if (newRotation != sDeviceRotation) { - sDeviceRotation = newRotation; - publishRotation(sDeviceRotation); + if (sRotation == null || sRotation != newRotation) { + sRotation = newRotation; + publishRotation(sRotation); } } } @@ -123,43 +123,52 @@ class RotationHelper { if (DEBUG_ROTATION) { Log.i(TAG, "publishing device rotation =" + rotation + " (x90deg)"); } - String rotationParam; + int rotationDegrees; switch (rotation) { case Surface.ROTATION_0: - rotationParam = "rotation=0"; + rotationDegrees = 0; break; case Surface.ROTATION_90: - rotationParam = "rotation=90"; + rotationDegrees = 90; break; case Surface.ROTATION_180: - rotationParam = "rotation=180"; + rotationDegrees = 180; break; case Surface.ROTATION_270: - rotationParam = "rotation=270"; + rotationDegrees = 270; break; default: Log.e(TAG, "Unknown device rotation"); - rotationParam = null; + rotationDegrees = -1; } - if (rotationParam != null) { - sRotationUpdateCb.accept(rotationParam); + if (rotationDegrees != -1) { + sRotationCallback.accept(rotationDegrees); } } /** * publish the change of device folded state if any. */ - static void updateFoldState(boolean newFolded) { + static void updateFoldState(boolean foldState) { synchronized (sFoldStateLock) { - if (sDeviceFold != newFolded) { - sDeviceFold = newFolded; - String foldParam; - if (newFolded) { - foldParam = "device_folded=on"; - } else { - foldParam = "device_folded=off"; - } - sFoldUpdateCb.accept(foldParam); + if (sFoldState == null || sFoldState != foldState) { + sFoldState = foldState; + sFoldStateCallback.accept(foldState); + } + } + } + + /** + * forceUpdate is called when audioserver restarts. + */ + static void forceUpdate() { + synchronized (sRotationLock) { + sRotation = null; + } + updateOrientation(); // We will get at least one orientation update now. + synchronized (sFoldStateLock) { + if (sFoldState != null) { + sFoldStateCallback.accept(sFoldState); } } } @@ -185,4 +194,4 @@ class RotationHelper { updateOrientation(); } } -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 8e8fd05bf72e..c9cdba970ccb 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -106,12 +106,12 @@ public class SpatializerHelper { }; // Spatializer state machine - private static final int STATE_UNINITIALIZED = 0; - private static final int STATE_NOT_SUPPORTED = 1; - private static final int STATE_DISABLED_UNAVAILABLE = 3; - private static final int STATE_ENABLED_UNAVAILABLE = 4; - private static final int STATE_ENABLED_AVAILABLE = 5; - private static final int STATE_DISABLED_AVAILABLE = 6; + /*package*/ static final int STATE_UNINITIALIZED = 0; + /*package*/ static final int STATE_NOT_SUPPORTED = 1; + /*package*/ static final int STATE_DISABLED_UNAVAILABLE = 3; + /*package*/ static final int STATE_ENABLED_UNAVAILABLE = 4; + /*package*/ static final int STATE_ENABLED_AVAILABLE = 5; + /*package*/ static final int STATE_DISABLED_AVAILABLE = 6; private int mState = STATE_UNINITIALIZED; private boolean mFeatureEnabled = false; @@ -147,9 +147,9 @@ public class SpatializerHelper { .setSampleRate(48000) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .build(); - // device array to store the routing for the default attributes and format, size 1 because - // media is never expected to be duplicated - private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1]; + // device array to store the routing for the default attributes and format, initialized to + // an empty list as routing hasn't been established yet + private static ArrayList<AudioDeviceAttributes> sRoutingDevices = new ArrayList<>(0); //--------------------------------------------------------------- // audio device compatibility / enabled @@ -171,18 +171,17 @@ public class SpatializerHelper { // initialization @SuppressWarnings("StaticAssignmentInConstructor") SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa, - boolean headTrackingEnabledByDefault) { + boolean binauralEnabledDefault, + boolean transauralEnabledDefault, + boolean headTrackingEnabledDefault) { mAudioService = mother; mASA = asa; // "StaticAssignmentInConstructor" warning is suppressed as the SpatializerHelper being // constructed here is the factory for SADeviceState, thus SADeviceState and its // private static field sHeadTrackingEnabledDefault should never be accessed directly. - SADeviceState.sHeadTrackingEnabledDefault = headTrackingEnabledByDefault; - } - - synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) { - mBinauralSupported = hasBinaural; - mTransauralSupported = hasTransaural; + SADeviceState.sBinauralEnabledDefault = binauralEnabledDefault; + SADeviceState.sTransauralEnabledDefault = transauralEnabledDefault; + SADeviceState.sHeadTrackingEnabledDefault = headTrackingEnabledDefault; } synchronized void init(boolean effectExpected, @Nullable String settings) { @@ -318,8 +317,7 @@ public class SpatializerHelper { return; } mState = STATE_DISABLED_UNAVAILABLE; - mASA.getDevicesForAttributes( - DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES); + sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES); // note at this point mSpat is still not instantiated } @@ -361,34 +359,35 @@ public class SpatializerHelper { case STATE_DISABLED_AVAILABLE: break; } - mASA.getDevicesForAttributes( - DEFAULT_ATTRIBUTES, false /* forVolume */).toArray(ROUTING_DEVICES); + + sRoutingDevices = getRoutingDevices(DEFAULT_ATTRIBUTES); // check validity of routing information - if (ROUTING_DEVICES[0] == null) { - logloge("onRoutingUpdated: device is null, no Spatial Audio"); + if (sRoutingDevices.isEmpty()) { + logloge("onRoutingUpdated: no device, no Spatial Audio"); setDispatchAvailableState(false); // not changing the spatializer level as this is likely a transient state return; } + final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); // is media routed to a new device? - if (isWireless(ROUTING_DEVICES[0].getType())) { - addWirelessDeviceIfNew(ROUTING_DEVICES[0]); + if (isWireless(currentDevice.getType())) { + addWirelessDeviceIfNew(currentDevice); } // find if media device enabled / available - final Pair<Boolean, Boolean> enabledAvailable = evaluateState(ROUTING_DEVICES[0]); + final Pair<Boolean, Boolean> enabledAvailable = evaluateState(currentDevice); boolean able = false; if (enabledAvailable.second) { // available for Spatial audio, check w/ effect - able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES); + able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, sRoutingDevices); loglogi("onRoutingUpdated: can spatialize media 5.1:" + able - + " on device:" + ROUTING_DEVICES[0]); + + " on device:" + currentDevice); setDispatchAvailableState(able); } else { - loglogi("onRoutingUpdated: device:" + ROUTING_DEVICES[0] + loglogi("onRoutingUpdated: device:" + currentDevice + " not available for Spatial Audio"); setDispatchAvailableState(false); } @@ -396,10 +395,10 @@ public class SpatializerHelper { boolean enabled = able && enabledAvailable.first; if (enabled) { loglogi("Enabling Spatial Audio since enabled for media device:" - + ROUTING_DEVICES[0]); + + currentDevice); } else { loglogi("Disabling Spatial Audio since disabled for media device:" - + ROUTING_DEVICES[0]); + + currentDevice); } if (mSpat != null) { byte level = enabled ? (byte) Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL @@ -732,9 +731,13 @@ public class SpatializerHelper { } private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes, - @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) { - if (isDeviceCompatibleWithSpatializationModes(devices[0])) { - return AudioSystem.canBeSpatialized(attributes, format, devices); + @NonNull AudioFormat format, @NonNull ArrayList<AudioDeviceAttributes> devices) { + if (devices.isEmpty()) { + return false; + } + if (isDeviceCompatibleWithSpatializationModes(devices.get(0))) { + AudioDeviceAttributes[] devArray = new AudioDeviceAttributes[devices.size()]; + return AudioSystem.canBeSpatialized(attributes, format, devices.toArray(devArray)); } return false; } @@ -1010,10 +1013,13 @@ public class SpatializerHelper { logd("canBeSpatialized false due to usage:" + attributes.getUsage()); return false; } - AudioDeviceAttributes[] devices = new AudioDeviceAttributes[1]; + // going through adapter to take advantage of routing cache - mASA.getDevicesForAttributes( - attributes, false /* forVolume */).toArray(devices); + final ArrayList<AudioDeviceAttributes> devices = getRoutingDevices(attributes); + if (devices.isEmpty()) { + logloge("canBeSpatialized got no device for " + attributes); + return false; + } final boolean able = canBeSpatializedOnDevice(attributes, format, devices); logd("canBeSpatialized usage:" + attributes.getUsage() + " format:" + format.toLogFriendlyString() + " returning " + able); @@ -1063,7 +1069,7 @@ public class SpatializerHelper { if (transform.length != 6) { throw new IllegalArgumentException("invalid array size" + transform.length); } - if (!checkSpatForHeadTracking("setGlobalTransform")) { + if (!checkSpatializerForHeadTracking("setGlobalTransform")) { return; } try { @@ -1074,7 +1080,7 @@ public class SpatializerHelper { } synchronized void recenterHeadTracker() { - if (!checkSpatForHeadTracking("recenterHeadTracker")) { + if (!checkSpatializerForHeadTracking("recenterHeadTracker")) { return; } try { @@ -1084,8 +1090,30 @@ public class SpatializerHelper { } } + synchronized void setDisplayOrientation(float displayOrientation) { + if (!checkSpatializer("setDisplayOrientation")) { + return; + } + try { + mSpat.setDisplayOrientation(displayOrientation); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setDisplayOrientation", e); + } + } + + synchronized void setFoldState(boolean folded) { + if (!checkSpatializer("setFoldState")) { + return; + } + try { + mSpat.setFoldState(folded); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setFoldState", e); + } + } + synchronized void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) { - if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) { + if (!checkSpatializerForHeadTracking("setDesiredHeadTrackingMode")) { return; } if (mode != Spatializer.HEAD_TRACKING_MODE_DISABLED) { @@ -1122,8 +1150,13 @@ public class SpatializerHelper { logDeviceState(deviceState, "setHeadTrackerEnabled"); // check current routing to see if it affects the headtracking mode - if (ROUTING_DEVICES[0] != null && ROUTING_DEVICES[0].getType() == ada.getType() - && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) { + if (sRoutingDevices.isEmpty()) { + logloge("setHeadTrackerEnabled: no device, bailing"); + return; + } + final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); + if (currentDevice.getType() == ada.getType() + && currentDevice.getAddress().equals(ada.getAddress())) { setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled : Spatializer.HEAD_TRACKING_MODE_DISABLED); if (enabled && !mHeadTrackerAvailable) { @@ -1178,7 +1211,7 @@ public class SpatializerHelper { return mHeadTrackerAvailable; } - private boolean checkSpatForHeadTracking(String funcName) { + private boolean checkSpatializer(String funcName) { switch (mState) { case STATE_UNINITIALIZED: case STATE_NOT_SUPPORTED: @@ -1189,14 +1222,18 @@ public class SpatializerHelper { case STATE_ENABLED_AVAILABLE: if (mSpat == null) { // try to recover by resetting the native spatializer state - Log.e(TAG, "checkSpatForHeadTracking(): " - + "native spatializer should not be null in state: " + mState); + Log.e(TAG, "checkSpatializer(): called from " + funcName + + "(), native spatializer should not be null in state: " + mState); postReset(); return false; } break; } - return mIsHeadTrackingSupported; + return true; + } + + private boolean checkSpatializerForHeadTracking(String funcName) { + return checkSpatializer(funcName) && mIsHeadTrackingSupported; } private void dispatchActualHeadTrackingMode(int newMode) { @@ -1513,10 +1550,12 @@ public class SpatializerHelper { } /*package*/ static final class SADeviceState { + private static boolean sBinauralEnabledDefault = true; + private static boolean sTransauralEnabledDefault = true; private static boolean sHeadTrackingEnabledDefault = false; final @AudioDeviceInfo.AudioDeviceType int mDeviceType; final @NonNull String mDeviceAddress; - boolean mEnabled = true; // by default, SA is enabled on any device + boolean mEnabled; boolean mHasHeadTracker = false; boolean mHeadTrackerEnabled; static final String SETTING_FIELD_SEPARATOR = ","; @@ -1532,6 +1571,12 @@ public class SpatializerHelper { SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @Nullable String address) { mDeviceType = deviceType; mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : ""; + final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE); + mEnabled = spatMode == SpatializationMode.SPATIALIZER_BINAURAL + ? sBinauralEnabledDefault + : spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL + ? sTransauralEnabledDefault + : false; mHeadTrackerEnabled = sHeadTrackingEnabledDefault; } @@ -1668,10 +1713,11 @@ public class SpatializerHelper { private int getHeadSensorHandleUpdateTracker() { int headHandle = -1; - final AudioDeviceAttributes currentDevice = ROUTING_DEVICES[0]; - if (currentDevice == null) { + if (sRoutingDevices.isEmpty()) { + logloge("getHeadSensorHandleUpdateTracker: no device, no head tracker"); return headHandle; } + final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice); // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR @@ -1705,6 +1751,23 @@ public class SpatializerHelper { return screenHandle; } + /** + * Returns routing for the given attributes + * @param aa AudioAttributes whose routing is being queried + * @return a non-null never-empty list of devices. If the routing query failed, the list + * will contain null. + */ + private @NonNull ArrayList<AudioDeviceAttributes> getRoutingDevices(AudioAttributes aa) { + final ArrayList<AudioDeviceAttributes> devices = mASA.getDevicesForAttributes( + aa, false /* forVolume */); + for (AudioDeviceAttributes ada : devices) { + if (ada == null) { + // invalid entry, reject this routing query by returning an empty list + return new ArrayList<>(0); + } + } + return devices; + } private static void loglogi(String msg) { AudioService.sSpatialLogger.loglogi(msg, TAG); @@ -1721,4 +1784,13 @@ public class SpatializerHelper { /*package*/ void clearSADevices() { mSADevices.clear(); } + + /*package*/ synchronized void forceStateForTest(int state) { + mState = state; + } + + /*package*/ synchronized void initForTest(boolean hasBinaural, boolean hasTransaural) { + mBinauralSupported = hasBinaural; + mTransauralSupported = hasTransaural; + } } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 6d3f8fd77232..b25206d3b621 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -31,6 +31,7 @@ import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO; import static android.os.PowerWhitelistManager.REASON_VPN; import static android.os.UserHandle.PER_USER_RANGE; import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; +import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT; import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; @@ -269,6 +270,49 @@ public class Vpn { @VisibleForTesting static final int DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT = 5 * 60; + /** + * Default keepalive value to consider long-lived TCP connections are expensive on the + * VPN network from battery usage point of view. + * TODO: consider reading from setting. + */ + @VisibleForTesting + static final int DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC = 60; + /** + * Prefer using {@link IkeSessionParams.ESP_IP_VERSION_AUTO} and + * {@link IkeSessionParams.ESP_ENCAP_TYPE_AUTO} for ESP packets. + * + * This is one of the possible customization values for + * CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT. + */ + @VisibleForTesting + public static final int PREFERRED_IKE_PROTOCOL_AUTO = 0; + /** + * Prefer using {@link IkeSessionParams.ESP_IP_VERSION_IPV4} and + * {@link IkeSessionParams.ESP_ENCAP_TYPE_UDP} for ESP packets. + * + * This is one of the possible customization values for + * CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT. + */ + @VisibleForTesting + public static final int PREFERRED_IKE_PROTOCOL_IPV4_UDP = 40; + /** + * Prefer using {@link IkeSessionParams.ESP_IP_VERSION_IPV6} and + * {@link IkeSessionParams.ESP_ENCAP_TYPE_UDP} for ESP packets. + * + * Do not use this value for production code. Its numeric value will change in future versions. + */ + @VisibleForTesting + public static final int PREFERRED_IKE_PROTOCOL_IPV6_UDP = 60; + /** + * Prefer using {@link IkeSessionParams.ESP_IP_VERSION_IPV6} and + * {@link IkeSessionParams.ESP_ENCAP_TYPE_NONE} for ESP packets. + * + * This is one of the possible customization values for + * CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT. + */ + @VisibleForTesting + public static final int PREFERRED_IKE_PROTOCOL_IPV6_ESP = 61; + // TODO: create separate trackers for each unique VPN to support // automated reconnection @@ -321,17 +365,17 @@ public class Vpn { return mVpnProfileStore; } - private static final int MAX_EVENTS_LOGS = 20; - private final LocalLog mUnderlyNetworkChanges = new LocalLog(MAX_EVENTS_LOGS); - private final LocalLog mVpnManagerEvents = new LocalLog(MAX_EVENTS_LOGS); + private static final int MAX_EVENTS_LOGS = 100; + private final LocalLog mEventChanges = new LocalLog(MAX_EVENTS_LOGS); /** - * Cached Map of <subscription ID, keepalive delay ms> since retrieving the PersistableBundle + * Cached Map of <subscription ID, CarrierConfigInfo> since retrieving the PersistableBundle * and the target value from CarrierConfigManager is somewhat expensive as it has hundreds of * fields. This cache is cleared when the carrier config changes to ensure data freshness. */ @GuardedBy("this") - private final SparseArray<Integer> mCachedKeepalivePerSubId = new SparseArray<>(); + private final SparseArray<CarrierConfigInfo> mCachedCarrierConfigInfoPerSubId = + new SparseArray<>(); /** * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This @@ -378,6 +422,28 @@ public class Vpn { void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException; } + private static class CarrierConfigInfo { + public final String mccMnc; + public final int keepaliveDelayMs; + public final int encapType; + public final int ipVersion; + + CarrierConfigInfo(String mccMnc, int keepaliveDelayMs, + int encapType, + int ipVersion) { + this.mccMnc = mccMnc; + this.keepaliveDelayMs = keepaliveDelayMs; + this.encapType = encapType; + this.ipVersion = ipVersion; + } + + @Override + public String toString() { + return "CarrierConfigInfo(" + mccMnc + ") [keepaliveDelayMs=" + keepaliveDelayMs + + ", encapType=" + encapType + ", ipVersion=" + ipVersion + "]"; + } + } + @VisibleForTesting public static class Dependencies { public boolean isCallerSystem() { @@ -890,7 +956,7 @@ public class Vpn { int errorCode, @NonNull final String packageName, @Nullable final String sessionKey, @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork, @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) { - mVpnManagerEvents.log("Event class=" + getVpnManagerEventClassName(errorClass) + mEventChanges.log("[VMEvent] Event class=" + getVpnManagerEventClassName(errorClass) + ", err=" + getVpnManagerEventErrorName(errorCode) + " for " + packageName + " on session " + sessionKey); final Intent intent = buildVpnManagerEventIntent(category, errorClass, errorCode, @@ -1040,6 +1106,8 @@ public class Vpn { mLockdownAllowlist = (mLockdown && lockdownAllowlist != null) ? Collections.unmodifiableList(new ArrayList<>(lockdownAllowlist)) : Collections.emptyList(); + mEventChanges.log("[LockdownAlwaysOn] Mode changed: lockdown=" + mLockdown + " alwaysOn=" + + mAlwaysOn + " calling from " + Binder.getCallingUid()); if (isCurrentPreparedPackage(packageName)) { updateAlwaysOnNotification(mNetworkInfo.getDetailedState()); @@ -1610,9 +1678,12 @@ public class Vpn { capsBuilder.setUids(createUserAndRestrictedProfilesRanges(mUserId, mConfig.allowedApplications, mConfig.disallowedApplications)); - capsBuilder.setTransportInfo( - new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass, - false /* longLivedTcpConnectionsExpensive */)); + final boolean expensive = areLongLivedTcpConnectionsExpensive(mVpnRunner); + capsBuilder.setTransportInfo(new VpnTransportInfo( + getActiveVpnType(), + mConfig.session, + mConfig.allowBypass, + expensive)); // Only apps targeting Q and above can explicitly declare themselves as metered. // These VPNs are assumed metered unless they state otherwise. @@ -1644,6 +1715,17 @@ public class Vpn { updateState(DetailedState.CONNECTED, "agentConnect"); } + private static boolean areLongLivedTcpConnectionsExpensive(@NonNull VpnRunner runner) { + if (!(runner instanceof IkeV2VpnRunner)) return false; + + final int delay = ((IkeV2VpnRunner) runner).getOrGuessKeepaliveDelaySeconds(); + return areLongLivedTcpConnectionsExpensive(delay); + } + + private static boolean areLongLivedTcpConnectionsExpensive(int keepaliveDelaySec) { + return keepaliveDelaySec < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC; + } + private boolean canHaveRestrictedProfile(int userId) { final long token = Binder.clearCallingIdentity(); try { @@ -1655,7 +1737,7 @@ public class Vpn { } private void logUnderlyNetworkChanges(List<Network> networks) { - mUnderlyNetworkChanges.log("Switch to " + mEventChanges.log("[UnderlyingNW] Switch to " + ((networks != null) ? TextUtils.join(", ", networks) : "null")); } @@ -2922,16 +3004,17 @@ public class Vpn { @Override public void onCarrierConfigChanged(int slotIndex, int subId, int carrierId, int specificCarrierId) { + mEventChanges.log("[CarrierConfig] Changed on slot " + slotIndex + " subId=" + + subId + " carrerId=" + carrierId + + " specificCarrierId=" + specificCarrierId); synchronized (Vpn.this) { - mCachedKeepalivePerSubId.remove(subId); + mCachedCarrierConfigInfoPerSubId.remove(subId); // Ignore stale runner. if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return; - maybeMigrateIkeSession(mActiveNetwork); + maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork); } - // TODO: update the longLivedTcpConnectionsExpensive value in the - // networkcapabilities of the VPN network. } }; @@ -3014,6 +3097,8 @@ public class Vpn { */ public void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration) { if (!isActiveToken(token)) { + mEventChanges.log("[IKEEvent-" + mSessionKey + "] onIkeOpened obsolete token=" + + token); Log.d(TAG, "onIkeOpened called for obsolete token " + token); return; } @@ -3021,7 +3106,12 @@ public class Vpn { mMobikeEnabled = ikeConfiguration.isIkeExtensionEnabled( IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE); - onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo()); + final IkeSessionConnectionInfo info = ikeConfiguration.getIkeSessionConnectionInfo(); + mEventChanges.log("[IKEEvent-" + mSessionKey + "] onIkeOpened token=" + token + + ", localAddr=" + info.getLocalAddress() + + ", network=" + info.getNetwork() + + ", mobikeEnabled= " + mMobikeEnabled); + onIkeConnectionInfoChanged(token, info); } /** @@ -3034,11 +3124,17 @@ public class Vpn { */ public void onIkeConnectionInfoChanged( int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + if (!isActiveToken(token)) { + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onIkeConnectionInfoChanged obsolete token=" + token); Log.d(TAG, "onIkeConnectionInfoChanged called for obsolete token " + token); return; } - + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onIkeConnectionInfoChanged token=" + token + + ", localAddr=" + ikeConnectionInfo.getLocalAddress() + + ", network=" + ikeConnectionInfo.getNetwork()); // The update on VPN and the IPsec tunnel will be done when migration is fully complete // in onChildMigrated mIkeConnectionInfo = ikeConnectionInfo; @@ -3052,6 +3148,8 @@ public class Vpn { */ public void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig) { if (!isActiveToken(token)) { + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onChildOpened obsolete token=" + token); Log.d(TAG, "onChildOpened called for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, @@ -3061,7 +3159,9 @@ public class Vpn { // sessions are torn down via resetIkeState(). return; } - + mEventChanges.log("[IKEEvent-" + mSessionKey + "] onChildOpened token=" + token + + ", addr=" + TextUtils.join(", ", childConfig.getInternalAddresses()) + + " dns=" + TextUtils.join(", ", childConfig.getInternalDnsServers())); try { final String interfaceName = mTunnelIface.getInterfaceName(); final List<LinkAddress> internalAddresses = childConfig.getInternalAddresses(); @@ -3158,6 +3258,8 @@ public class Vpn { public void onChildTransformCreated( int token, @NonNull IpSecTransform transform, int direction) { if (!isActiveToken(token)) { + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onChildTransformCreated obsolete token=" + token); Log.d(TAG, "ChildTransformCreated for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, @@ -3167,7 +3269,9 @@ public class Vpn { // sessions are torn down via resetIkeState(). return; } - + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onChildTransformCreated token=" + token + ", direction=" + direction + + ", transform=" + transform); try { mTunnelIface.setUnderlyingNetwork(mIkeConnectionInfo.getNetwork()); @@ -3192,10 +3296,14 @@ public class Vpn { @NonNull IpSecTransform inTransform, @NonNull IpSecTransform outTransform) { if (!isActiveToken(token)) { + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onChildMigrated obsolete token=" + token); Log.d(TAG, "onChildMigrated for obsolete token " + token); return; } - + mEventChanges.log("[IKEEvent-" + mSessionKey + + "] onChildMigrated token=" + token + + ", in=" + inTransform + ", out=" + outTransform); // The actual network of this IKE session has migrated to is // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because mActiveNetwork // might have been updated after the migration was triggered. @@ -3382,56 +3490,150 @@ public class Vpn { return; } - if (maybeMigrateIkeSession(underlyingNetwork)) return; + if (maybeMigrateIkeSessionAndUpdateVpnTransportInfo(underlyingNetwork)) return; startIkeSession(underlyingNetwork); } private int guessEspIpVersionForNetwork() { - // TODO : guess the IP version based on carrier if auto IP version selection is enabled - return ESP_IP_VERSION_AUTO; + final CarrierConfigInfo carrierconfig = getCarrierConfig(); + final int ipVersion = (carrierconfig != null) + ? carrierconfig.ipVersion : ESP_IP_VERSION_AUTO; + if (carrierconfig != null) { + Log.d(TAG, "Get customized IP version(" + ipVersion + ") on SIM(" + + carrierconfig.mccMnc + ")"); + } + return ipVersion; } private int guessEspEncapTypeForNetwork() { - // TODO : guess the ESP encap type based on carrier if auto IP version selection is - // enabled - return ESP_ENCAP_TYPE_AUTO; + final CarrierConfigInfo carrierconfig = getCarrierConfig(); + final int encapType = (carrierconfig != null) + ? carrierconfig.encapType : ESP_ENCAP_TYPE_AUTO; + if (carrierconfig != null) { + Log.d(TAG, "Get customized encap type(" + encapType + ") on SIM(" + + carrierconfig.mccMnc + ")"); + } + return encapType; } private int guessNattKeepaliveTimerForNetwork() { + final CarrierConfigInfo carrierconfig = getCarrierConfig(); + final int natKeepalive = (carrierconfig != null) + ? carrierconfig.keepaliveDelayMs : AUTOMATIC_KEEPALIVE_DELAY_SECONDS; + if (carrierconfig != null) { + Log.d(TAG, "Get customized keepalive(" + natKeepalive + ") on SIM(" + + carrierconfig.mccMnc + ")"); + } + return natKeepalive; + } + + private CarrierConfigInfo getCarrierConfig() { final int subId = getCellSubIdForNetworkCapabilities(mUnderlyingNetworkCapabilities); if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.d(TAG, "Underlying network is not a cellular network"); - return AUTOMATIC_KEEPALIVE_DELAY_SECONDS; + return null; } synchronized (Vpn.this) { - if (mCachedKeepalivePerSubId.contains(subId)) { - Log.d(TAG, "Get cached keepalive config"); - return mCachedKeepalivePerSubId.get(subId); + if (mCachedCarrierConfigInfoPerSubId.contains(subId)) { + Log.d(TAG, "Get cached config"); + return mCachedCarrierConfigInfoPerSubId.get(subId); } + } - final TelephonyManager perSubTm = mTelephonyManager.createForSubscriptionId(subId); - if (perSubTm.getSimApplicationState() != TelephonyManager.SIM_STATE_LOADED) { - Log.d(TAG, "SIM card is not ready on sub " + subId); - return AUTOMATIC_KEEPALIVE_DELAY_SECONDS; - } + final TelephonyManager perSubTm = mTelephonyManager.createForSubscriptionId(subId); + if (perSubTm.getSimApplicationState() != TelephonyManager.SIM_STATE_LOADED) { + Log.d(TAG, "SIM card is not ready on sub " + subId); + return null; + } - final PersistableBundle carrierConfig = - mCarrierConfigManager.getConfigForSubId(subId); - if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) { - return AUTOMATIC_KEEPALIVE_DELAY_SECONDS; - } + final PersistableBundle carrierConfig = + mCarrierConfigManager.getConfigForSubId(subId); + if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) { + return null; + } + + final int natKeepalive = + carrierConfig.getInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT); + final int preferredIpPortocol = + carrierConfig.getInt(KEY_PREFERRED_IKE_PROTOCOL_INT); + final String mccMnc = perSubTm.getSimOperator(subId); + final CarrierConfigInfo info = + buildCarrierConfigInfo(mccMnc, natKeepalive, preferredIpPortocol); + synchronized (Vpn.this) { + mCachedCarrierConfigInfoPerSubId.put(subId, info); + } + + return info; + } + + private CarrierConfigInfo buildCarrierConfigInfo(String mccMnc, + int natKeepalive, int preferredIpPortocol) { + final int ipVersion; + final int encapType; + switch (preferredIpPortocol) { + case PREFERRED_IKE_PROTOCOL_AUTO: + ipVersion = IkeSessionParams.ESP_IP_VERSION_AUTO; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_AUTO; + break; + case PREFERRED_IKE_PROTOCOL_IPV4_UDP: + ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV4; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_UDP; + break; + case PREFERRED_IKE_PROTOCOL_IPV6_UDP: + ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV6; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_UDP; + break; + case PREFERRED_IKE_PROTOCOL_IPV6_ESP: + ipVersion = IkeSessionParams.ESP_IP_VERSION_IPV6; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_NONE; + break; + default: + ipVersion = IkeSessionParams.ESP_IP_VERSION_AUTO; + encapType = IkeSessionParams.ESP_ENCAP_TYPE_AUTO; + break; + } + return new CarrierConfigInfo(mccMnc, natKeepalive, encapType, ipVersion); + } - final int natKeepalive = - carrierConfig.getInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT); - mCachedKeepalivePerSubId.put(subId, natKeepalive); - Log.d(TAG, "Get customized keepalive=" + natKeepalive); - return natKeepalive; + private int getOrGuessKeepaliveDelaySeconds() { + if (mProfile.isAutomaticNattKeepaliveTimerEnabled()) { + return guessNattKeepaliveTimerForNetwork(); + } else if (mProfile.getIkeTunnelConnectionParams() != null) { + return mProfile.getIkeTunnelConnectionParams() + .getIkeSessionParams().getNattKeepAliveDelaySeconds(); + } + return DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; + } + + boolean maybeMigrateIkeSessionAndUpdateVpnTransportInfo( + @NonNull Network underlyingNetwork) { + final int keepaliveDelaySec = getOrGuessKeepaliveDelaySeconds(); + final boolean migrated = maybeMigrateIkeSession(underlyingNetwork, keepaliveDelaySec); + if (migrated) { + updateVpnTransportInfoAndNetCap(keepaliveDelaySec); } + return migrated; } - boolean maybeMigrateIkeSession(@NonNull Network underlyingNetwork) { + public void updateVpnTransportInfoAndNetCap(int keepaliveDelaySec) { + final VpnTransportInfo info = new VpnTransportInfo( + getActiveVpnType(), + mConfig.session, + mConfig.allowBypass, + areLongLivedTcpConnectionsExpensive(keepaliveDelaySec)); + final boolean ncUpdateRequired = !info.equals(mNetworkCapabilities.getTransportInfo()); + if (ncUpdateRequired) { + mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) + .setTransportInfo(info) + .build(); + doSendNetworkCapabilities(mNetworkAgent, mNetworkCapabilities); + } + } + + private boolean maybeMigrateIkeSession(@NonNull Network underlyingNetwork, + int keepaliveDelaySeconds) { if (mSession == null || !mMobikeEnabled) return false; // IKE session can schedule a migration event only when IKE AUTH is finished @@ -3440,19 +3642,22 @@ public class Vpn { + mCurrentToken + " to network " + underlyingNetwork); - final int ipVersion = mProfile.isAutomaticIpVersionSelectionEnabled() - ? guessEspIpVersionForNetwork() : ESP_IP_VERSION_AUTO; - final int encapType = mProfile.isAutomaticIpVersionSelectionEnabled() - ? guessEspEncapTypeForNetwork() : ESP_ENCAP_TYPE_AUTO; - final int keepaliveDelaySeconds; - if (mProfile.isAutomaticNattKeepaliveTimerEnabled()) { - keepaliveDelaySeconds = guessNattKeepaliveTimerForNetwork(); + + final int ipVersion; + final int encapType; + if (mProfile.isAutomaticIpVersionSelectionEnabled()) { + ipVersion = guessEspIpVersionForNetwork(); + encapType = guessEspEncapTypeForNetwork(); } else if (mProfile.getIkeTunnelConnectionParams() != null) { - keepaliveDelaySeconds = mProfile.getIkeTunnelConnectionParams() - .getIkeSessionParams().getNattKeepAliveDelaySeconds(); + ipVersion = mProfile.getIkeTunnelConnectionParams() + .getIkeSessionParams().getIpVersion(); + encapType = mProfile.getIkeTunnelConnectionParams() + .getIkeSessionParams().getEncapType(); } else { - keepaliveDelaySeconds = DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; + ipVersion = ESP_IP_VERSION_AUTO; + encapType = ESP_ENCAP_TYPE_AUTO; } + mSession.setNetwork(underlyingNetwork, ipVersion, encapType, keepaliveDelaySeconds); return true; } @@ -3526,6 +3731,8 @@ public class Vpn { /** Called when the NetworkCapabilities of underlying network is changed */ public void onDefaultNetworkCapabilitiesChanged(@NonNull NetworkCapabilities nc) { + mEventChanges.log("[UnderlyingNW] Cap changed from " + + mUnderlyingNetworkCapabilities + " to " + nc); final NetworkCapabilities oldNc = mUnderlyingNetworkCapabilities; mUnderlyingNetworkCapabilities = nc; if (oldNc == null) { @@ -3533,12 +3740,14 @@ public class Vpn { startOrMigrateIkeSession(mActiveNetwork); } else if (!nc.getSubscriptionIds().equals(oldNc.getSubscriptionIds())) { // Renew carrierConfig values. - maybeMigrateIkeSession(mActiveNetwork); + maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork); } } /** Called when the LinkProperties of underlying network is changed */ public void onDefaultNetworkLinkPropertiesChanged(@NonNull LinkProperties lp) { + mEventChanges.log("[UnderlyingNW] Lp changed from " + + mUnderlyingLinkProperties + " to " + lp); mUnderlyingLinkProperties = lp; } @@ -3561,7 +3770,7 @@ public class Vpn { Log.d(TAG, "Data stall suspected"); // Trigger MOBIKE. - maybeMigrateIkeSession(mActiveNetwork); + maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork); mDataStallSuspected = true; } } @@ -4543,7 +4752,7 @@ public class Vpn { // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from // ConnectivityServiceTest. if (SdkLevel.isAtLeastT()) { - mVpnManagerEvents.log(packageName + " stopped"); + mEventChanges.log("[VMEvent] " + packageName + " stopped"); sendEventToVpnManagerApp(intent, packageName); } } @@ -4877,23 +5086,21 @@ public class Vpn { pw.println("NetworkCapabilities: " + mNetworkCapabilities); if (isIkev2VpnRunner()) { final IkeV2VpnRunner runner = ((IkeV2VpnRunner) mVpnRunner); - pw.println("Token: " + runner.mSessionKey); + pw.println("SessionKey: " + runner.mSessionKey); pw.println("MOBIKE " + (runner.mMobikeEnabled ? "enabled" : "disabled")); + pw.println("Profile: " + runner.mProfile); + pw.println("Token: " + runner.mCurrentToken); if (mDataStallSuspected) pw.println("Data stall suspected"); if (runner.mScheduledHandleDataStallFuture != null) { pw.println("Reset session scheduled"); } } - pw.println("mCachedKeepalivePerSubId=" + mCachedKeepalivePerSubId); - - pw.println("mUnderlyNetworkChanges (most recent first):"); - pw.increaseIndent(); - mUnderlyNetworkChanges.reverseDump(pw); - pw.decreaseIndent(); + pw.println(); + pw.println("mCachedCarrierConfigInfoPerSubId=" + mCachedCarrierConfigInfoPerSubId); - pw.println("mVpnManagerEvent (most recent first):"); + pw.println("mEventChanges (most recent first):"); pw.increaseIndent(); - mVpnManagerEvents.reverseDump(pw); + mEventChanges.reverseDump(pw); pw.decreaseIndent(); } } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 8b579ac6539d..6cc89b8fba3e 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -883,7 +883,7 @@ class AutomaticBrightnessController { if (mLoggingEnabled) { Slog.d(TAG, "updateAmbientLux: " + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " - + "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", " + + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", " + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " + "mAmbientLux=" + mAmbientLux); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 195101dd3c5c..571afa822cc3 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2317,7 +2317,6 @@ public class NotificationManagerService extends SystemService { mAppOps, new SysUiStatsEvent.BuilderFactory(), mShowReviewPermissionsNotification); - mPreferencesHelper.updateFixedImportance(mUm.getUsers()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, @@ -2760,6 +2759,9 @@ public class NotificationManagerService extends SystemService { maybeShowInitialReviewPermissionsNotification(); } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis()); + } else if (phase == SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY) { + mPreferencesHelper.updateFixedImportance(mUm.getUsers()); + mPreferencesHelper.migrateNotificationPermissions(mUm.getUsers()); } } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 78dad124a4c1..65bd3f12a61a 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -237,7 +237,6 @@ public class PreferencesHelper implements RankingConfig { Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); } - ArrayList<PermissionHelper.PackagePermission> pkgPerms = new ArrayList<>(); synchronized (mPackagePreferences) { while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { tag = parser.getName(); @@ -255,27 +254,18 @@ public class PreferencesHelper implements RankingConfig { String name = parser.getAttributeValue(null, ATT_NAME); if (!TextUtils.isEmpty(name)) { restorePackage(parser, forRestore, userId, name, upgradeForBubbles, - migrateToPermission, pkgPerms); + migrateToPermission); } } } } } - if (migrateToPermission) { - for (PackagePermission p : pkgPerms) { - try { - mPermissionHelper.setNotificationPermission(p); - } catch (Exception e) { - Slog.e(TAG, "could not migrate setting for " + p.packageName, e); - } - } - } } @GuardedBy("mPackagePreferences") private void restorePackage(TypedXmlPullParser parser, boolean forRestore, @UserIdInt int userId, String name, boolean upgradeForBubbles, - boolean migrateToPermission, ArrayList<PermissionHelper.PackagePermission> pkgPerms) { + boolean migrateToPermission) { try { int uid = parser.getAttributeInt(null, ATT_UID, UNKNOWN_UID); if (forRestore) { @@ -382,14 +372,6 @@ public class PreferencesHelper implements RankingConfig { if (migrateToPermission) { r.importance = appImportance; r.migrateToPm = true; - if (r.uid != UNKNOWN_UID) { - // Don't call into permission system until we have a valid uid - PackagePermission pkgPerm = new PackagePermission( - r.pkg, UserHandle.getUserId(r.uid), - r.importance != IMPORTANCE_NONE, - hasUserConfiguredSettings(r)); - pkgPerms.add(pkgPerm); - } } } catch (Exception e) { Slog.w(TAG, "Failed to restore pkg", e); @@ -2681,6 +2663,31 @@ public class PreferencesHelper implements RankingConfig { } } + public void migrateNotificationPermissions(List<UserInfo> users) { + for (UserInfo user : users) { + List<PackageInfo> packages = mPm.getInstalledPackagesAsUser( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL), + user.getUserHandle().getIdentifier()); + for (PackageInfo pi : packages) { + synchronized (mPackagePreferences) { + PackagePreferences p = getOrCreatePackagePreferencesLocked( + pi.packageName, pi.applicationInfo.uid); + if (p.migrateToPm && p.uid != UNKNOWN_UID) { + try { + PackagePermission pkgPerm = new PackagePermission( + p.pkg, UserHandle.getUserId(p.uid), + p.importance != IMPORTANCE_NONE, + hasUserConfiguredSettings(p)); + mPermissionHelper.setNotificationPermission(pkgPerm); + } catch (Exception e) { + Slog.e(TAG, "could not migrate setting for " + p.pkg, e); + } + } + } + } + } + } + private void updateConfig() { mRankingHandler.requestSort(); } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 3816b07042dc..0de44bc5d6b3 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3938,10 +3938,14 @@ final class InstallPackageHelper { deletePackageHelper.deletePackageLIF(parsedPackage.getPackageName(), null, true, mPm.mUserManager.getUserIds(), 0, null, false); } - } else if (newPkgVersionGreater) { + } else if (newPkgVersionGreater || newSharedUserSetting) { // The application on /system is newer than the application on /data. // Simply remove the application on /data [keeping application data] // and replace it with the version on /system. + // Also, if the sharedUserSetting of the application on /system is different + // from the sharedUserSetting on data, we should trust the sharedUserSetting + // on /system, even if the application version on /system is smaller than + // the version on /data. logCriticalInfo(Log.WARN, "System package enabled;" + " name: " + pkgSetting.getPackageName() diff --git a/services/core/java/com/android/server/policy/GlobalActions.java b/services/core/java/com/android/server/policy/GlobalActions.java index e1461356cc4c..76a714c47f2e 100644 --- a/services/core/java/com/android/server/policy/GlobalActions.java +++ b/services/core/java/com/android/server/policy/GlobalActions.java @@ -93,7 +93,8 @@ class GlobalActions implements GlobalActionsProvider.GlobalActionsListener { mGlobalActionsAvailable = available; if (mShowing && !mGlobalActionsAvailable) { // Global actions provider died but we need to be showing global actions still, show the - // legacy global acrions provider. + // legacy global actions provider and remove timeout callbacks to avoid legacy re-show. + mHandler.removeCallbacks(mShowTimeout); ensureLegacyCreated(); mLegacyGlobalActions.showDialog(mKeyguardShowing, mDeviceProvisioned); } diff --git a/services/core/java/com/android/server/soundtrigger_middleware/OWNERS b/services/core/java/com/android/server/soundtrigger_middleware/OWNERS index 01b2cb981bbb..1e41886fe716 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/OWNERS +++ b/services/core/java/com/android/server/soundtrigger_middleware/OWNERS @@ -1,2 +1 @@ -atneya@google.com -elaurent@google.com +include /media/java/android/media/soundtrigger/OWNERS diff --git a/services/core/java/com/android/server/vcn/TEST_MAPPING b/services/core/java/com/android/server/vcn/TEST_MAPPING new file mode 100644 index 000000000000..5b04d884fc1a --- /dev/null +++ b/services/core/java/com/android/server/vcn/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "presubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } + ] +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java index 2141eba3be50..7f129ea3801c 100644 --- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java +++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java @@ -171,6 +171,18 @@ class NetworkPriorityClassifier { return false; } + for (Map.Entry<Integer, Integer> entry : + networkPriority.getCapabilitiesMatchCriteria().entrySet()) { + final int cap = entry.getKey(); + final int matchCriteria = entry.getValue(); + + if (matchCriteria == MATCH_REQUIRED && !caps.hasCapability(cap)) { + return false; + } else if (matchCriteria == MATCH_FORBIDDEN && caps.hasCapability(cap)) { + return false; + } + } + if (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST)) { return true; } @@ -319,18 +331,6 @@ class NetworkPriorityClassifier { return false; } - for (Map.Entry<Integer, Integer> entry : - networkPriority.getCapabilitiesMatchCriteria().entrySet()) { - final int cap = entry.getKey(); - final int matchCriteria = entry.getValue(); - - if (matchCriteria == MATCH_REQUIRED && !caps.hasCapability(cap)) { - return false; - } else if (matchCriteria == MATCH_FORBIDDEN && caps.hasCapability(cap)) { - return false; - } - } - return true; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 75d84eabd146..bb2a2cfd0161 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1183,7 +1183,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private void finishHoldScreenUpdate() { final boolean hold = mTmpHoldScreenWindow != null; if (hold && mTmpHoldScreenWindow != mHoldScreenWindow) { - mHoldScreenWakeLock.setWorkSource(new WorkSource(mTmpHoldScreenWindow.mSession.mUid)); + mHoldScreenWakeLock.setWorkSource(new WorkSource(mTmpHoldScreenWindow.mSession.mUid, + mTmpHoldScreenWindow.mSession.mPackageName)); } mHoldScreenWindow = mTmpHoldScreenWindow; mTmpHoldScreenWindow = null; diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index b9739f03bec5..b64122d5ca72 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -115,7 +115,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private boolean mShowingAlertWindowNotificationAllowed; private boolean mClientDead = false; private float mLastReportedAnimatorScale; - private String mPackageName; + protected String mPackageName; private String mRelayoutTag; private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities(); private final InsetsSourceControl[] mDummyControls = new InsetsSourceControl[0]; diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java index 428eaff9e5bc..3ad24de4cdca 100644 --- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java @@ -17,12 +17,17 @@ package com.android.server.audio; import com.android.server.audio.SpatializerHelper.SADeviceState; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; +import android.media.AudioFormat; import android.media.AudioSystem; import android.util.Log; @@ -36,6 +41,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; +import java.util.ArrayList; import java.util.List; @MediumTest @@ -49,14 +55,35 @@ public class SpatializerHelperTest { @Mock private AudioService mMockAudioService; @Spy private AudioSystemAdapter mSpyAudioSystem; + @Mock private AudioSystemAdapter mMockAudioSystem; @Before public void setUp() throws Exception { mMockAudioService = mock(AudioService.class); - mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + } + + /** + * Initializes mSpatHelper, the SpatizerHelper instance under test, to use the mock or spy + * AudioSystemAdapter + * @param useSpyAudioSystem true to use the spy adapter, mSpyAudioSystem, or false to use + * the mock adapter, mMockAudioSystem. + */ + private void setUpSpatHelper(boolean useSpyAudioSystem) { + final AudioSystemAdapter asAdapter; + if (useSpyAudioSystem) { + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + asAdapter = mSpyAudioSystem; + mMockAudioSystem = null; + } else { + mSpyAudioSystem = null; + mMockAudioSystem = mock(NoOpAudioSystemAdapter.class); + asAdapter = mMockAudioSystem; + } + mSpatHelper = new SpatializerHelper(mMockAudioService, asAdapter, + true /*binauralEnabledDefault*/, + true /*transauralEnabledDefault*/, + false /*headTrackingEnabledDefault*/); - mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem, - false /*headTrackingEnabledByDefault*/); } /** @@ -66,6 +93,7 @@ public class SpatializerHelperTest { */ @Test public void testSADeviceStateNullAddressCtor() throws Exception { + setUpSpatHelper(true /*useSpyAudioSystem*/); try { SADeviceState devState = new SADeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null); devState = new SADeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, null); @@ -76,6 +104,7 @@ public class SpatializerHelperTest { @Test public void testSADeviceStateStringSerialization() throws Exception { Log.i(TAG, "starting testSADeviceStateStringSerialization"); + setUpSpatHelper(true /*useSpyAudioSystem*/); final SADeviceState devState = new SADeviceState( AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla"); devState.mHasHeadTracker = false; @@ -91,6 +120,7 @@ public class SpatializerHelperTest { @Test public void testSADeviceSettings() throws Exception { Log.i(TAG, "starting testSADeviceSettings"); + setUpSpatHelper(true /*useSpyAudioSystem*/); final AudioDeviceAttributes dev1 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""); final AudioDeviceAttributes dev2 = @@ -141,4 +171,34 @@ public class SpatializerHelperTest { Log.i(TAG, "device settingsRestored: " + settingsRestored); Assert.assertEquals(settings, settingsRestored); } + + /** + * Test that null devices for routing do not break canBeSpatialized + * @throws Exception + */ + @Test + public void testNoRoutingCanBeSpatialized() throws Exception { + Log.i(TAG, "Starting testNoRoutingCanBeSpatialized"); + setUpSpatHelper(false /*useSpyAudioSystem*/); + mSpatHelper.forceStateForTest(SpatializerHelper.STATE_ENABLED_AVAILABLE); + + final ArrayList<AudioDeviceAttributes> emptyList = new ArrayList<>(0); + final ArrayList<AudioDeviceAttributes> listWithNull = new ArrayList<>(1); + listWithNull.add(null); + final AudioAttributes media = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA).build(); + final AudioFormat spatialFormat = new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1).build(); + + when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) + .thenReturn(emptyList); + Assert.assertFalse("can be spatialized on empty routing", + mSpatHelper.canBeSpatialized(media, spatialFormat)); + + when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) + .thenReturn(listWithNull); + Assert.assertFalse("can be spatialized on null routing", + mSpatHelper.canBeSpatialized(media, spatialFormat)); + } } diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/OWNERS b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/OWNERS index 33385afbdfd6..1e41886fe716 100644 --- a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/OWNERS +++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/OWNERS @@ -1 +1 @@ -include /media/aidl/android/media/soundtrigger_middleware/OWNERS +include /media/java/android/media/soundtrigger/OWNERS diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index d46530c27690..4fd91ba90f34 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -676,10 +676,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { compareChannels(ido, mHelper.getNotificationChannel(PKG_O, UID_O, ido.getId(), false)); compareChannels(idp, mHelper.getNotificationChannel(PKG_P, UID_P, idp.getId(), false)); - verify(mPermissionHelper).setNotificationPermission(nMr1Expected); - verify(mPermissionHelper).setNotificationPermission(oExpected); - verify(mPermissionHelper).setNotificationPermission(pExpected); - // verify that we also write a state for review_permissions_notification to eventually // show a notification assertEquals(NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW, diff --git a/services/voiceinteraction/OWNERS b/services/voiceinteraction/OWNERS index ef1061b28b63..40e8d26931e1 100644 --- a/services/voiceinteraction/OWNERS +++ b/services/voiceinteraction/OWNERS @@ -1 +1,2 @@ include /core/java/android/service/voice/OWNERS +include /media/java/android/media/soundtrigger/OWNERS diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/OWNERS b/services/voiceinteraction/java/com/android/server/soundtrigger/OWNERS index 01b2cb981bbb..1e41886fe716 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/OWNERS +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/OWNERS @@ -1,2 +1 @@ -atneya@google.com -elaurent@google.com +include /media/java/android/media/soundtrigger/OWNERS diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java index fdf694303dbc..f90eabc7175e 100644 --- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java +++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java @@ -18,6 +18,7 @@ package com.android.internal.telephony; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.Context; @@ -822,4 +823,35 @@ public final class TelephonyPermissions { } return Integer.MAX_VALUE; } + + /** + * Check if calling user is associated with the given subscription. + * @param context Context + * @param subId subscription ID + * @param callerUserHandle caller user handle + * @return false if user is not associated with the subscription. + */ + public static boolean checkSubscriptionAssociatedWithUser(@NonNull Context context, int subId, + @NonNull UserHandle callerUserHandle) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + // No subscription on device, return true. + return true; + } + + SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); + final long token = Binder.clearCallingIdentity(); + try { + if ((subManager != null) && + (!subManager.isSubscriptionAssociatedWithUser(subId, callerUserHandle))) { + // If subId is not associated with calling user, return false. + Log.e(LOG_TAG,"User[User ID:" + callerUserHandle.getIdentifier() + + "] is not associated with Subscription ID:" + subId); + return false; + + } + } finally { + Binder.restoreCallingIdentity(token); + } + return true; + } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index c175fc635e5c..17780af8061c 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -4334,6 +4334,22 @@ public class CarrierConfigManager { "min_udp_port_4500_nat_timeout_sec_int"; /** + * The preferred IKE protocol for ESP packets. + * + * This will be used by Android platform VPNs to select preferred encapsulation type and IP + * protocol type. The possible customization values are: + * + * AUTO IP VERSION and ENCAPSULATION TYPE SELECTION : "0" + * IPv4 UDP : "40" + * IPv6 ESP : "61" + * + * See the {@code PREFERRED_IKE_PROTOCOL_} constants in + * {@link com.android.server.connectivity.Vpn}. + * @hide + */ + public static final String KEY_PREFERRED_IKE_PROTOCOL_INT = "preferred_ike_protocol_int"; + + /** * Specifies whether the system should prefix the EAP method to the anonymous identity. * The following prefix will be added if this key is set to TRUE: * EAP-AKA: "0" @@ -4462,6 +4478,57 @@ public class CarrierConfigManager { "data_stall_recovery_should_skip_bool_array"; /** + * String array containing the list of names for service numbers provided by carriers. This key + * should be used with {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY}. The names provided in + * this array will be mapped 1:1 with the numbers provided in the {@link + * #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} array. + * + * <p>The data would be considered valid if and only if: + * + * <ul> + * <li>The number of items in both the arrays are equal + * <li>The data added to the {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} array is valid. + * See {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} for more information. + * </ul> + * + * <p>Example: + * + * <pre><code> + * <string-array name="carrier_service_name_array" num="2"> + * <item value="Police"/> + * <item value="Ambulance"/> + * </string-array> + * </code></pre> + */ + public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array"; + + /** + * String array containing the list of service numbers provided by carriers. This key should be + * used with {@link #KEY_CARRIER_SERVICE_NAME_STRING_ARRAY}. The numbers provided in this array + * will be mapped 1:1 with the names provided in the {@link + * #KEY_CARRIER_SERVICE_NAME_STRING_ARRAY} array. + * + * <p>The data would be considered valid if and only if: + * + * <ul> + * <li>The number of items in both the arrays are equal + * <li>The item added in this key follows a specific format. Either it should be all numbers, + * or "+" followed by all numbers. + * </ul> + * + * <p>Example: + * + * <pre><code> + * <string-array name="carrier_service_number_array" num="2"> + * <item value="123"/> + * <item value="+343"/> + * </string-array> + * </code></pre> + */ + public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = + "carrier_service_number_array"; + + /** * Configs used by ImsServiceEntitlement. */ public static final class ImsServiceEntitlement { @@ -7880,6 +7947,16 @@ public class CarrierConfigManager { public static final String KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY = KEY_PREFIX + "epdg_address_priority_int_array"; + /** + * A priority list of PLMN to be used in EPDG_ADDRESS_PLMN. Possible values are {@link + * #EPDG_PLMN_RPLMN}, {@link #EPDG_PLMN_HPLMN}, {@link #EPDG_PLMN_EHPLMN_ALL}, {@link + * #EPDG_PLMN_EHPLMN_FIRST} + * + * @hide + */ + public static final String KEY_EPDG_PLMN_PRIORITY_INT_ARRAY = + KEY_PREFIX + "epdg_plmn_priority_int_array"; + /** Epdg static IP address or FQDN */ public static final String KEY_EPDG_STATIC_ADDRESS_STRING = KEY_PREFIX + "epdg_static_address_string"; @@ -8081,6 +8158,36 @@ public class CarrierConfigManager { public static final int EPDG_ADDRESS_VISITED_COUNTRY = 4; /** @hide */ + @IntDef({ + EPDG_PLMN_RPLMN, + EPDG_PLMN_HPLMN, + EPDG_PLMN_EHPLMN_ALL, + EPDG_PLMN_EHPLMN_FIRST + }) + public @interface EpdgAddressPlmnType {} + + /** + * Use the Registered PLMN + * @hide + */ + public static final int EPDG_PLMN_RPLMN = 0; + /** + * Use the PLMN derived from IMSI + * @hide + */ + public static final int EPDG_PLMN_HPLMN = 1; + /** + * Use all EHPLMN from SIM EF files + * @hide + */ + public static final int EPDG_PLMN_EHPLMN_ALL = 2; + /** + * Use the first EHPLMN from SIM EF files + * @hide + */ + public static final int EPDG_PLMN_EHPLMN_FIRST = 3; + + /** @hide */ @IntDef({ID_TYPE_FQDN, ID_TYPE_RFC822_ADDR, ID_TYPE_KEY_ID}) public @interface IkeIdType {} @@ -8215,6 +8322,12 @@ public class CarrierConfigManager { defaults.putIntArray( KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY, new int[] {EPDG_ADDRESS_PLMN, EPDG_ADDRESS_STATIC}); + defaults.putIntArray( + KEY_EPDG_PLMN_PRIORITY_INT_ARRAY, + new int[]{ + EPDG_PLMN_RPLMN, + EPDG_PLMN_HPLMN, + EPDG_PLMN_EHPLMN_ALL}); defaults.putStringArray(KEY_MCC_MNCS_STRING_ARRAY, new String[] {}); defaults.putInt(KEY_IKE_LOCAL_ID_TYPE_INT, ID_TYPE_RFC822_ADDR); defaults.putInt(KEY_IKE_REMOTE_ID_TYPE_INT, ID_TYPE_FQDN); @@ -9210,6 +9323,7 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_PARAMETERS_USED_FOR_LTE_SIGNAL_BAR_INT, CellSignalStrengthLte.USE_RSRP); sDefaults.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, 300); + sDefaults.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, 0); // Default wifi configurations. sDefaults.putAll(Wifi.getDefaults()); sDefaults.putBoolean(ENABLE_EAP_METHOD_PREFIX_BOOL, false); @@ -9287,6 +9401,8 @@ public class CarrierConfigManager { new long[] {180000, 180000, 180000, 180000}); sDefaults.putBooleanArray(KEY_DATA_STALL_RECOVERY_SHOULD_SKIP_BOOL_ARRAY, new boolean[] {false, false, true, false, false}); + sDefaults.putStringArray(KEY_CARRIER_SERVICE_NAME_STRING_ARRAY, new String[0]); + sDefaults.putStringArray(KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY, new String[0]); } /** diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java index 1d6798b7fc6e..f1f13bc6bb55 100644 --- a/telephony/java/android/telephony/NetworkRegistrationInfo.java +++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java @@ -184,10 +184,11 @@ public final class NetworkRegistrationInfo implements Parcelable { private final int mTransportType; /** - * The initial registration state + * The true registration state of network, This is not affected by any carrier config or + * resource overlay. */ @RegistrationState - private final int mInitialRegistrationState; + private final int mNetworkRegistrationState; /** * The registration state that might have been overridden by config @@ -264,7 +265,7 @@ public final class NetworkRegistrationInfo implements Parcelable { mDomain = domain; mTransportType = transportType; mRegistrationState = registrationState; - mInitialRegistrationState = registrationState; + mNetworkRegistrationState = registrationState; mRoamingType = (registrationState == REGISTRATION_STATE_ROAMING) ? ServiceState.ROAMING_TYPE_UNKNOWN : ServiceState.ROAMING_TYPE_NOT_ROAMING; setAccessNetworkTechnology(accessNetworkTechnology); @@ -320,7 +321,7 @@ public final class NetworkRegistrationInfo implements Parcelable { mDomain = source.readInt(); mTransportType = source.readInt(); mRegistrationState = source.readInt(); - mInitialRegistrationState = source.readInt(); + mNetworkRegistrationState = source.readInt(); mRoamingType = source.readInt(); mAccessNetworkTechnology = source.readInt(); mRejectCause = source.readInt(); @@ -347,7 +348,7 @@ public final class NetworkRegistrationInfo implements Parcelable { mDomain = nri.mDomain; mTransportType = nri.mTransportType; mRegistrationState = nri.mRegistrationState; - mInitialRegistrationState = nri.mInitialRegistrationState; + mNetworkRegistrationState = nri.mNetworkRegistrationState; mRoamingType = nri.mRoamingType; mAccessNetworkTechnology = nri.mAccessNetworkTechnology; mIsUsingCarrierAggregation = nri.mIsUsingCarrierAggregation; @@ -400,40 +401,72 @@ public final class NetworkRegistrationInfo implements Parcelable { } /** - * @return The registration state. + * @return The registration state. Note this value can be affected by the carrier config + * override. * + * @deprecated Use {@link #getNetworkRegistrationState}, which is not affected by any carrier + * config or resource overlay, instead. * @hide */ + @Deprecated @SystemApi public @RegistrationState int getRegistrationState() { return mRegistrationState; } /** - * @return The initial registration state. + * @return The true registration state of network. (This value is not affected by any carrier + * config or resource overlay override). * * @hide */ - public @RegistrationState int getInitialRegistrationState() { - return mInitialRegistrationState; + @SystemApi + public @RegistrationState int getNetworkRegistrationState() { + return mNetworkRegistrationState; } /** - * @return {@code true} if registered on roaming or home network, {@code false} otherwise. + * @return {@code true} if registered on roaming or home network. Note this value can be + * affected by the carrier config override. + * + * @deprecated Use {@link #isNetworkRegistered}, which is not affected by any carrier config or + * resource overlay, instead. */ + @Deprecated public boolean isRegistered() { return mRegistrationState == REGISTRATION_STATE_HOME || mRegistrationState == REGISTRATION_STATE_ROAMING; } /** + * @return {@code true} if registered on roaming or home network, {@code false} otherwise. (This + * value is not affected by any carrier config or resource overlay override). + */ + public boolean isNetworkRegistered() { + return mNetworkRegistrationState == REGISTRATION_STATE_HOME + || mNetworkRegistrationState == REGISTRATION_STATE_ROAMING; + } + + /** * @return {@code true} if searching for service, {@code false} otherwise. + * + * @deprecated Use {@link #isNetworkRegistered}, which is not affected by any carrier config or + * resource overlay, instead. */ + @Deprecated public boolean isSearching() { return mRegistrationState == REGISTRATION_STATE_NOT_REGISTERED_SEARCHING; } /** + * @return {@code true} if searching for service, {@code false} otherwise. (This value is not + * affected by any carrier config or resource overlay override). + */ + public boolean isNetworkSearching() { + return mNetworkRegistrationState == REGISTRATION_STATE_NOT_REGISTERED_SEARCHING; + } + + /** * Get the PLMN-ID for this Network Registration, also known as the RPLMN. * * <p>If the device is registered, this will return the registered PLMN-ID. If registration @@ -450,13 +483,25 @@ public final class NetworkRegistrationInfo implements Parcelable { } /** - * @return {@code true} if registered on roaming network, {@code false} otherwise. + * @return {@code true} if registered on roaming network overridden by config. Note this value + * can be affected by the carrier config override. + * + * @deprecated Use {@link TelephonyDisplayInfo#isRoaming} instead. */ + @Deprecated public boolean isRoaming() { return mRoamingType != ServiceState.ROAMING_TYPE_NOT_ROAMING; } /** + * @return {@code true} if registered on roaming network. (This value is not affected by any + * carrier config or resource overlay override). + */ + public boolean isNetworkRoaming() { + return mNetworkRegistrationState == REGISTRATION_STATE_ROAMING; + } + + /** * @hide * @return {@code true} if in service. */ @@ -486,7 +531,8 @@ public final class NetworkRegistrationInfo implements Parcelable { } /** - * @return the current network roaming type. + * @return the current network roaming type. Note that this value can be possibly overridden by + * the carrier config or resource overlay. * @hide */ @SystemApi @@ -666,8 +712,8 @@ public final class NetworkRegistrationInfo implements Parcelable { .append(" transportType=").append( AccessNetworkConstants.transportTypeToString(mTransportType)) .append(" registrationState=").append(registrationStateToString(mRegistrationState)) - .append(" mInitialRegistrationState=") - .append(registrationStateToString(mInitialRegistrationState)) + .append(" networkRegistrationState=") + .append(registrationStateToString(mNetworkRegistrationState)) .append(" roamingType=").append(ServiceState.roamingTypeToString(mRoamingType)) .append(" accessNetworkTechnology=") .append(TelephonyManager.getNetworkTypeName(mAccessNetworkTechnology)) @@ -688,7 +734,7 @@ public final class NetworkRegistrationInfo implements Parcelable { @Override public int hashCode() { - return Objects.hash(mDomain, mTransportType, mRegistrationState, mInitialRegistrationState, + return Objects.hash(mDomain, mTransportType, mRegistrationState, mNetworkRegistrationState, mRoamingType, mAccessNetworkTechnology, mRejectCause, mEmergencyOnly, mAvailableServices, mCellIdentity, mVoiceSpecificInfo, mDataSpecificInfo, mNrState, mRplmn, mIsUsingCarrierAggregation); @@ -706,7 +752,7 @@ public final class NetworkRegistrationInfo implements Parcelable { return mDomain == other.mDomain && mTransportType == other.mTransportType && mRegistrationState == other.mRegistrationState - && mInitialRegistrationState == other.mInitialRegistrationState + && mNetworkRegistrationState == other.mNetworkRegistrationState && mRoamingType == other.mRoamingType && mAccessNetworkTechnology == other.mAccessNetworkTechnology && mRejectCause == other.mRejectCause @@ -729,7 +775,7 @@ public final class NetworkRegistrationInfo implements Parcelable { dest.writeInt(mDomain); dest.writeInt(mTransportType); dest.writeInt(mRegistrationState); - dest.writeInt(mInitialRegistrationState); + dest.writeInt(mNetworkRegistrationState); dest.writeInt(mRoamingType); dest.writeInt(mAccessNetworkTechnology); dest.writeInt(mRejectCause); @@ -826,7 +872,7 @@ public final class NetworkRegistrationInfo implements Parcelable { private int mTransportType; @RegistrationState - private int mInitialRegistrationState; + private int mNetworkRegistrationState; @NetworkType private int mAccessNetworkTechnology; @@ -856,6 +902,31 @@ public final class NetworkRegistrationInfo implements Parcelable { public Builder() {} /** + * Builder from the existing {@link NetworkRegistrationInfo}. + * + * @param nri The network registration info object. + * @hide + */ + public Builder(@NonNull NetworkRegistrationInfo nri) { + mDomain = nri.mDomain; + mTransportType = nri.mTransportType; + mNetworkRegistrationState = nri.mNetworkRegistrationState; + mAccessNetworkTechnology = nri.mAccessNetworkTechnology; + mRejectCause = nri.mRejectCause; + mEmergencyOnly = nri.mEmergencyOnly; + mAvailableServices = new ArrayList<>(nri.mAvailableServices); + mCellIdentity = nri.mCellIdentity; + if (nri.mDataSpecificInfo != null) { + mDataSpecificRegistrationInfo = new DataSpecificRegistrationInfo( + nri.mDataSpecificInfo); + } + if (nri.mVoiceSpecificInfo != null) { + mVoiceSpecificRegistrationInfo = new VoiceSpecificRegistrationInfo( + nri.mVoiceSpecificInfo); + } + } + + /** * Set the network domain. * * @param domain Network domain. @@ -887,7 +958,7 @@ public final class NetworkRegistrationInfo implements Parcelable { * @return The same instance of the builder. */ public @NonNull Builder setRegistrationState(@RegistrationState int registrationState) { - mInitialRegistrationState = registrationState; + mNetworkRegistrationState = registrationState; return this; } @@ -1006,7 +1077,7 @@ public final class NetworkRegistrationInfo implements Parcelable { */ @SystemApi public @NonNull NetworkRegistrationInfo build() { - return new NetworkRegistrationInfo(mDomain, mTransportType, mInitialRegistrationState, + return new NetworkRegistrationInfo(mDomain, mTransportType, mNetworkRegistrationState, mAccessNetworkTechnology, mRejectCause, mEmergencyOnly, mAvailableServices, mCellIdentity, mRplmn, mVoiceSpecificRegistrationInfo, mDataSpecificRegistrationInfo); diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index ac740166a024..03e019d2edde 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -631,11 +631,17 @@ public class ServiceState implements Parcelable { } /** - * Get current roaming indicator of phone + * Get current roaming indicator of phone. This roaming state could be overridden by the carrier + * config. * (note: not just decoding from TS 27.007 7.2) - * + * @see TelephonyDisplayInfo#isRoaming() for visualization purpose. * @return true if TS 27.007 7.2 roaming is true * and ONS is different from SPN + * @see CarrierConfigManager#KEY_FORCE_HOME_NETWORK_BOOL + * @see CarrierConfigManager#KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY */ public boolean getRoaming() { return getVoiceRoaming() || getDataRoaming(); @@ -650,8 +656,9 @@ public class ServiceState implements Parcelable { public boolean getVoiceRoaming() { return getVoiceRoamingType() != ROAMING_TYPE_NOT_ROAMING; } + /** - * Get current voice network roaming type + * Get current voice roaming type. This roaming type could be overridden by the carrier config. * @return roaming type * @hide */ @@ -701,7 +708,7 @@ public class ServiceState implements Parcelable { } /** - * Get current data network roaming type + * Get current data roaming type. This roaming type could be overridden by the carrier config. * @return roaming type * @hide */ @@ -1207,8 +1214,13 @@ public class ServiceState implements Parcelable { /** * Initialize the service state. Set everything to the default value. + * + * @param legacyMode {@code true} if the device is on IWLAN legacy mode, where IWLAN is + * considered as a RAT on WWAN {@link NetworkRegistrationInfo}. {@code false} if the device + * is on AP-assisted mode, where IWLAN should be reported through WLAN. + * {@link NetworkRegistrationInfo}. */ - private void init() { + private void init(boolean legacyMode) { if (DBG) Rlog.d(LOG_TAG, "init"); mVoiceRegState = STATE_OUT_OF_SERVICE; mDataRegState = STATE_OUT_OF_SERVICE; @@ -1240,11 +1252,13 @@ public class ServiceState implements Parcelable { .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WWAN) .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN) .build()); - addNetworkRegistrationInfo(new NetworkRegistrationInfo.Builder() - .setDomain(NetworkRegistrationInfo.DOMAIN_PS) - .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WLAN) - .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN) - .build()); + if (!legacyMode) { + addNetworkRegistrationInfo(new NetworkRegistrationInfo.Builder() + .setDomain(NetworkRegistrationInfo.DOMAIN_PS) + .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WLAN) + .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_UNKNOWN) + .build()); + } } mOperatorAlphaLongRaw = null; mOperatorAlphaShortRaw = null; @@ -1253,11 +1267,11 @@ public class ServiceState implements Parcelable { } public void setStateOutOfService() { - init(); + init(true); } public void setStateOff() { - init(); + init(true); mVoiceRegState = STATE_POWER_OFF; mDataRegState = STATE_POWER_OFF; } @@ -1265,11 +1279,14 @@ public class ServiceState implements Parcelable { /** * Set the service state to out-of-service * + * @param legacyMode {@code true} if the device is on IWLAN legacy mode, where IWLAN is + * considered as a RAT on WWAN {@link NetworkRegistrationInfo}. {@code false} if the device + * is on AP-assisted mode, where IWLAN should be reported through WLAN. * @param powerOff {@code true} if this is a power off case (i.e. Airplane mode on). * @hide */ - public void setOutOfService(boolean powerOff) { - init(); + public void setOutOfService(boolean legacyMode, boolean powerOff) { + init(legacyMode); if (powerOff) { mVoiceRegState = STATE_POWER_OFF; mDataRegState = STATE_POWER_OFF; diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index d670e5592c42..1cf2969ea9b5 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -2245,6 +2245,7 @@ public final class SmsManager { RESULT_SMS_SEND_RETRY_FAILED, RESULT_REMOTE_EXCEPTION, RESULT_NO_DEFAULT_SMS_APP, + RESULT_USER_NOT_ALLOWED, RESULT_RIL_RADIO_NOT_AVAILABLE, RESULT_RIL_SMS_SEND_FAIL_RETRY, RESULT_RIL_NETWORK_REJECT, @@ -2425,6 +2426,13 @@ public final class SmsManager { */ public static final int RESULT_NO_DEFAULT_SMS_APP = 32; + /** + * User is not associated with the subscription. + * TODO(b/263279115): Make this error code public. + * @hide + */ + public static final int RESULT_USER_NOT_ALLOWED = 33; + // Radio Error results /** diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 0cfd96e46c4b..d590ce22e9df 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1116,6 +1116,14 @@ public class SubscriptionManager { */ public static final String USER_HANDLE = SimInfo.COLUMN_USER_HANDLE; + /** + * TelephonyProvider column name for satellite enabled. + * By default, it's disabled. + * <P>Type: INTEGER (int)</P> + * @hide + */ + public static final String SATELLITE_ENABLED = SimInfo.COLUMN_SATELLITE_ENABLED; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"USAGE_SETTING_"}, @@ -1707,31 +1715,30 @@ public class SubscriptionManager { } /** - * Get all subscription info records from SIMs that are inserted now or were inserted before. + * Get all subscription info records from SIMs that are inserted now or previously inserted. * * <p> * If the caller does not have {@link Manifest.permission#READ_PHONE_NUMBERS} permission, * {@link SubscriptionInfo#getNumber()} will return empty string. * If the caller does not have {@link Manifest.permission#USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER}, - * {@link SubscriptionInfo#getIccId()} and {@link SubscriptionInfo#getCardString()} will return - * empty string, and {@link SubscriptionInfo#getGroupUuid()} will return {@code null}. + * {@link SubscriptionInfo#getIccId()} will return an empty string, and + * {@link SubscriptionInfo#getGroupUuid()} will return {@code null}. * * <p> - * The carrier app will always have full {@link SubscriptionInfo} for the subscriptions - * that it has carrier privilege. + * The carrier app will only get the list of subscriptions that it has carrier privilege on, + * but will have non-stripped {@link SubscriptionInfo} in the list. * * @return List of all {@link SubscriptionInfo} records from SIMs that are inserted or - * inserted before. Sorted by {@link SubscriptionInfo#getSimSlotIndex()}, then + * previously inserted. Sorted by {@link SubscriptionInfo#getSimSlotIndex()}, then * {@link SubscriptionInfo#getSubscriptionId()}. * - * @hide + * @throws SecurityException if callers do not hold the required permission. */ + @NonNull @RequiresPermission(anyOf = { Manifest.permission.READ_PHONE_STATE, - Manifest.permission.READ_PRIVILEGED_PHONE_STATE, "carrier privileges", }) - @NonNull public List<SubscriptionInfo> getAllSubscriptionInfoList() { List<SubscriptionInfo> result = null; try { @@ -2205,13 +2212,23 @@ public class SubscriptionManager { } /** - * Get an array of Subscription Ids for specified slot Index. - * @param slotIndex the slot index. - * @return subscription Ids or null if the given slot Index is not valid or there are no active - * subscriptions in the slot. + * Get an array of subscription ids for specified logical SIM slot Index. + * + * @param slotIndex The logical SIM slot index. + * + * @return subscription Ids or {@code null} if the given slot index is not valid or there are + * no active subscription in the slot. In the implementation today, there will be no more + * than one subscriptions per logical SIM slot. + * + * @deprecated Use {@link #getSubscriptionId(int)} instead. */ + @Deprecated @Nullable public int[] getSubscriptionIds(int slotIndex) { + int subId = getSubscriptionId(slotIndex); + if (!isValidSubscriptionId(subId)) { + return null; + } return new int[]{getSubscriptionId(slotIndex)}; } @@ -2238,12 +2255,10 @@ public class SubscriptionManager { } /** - * Get the subscription id for specified slot index. + * Get the subscription id for specified logical SIM slot index. * - * @param slotIndex Logical SIM slot index. + * @param slotIndex The logical SIM slot index. * @return The subscription id. {@link #INVALID_SUBSCRIPTION_ID} if SIM is absent. - * - * @hide */ public static int getSubscriptionId(int slotIndex) { if (!isValidSlotIndex(slotIndex)) { @@ -4387,5 +4402,70 @@ public class SubscriptionManager { } return null; } + + /** + * Check if subscription and user are associated with each other. + * + * @param subscriptionId the subId of the subscription + * @param userHandle user handle of the user + * @return {@code true} if subscription is associated with user + * {code true} if there are no subscriptions on device + * else {@code false} if subscription is not associated with user. + * + * @throws IllegalArgumentException if subscription is invalid. + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) + public boolean isSubscriptionAssociatedWithUser(int subscriptionId, + @NonNull UserHandle userHandle) { + if (!isValidSubscriptionId(subscriptionId)) { + throw new IllegalArgumentException("[isSubscriptionAssociatedWithUser]: " + + "Invalid subscriptionId: " + subscriptionId); + } + + try { + ISub iSub = TelephonyManager.getSubscriptionService(); + if (iSub != null) { + return iSub.isSubscriptionAssociatedWithUser(subscriptionId, userHandle); + } else { + throw new IllegalStateException("[isSubscriptionAssociatedWithUser]: " + + "subscription service unavailable"); + } + } catch (RemoteException ex) { + ex.rethrowAsRuntimeException(); + } + return false; + } + + /** + * Get list of subscriptions associated with user. + * + * @param userHandle user handle of the user + * @return list of subscriptionInfo associated with the user. + * + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) + public @NonNull List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser( + @NonNull UserHandle userHandle) { + try { + ISub iSub = TelephonyManager.getSubscriptionService(); + if (iSub != null) { + return iSub.getSubscriptionInfoListAssociatedWithUser(userHandle); + } else { + throw new IllegalStateException("[getSubscriptionInfoListAssociatedWithUser]: " + + "subscription service unavailable"); + } + } catch (RemoteException ex) { + ex.rethrowAsRuntimeException(); + } + return new ArrayList<>(); + } } diff --git a/telephony/java/android/telephony/TelephonyDisplayInfo.java b/telephony/java/android/telephony/TelephonyDisplayInfo.java index f4e2ade643c7..e01b10eed4db 100644 --- a/telephony/java/android/telephony/TelephonyDisplayInfo.java +++ b/telephony/java/android/telephony/TelephonyDisplayInfo.java @@ -87,10 +87,12 @@ public final class TelephonyDisplayInfo implements Parcelable { public static final int OVERRIDE_NETWORK_TYPE_NR_ADVANCED = 5; @NetworkType - private final int mNetworkType; + private final int mNetworkType; @OverrideNetworkType - private final int mOverrideNetworkType; + private final int mOverrideNetworkType; + + private final boolean mIsRoaming; /** * Constructor @@ -98,18 +100,37 @@ public final class TelephonyDisplayInfo implements Parcelable { * @param networkType Current packet-switching cellular network type * @param overrideNetworkType The override network type * + * @deprecated will not use this constructor anymore. * @hide */ + @Deprecated public TelephonyDisplayInfo(@NetworkType int networkType, @OverrideNetworkType int overrideNetworkType) { + this(networkType, overrideNetworkType, false); + } + + /** + * Constructor + * + * @param networkType Current packet-switching cellular network type + * @param overrideNetworkType The override network type + * @param isRoaming True if the device is roaming after override. + * + * @hide + */ + public TelephonyDisplayInfo(@NetworkType int networkType, + @OverrideNetworkType int overrideNetworkType, + boolean isRoaming) { mNetworkType = networkType; mOverrideNetworkType = overrideNetworkType; + mIsRoaming = isRoaming; } /** @hide */ public TelephonyDisplayInfo(Parcel p) { mNetworkType = p.readInt(); mOverrideNetworkType = p.readInt(); + mIsRoaming = p.readBoolean(); } /** @@ -135,10 +156,25 @@ public final class TelephonyDisplayInfo implements Parcelable { return mOverrideNetworkType; } + /** + * Get device is roaming or not. Note the isRoaming is for market branding or visualization + * purposes only. It cannot be treated as the actual roaming device is camped on. + * + * @return True if the device is registered on roaming network overridden by config. + * @see CarrierConfigManager#KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_GSM_NONROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_CDMA_ROAMING_NETWORKS_STRING_ARRAY + * @see CarrierConfigManager#KEY_CDMA_NONROAMING_NETWORKS_STRING_ARRAY + */ + public boolean isRoaming() { + return mIsRoaming; + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mNetworkType); dest.writeInt(mOverrideNetworkType); + dest.writeBoolean(mIsRoaming); } public static final @NonNull Parcelable.Creator<TelephonyDisplayInfo> CREATOR = @@ -165,12 +201,13 @@ public final class TelephonyDisplayInfo implements Parcelable { if (o == null || getClass() != o.getClass()) return false; TelephonyDisplayInfo that = (TelephonyDisplayInfo) o; return mNetworkType == that.mNetworkType - && mOverrideNetworkType == that.mOverrideNetworkType; + && mOverrideNetworkType == that.mOverrideNetworkType + && mIsRoaming == that.mIsRoaming; } @Override public int hashCode() { - return Objects.hash(mNetworkType, mOverrideNetworkType); + return Objects.hash(mNetworkType, mOverrideNetworkType, mIsRoaming); } /** @@ -195,6 +232,7 @@ public final class TelephonyDisplayInfo implements Parcelable { @Override public String toString() { return "TelephonyDisplayInfo {network=" + TelephonyManager.getNetworkTypeName(mNetworkType) - + ", override=" + overrideNetworkTypeToString(mOverrideNetworkType) + "}"; + + ", overrideNetwork=" + overrideNetworkTypeToString(mOverrideNetworkType) + + ", isRoaming=" + mIsRoaming + "}"; } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index d23b75bd8c45..fd5ec258d697 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17027,4 +17027,44 @@ public class TelephonyManager { } return TelephonyManager.SIM_STATE_UNKNOWN; } + + /** + * Convert SIM state into string. + * + * @param state SIM state. + * @return SIM state in string format. + * + * @hide + */ + @NonNull + public static String simStateToString(@SimState int state) { + switch (state) { + case TelephonyManager.SIM_STATE_UNKNOWN: + return "UNKNOWN"; + case TelephonyManager.SIM_STATE_ABSENT: + return "ABSENT"; + case TelephonyManager.SIM_STATE_PIN_REQUIRED: + return "PIN_REQUIRED"; + case TelephonyManager.SIM_STATE_PUK_REQUIRED: + return "PUK_REQUIRED"; + case TelephonyManager.SIM_STATE_NETWORK_LOCKED: + return "NETWORK_LOCKED"; + case TelephonyManager.SIM_STATE_READY: + return "READY"; + case TelephonyManager.SIM_STATE_NOT_READY: + return "NOT_READY"; + case TelephonyManager.SIM_STATE_PERM_DISABLED: + return "PERM_DISABLED"; + case TelephonyManager.SIM_STATE_CARD_IO_ERROR: + return "CARD_IO_ERROR"; + case TelephonyManager.SIM_STATE_CARD_RESTRICTED: + return "CARD_RESTRICTED"; + case TelephonyManager.SIM_STATE_LOADED: + return "LOADED"; + case TelephonyManager.SIM_STATE_PRESENT: + return "PRESENT"; + default: + return "UNKNOWN(" + state + ")"; + } + } } diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index c5f6902062ff..25a714a4bb81 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -326,4 +326,34 @@ interface ISub { * @throws IllegalArgumentException if subId is invalid. */ UserHandle getSubscriptionUserHandle(int subId); + + /** + * Check if subscription and user are associated with each other. + * + * @param subscriptionId the subId of the subscription + * @param userHandle user handle of the user + * @return {@code true} if subscription is associated with user + * {code true} if there are no subscriptions on device + * else {@code false} if subscription is not associated with user. + * + * @throws IllegalArgumentException if subscription is invalid. + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + boolean isSubscriptionAssociatedWithUser(int subscriptionId, in UserHandle userHandle); + + /** + * Get list of subscriptions associated with user. + * + * @param userHandle user handle of the user + * @return list of subscriptionInfo associated with the user. + * + * @throws SecurityException if the caller doesn't have permissions required. + * @throws IllegalStateException if subscription service is not available. + * + * @hide + */ + List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser(in UserHandle userHandle); } diff --git a/tests/SoundTriggerTestApp/OWNERS b/tests/SoundTriggerTestApp/OWNERS index 9db19a37812b..a0fcfc52704d 100644 --- a/tests/SoundTriggerTestApp/OWNERS +++ b/tests/SoundTriggerTestApp/OWNERS @@ -1,2 +1,2 @@ -include /core/java/android/media/soundtrigger/OWNERS +include /media/java/android/media/soundtrigger/OWNERS mdooley@google.com diff --git a/tests/SoundTriggerTests/OWNERS b/tests/SoundTriggerTests/OWNERS index 816bc6bba639..1e41886fe716 100644 --- a/tests/SoundTriggerTests/OWNERS +++ b/tests/SoundTriggerTests/OWNERS @@ -1 +1 @@ -include /core/java/android/media/soundtrigger/OWNERS +include /media/java/android/media/soundtrigger/OWNERS diff --git a/tests/utils/testutils/java/android/os/test/FakePermissionEnforcer.java b/tests/utils/testutils/java/android/os/test/FakePermissionEnforcer.java new file mode 100644 index 000000000000..b94bb41c0988 --- /dev/null +++ b/tests/utils/testutils/java/android/os/test/FakePermissionEnforcer.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.test; + +import static android.permission.PermissionManager.PERMISSION_GRANTED; +import static android.permission.PermissionManager.PERMISSION_HARD_DENIED; + +import android.annotation.NonNull; +import android.content.AttributionSource; +import android.os.PermissionEnforcer; + +import java.util.HashSet; +import java.util.Set; + +/** + * Fake for {@link PermissionEnforcer}. Useful for tests wanting to mock the + * permission checks of an AIDL service. FakePermissionEnforcer may be passed + * to the constructor of the AIDL-generated Stub class. + * + */ +public class FakePermissionEnforcer extends PermissionEnforcer { + private Set<String> mGranted; + + public FakePermissionEnforcer() { + mGranted = new HashSet(); + } + + public void grant(String permission) { + mGranted.add(permission); + } + + public void revoke(String permission) { + mGranted.remove(permission); + } + + private boolean granted(String permission) { + return mGranted.contains(permission); + } + + @Override + protected int checkPermission(@NonNull String permission, + @NonNull AttributionSource source) { + return granted(permission) ? PERMISSION_GRANTED : PERMISSION_HARD_DENIED; + } + + @Override + protected int checkPermission(@NonNull String permission, int pid, int uid) { + return granted(permission) ? PERMISSION_GRANTED : PERMISSION_HARD_DENIED; + } +} diff --git a/tests/utils/testutils/java/android/os/test/OWNERS b/tests/utils/testutils/java/android/os/test/OWNERS new file mode 100644 index 000000000000..3a9129e1bb69 --- /dev/null +++ b/tests/utils/testutils/java/android/os/test/OWNERS @@ -0,0 +1 @@ +per-file FakePermissionEnforcer.java = file:/tests/EnforcePermission/OWNERS diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java index b313c9fc6c28..73a0a6183cb6 100644 --- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java @@ -17,6 +17,7 @@ package android.net.vcn; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static org.junit.Assert.assertEquals; @@ -160,6 +161,37 @@ public class VcnConfigTest { assertNotEquals(config, configNotEqual); } + private VcnConfig buildConfigRestrictTransportTest(boolean isTestMode) throws Exception { + VcnConfig.Builder builder = + new VcnConfig.Builder(mContext) + .setRestrictedUnderlyingNetworkTransports(Set.of(TRANSPORT_TEST)); + if (isTestMode) { + builder.setIsTestModeProfile(); + } + + for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) { + builder.addGatewayConnectionConfig(gatewayConnectionConfig); + } + + return builder.build(); + } + + @Test + public void testRestrictTransportTestInTestModeProfile() throws Exception { + final VcnConfig config = buildConfigRestrictTransportTest(true /* isTestMode */); + assertEquals(Set.of(TRANSPORT_TEST), config.getRestrictedUnderlyingNetworkTransports()); + } + + @Test + public void testRestrictTransportTestInNonTestModeProfile() throws Exception { + try { + buildConfigRestrictTransportTest(false /* isTestMode */); + fail("Expected exception because the config is not a test mode profile"); + } catch (Exception expected) { + + } + } + @Test public void testParceling() { final VcnConfig config = buildTestConfig(mContext); diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 075bc5e5214e..4123f8070e36 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -17,7 +17,9 @@ package com.android.server; import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; @@ -67,7 +69,6 @@ import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; -import android.net.TelephonyNetworkSpecifier; import android.net.Uri; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; @@ -128,6 +129,15 @@ public class VcnManagementServiceTest { private static final VcnConfig TEST_VCN_CONFIG; private static final VcnConfig TEST_VCN_CONFIG_PKG_2; private static final int TEST_UID = Process.FIRST_APPLICATION_UID; + private static final String TEST_IFACE_NAME = "TEST_IFACE"; + private static final String TEST_IFACE_NAME_2 = "TEST_IFACE2"; + private static final LinkProperties TEST_LP_1 = new LinkProperties(); + private static final LinkProperties TEST_LP_2 = new LinkProperties(); + + static { + TEST_LP_1.setInterfaceName(TEST_IFACE_NAME); + TEST_LP_2.setInterfaceName(TEST_IFACE_NAME_2); + } static { final Context mockConfigContext = mock(Context.class); @@ -1034,8 +1044,7 @@ public class VcnManagementServiceTest { setupSubscriptionAndStartVcn(subId, subGrp, isVcnActive); return mVcnMgmtSvc.getUnderlyingNetworkPolicy( - getNetworkCapabilitiesBuilderForTransport(subId, transport).build(), - new LinkProperties()); + getNetworkCapabilitiesBuilderForTransport(subId, transport).build(), TEST_LP_1); } private void checkGetRestrictedTransportsFromCarrierConfig( @@ -1260,7 +1269,7 @@ public class VcnManagementServiceTest { false /* expectRestricted */); } - private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) { + private void setupTrackedNetwork(NetworkCapabilities caps, LinkProperties lp) { mVcnMgmtSvc.systemReady(); final ArgumentCaptor<NetworkCallback> captor = @@ -1269,7 +1278,10 @@ public class VcnManagementServiceTest { .registerNetworkCallback( eq(new NetworkRequest.Builder().clearCapabilities().build()), captor.capture()); - captor.getValue().onCapabilitiesChanged(mock(Network.class, CALLS_REAL_METHODS), caps); + + Network mockNetwork = mock(Network.class, CALLS_REAL_METHODS); + captor.getValue().onCapabilitiesChanged(mockNetwork, caps); + captor.getValue().onLinkPropertiesChanged(mockNetwork, lp); } @Test @@ -1279,7 +1291,7 @@ public class VcnManagementServiceTest { getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); // Trigger test without VCN instance alive; expect restart due to change of NOT_RESTRICTED // immutable capability @@ -1288,7 +1300,7 @@ public class VcnManagementServiceTest { getNetworkCapabilitiesBuilderForTransport( TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .build(), - new LinkProperties()); + TEST_LP_1); assertTrue(policy.isTeardownRequested()); } @@ -1298,7 +1310,7 @@ public class VcnManagementServiceTest { final NetworkCapabilities existingNetworkCaps = getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); final VcnUnderlyingNetworkPolicy policy = startVcnAndGetPolicyForTransport( @@ -1315,7 +1327,7 @@ public class VcnManagementServiceTest { .addCapability(NET_CAPABILITY_NOT_RESTRICTED) .removeCapability(NET_CAPABILITY_IMS) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); final VcnUnderlyingNetworkPolicy policy = mVcnMgmtSvc.getUnderlyingNetworkPolicy( @@ -1336,7 +1348,7 @@ public class VcnManagementServiceTest { new NetworkCapabilities.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) - .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID_2)) + .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID_2)) .build(); VcnUnderlyingNetworkPolicy policy = @@ -1346,6 +1358,38 @@ public class VcnManagementServiceTest { assertEquals(nc, policy.getMergedNetworkCapabilities()); } + /** + * Checks that networks with similar capabilities do not clobber each other. + * + * <p>In previous iterations, the VcnMgmtSvc used capability-matching to check if a network + * undergoing policy checks were the same as an existing networks. However, this meant that if + * there were newly added capabilities that the VCN did not check, two networks differing only + * by that capability would restart each other constantly. + */ + @Test + public void testGetUnderlyingNetworkPolicySimilarNetworks() throws Exception { + NetworkCapabilities nc1 = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_INTERNET) + .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID_2)) + .build(); + + NetworkCapabilities nc2 = + new NetworkCapabilities.Builder(nc1) + .addCapability(NET_CAPABILITY_ENTERPRISE) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(); + + setupTrackedNetwork(nc1, TEST_LP_1); + + VcnUnderlyingNetworkPolicy policy = mVcnMgmtSvc.getUnderlyingNetworkPolicy(nc2, TEST_LP_2); + + assertFalse(policy.isTeardownRequested()); + assertEquals(nc2, policy.getMergedNetworkCapabilities()); + } + @Test(expected = SecurityException.class) public void testGetUnderlyingNetworkPolicyInvalidPermission() { doReturn(PackageManager.PERMISSION_DENIED) diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java index 629e988495cc..226604108522 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java @@ -95,6 +95,7 @@ public class NetworkPriorityClassifierTest { private static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES = new NetworkCapabilities.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .setSignalStrength(WIFI_RSSI) .setSsid(SSID) .setLinkUpstreamBandwidthKbps(LINK_UPSTREAM_BANDWIDTH_KBPS) @@ -509,12 +510,14 @@ public class NetworkPriorityClassifierTest { VcnCellUnderlyingNetworkTemplate template, boolean expectMatch) { assertEquals( expectMatch, - checkMatchesCellPriorityRule( + checkMatchesPriorityRule( mVcnContext, template, mCellNetworkRecord, SUB_GROUP, - mSubscriptionSnapshot)); + mSubscriptionSnapshot, + null /* currentlySelected */, + null /* carrierConfig */)); } @Test diff --git a/tools/lint/README.md b/tools/lint/README.md index b534b62cb395..b235ad60c799 100644 --- a/tools/lint/README.md +++ b/tools/lint/README.md @@ -1,15 +1,44 @@ -# Android Framework Lint Checker +# Android Lint Checks for AOSP -Custom lint checks written here are going to be executed for modules that opt in to those (e.g. any +Custom Android Lint checks are written here to be executed against java modules +in AOSP. These checks are broken down into two subdirectories: + +1. [Global Checks](#android-global-lint-checker) +2. [Framework Checks](#android-framework-lint-checker) + +# [Android Global Lint Checker](/global) +Checks written here are executed for the entire tree. The `AndroidGlobalLintChecker` +build target produces a jar file that is included in the overall build output +(`AndroidGlobalLintChecker.jar`). This file is then downloaded as a prebuilt under the +`prebuilts/cmdline-tools` subproject, and included by soong with all invocations of lint. + +## How to add new global lint checks +1. Write your detector with its issues and put it into + `global/checks/src/main/java/com/google/android/lint`. +2. Add your detector's issues into `AndroidGlobalIssueRegistry`'s `issues` + field. +3. Write unit tests for your detector in one file and put it into + `global/checks/test/java/com/google/android/lint`. +4. Have your change reviewed and merged. Once your change is merged, + obtain a build number from a successful build that includes your change. +5. Run `prebuilts/cmdline-tools/update-android-global-lint-checker.sh + <build_number>`. The script will create a commit that you can upload for + approval to the `prebuilts/cmdline-tools` subproject. +6. Done! Your lint check should be applied in lint report builds across the + entire tree! + +# [Android Framework Lint Checker](/framework) + +Checks written here are going to be executed for modules that opt in to those (e.g. any `services.XXX` module) and results will be automatically reported on CLs on gerrit. -## How to add new lint checks +## How to add new framework lint checks 1. Write your detector with its issues and put it into - `checks/src/main/java/com/google/android/lint`. + `framework/checks/src/main/java/com/google/android/lint`. 2. Add your detector's issues into `AndroidFrameworkIssueRegistry`'s `issues` field. 3. Write unit tests for your detector in one file and put it into - `checks/test/java/com/google/android/lint`. + `framework/checks/test/java/com/google/android/lint`. 4. Done! Your lint checks should be applied in lint report builds for modules that include `AndroidFrameworkLintChecker`. @@ -44,7 +73,11 @@ m out/soong/.intermediates/frameworks/base/services/autofill/services.autofill/a environment variable with the id of the lint. For example: `ANDROID_LINT_CHECK=UnusedTokenOfOriginalCallingIdentity m out/[...]/lint-report.html` -## Create or update a baseline +# How to apply automatic fixes suggested by lint + +See [lint_fix](fix/README.md) + +# Create or update a baseline Baseline files can be used to silence known errors (and warnings) that are deemed to be safe. When there is a lint-baseline.xml file in the root folder of the java library, soong will @@ -75,9 +108,10 @@ locally change the soong code in [lint.go](http://cs/aosp-master/build/soong/java/lint.go;l=451;rcl=2e778d5bc4a8d1d77b4f4a3029a4a254ad57db75) adding `cmd.Flag("--nowarn")` and running lint again. -## Documentation +# Documentation - [Android Lint Docs](https://googlesamples.github.io/android-custom-lint-rules/) +- [Lint Check Unit Testing](https://googlesamples.github.io/android-custom-lint-rules/api-guide/unit-testing.md.html) - [Android Lint source files](https://source.corp.google.com/studio-main/tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/) - [PSI source files](https://github.com/JetBrains/intellij-community/tree/master/java/java-psi-api/src/com/intellij/psi) - [UAST source files](https://upsource.jetbrains.com/idea-ce/structure/idea-ce-7b9b8cc138bbd90aec26433f82cd2c6838694003/uast/uast-common/src/org/jetbrains/uast) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt deleted file mode 100644 index 8f553abfee31..000000000000 --- a/tools/lint/checks/src/main/java/com/google/android/lint/EnforcePermissionDetector.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2022 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.google.android.lint - -import com.android.tools.lint.detector.api.AnnotationInfo -import com.android.tools.lint.detector.api.AnnotationOrigin -import com.android.tools.lint.detector.api.AnnotationUsageInfo -import com.android.tools.lint.detector.api.AnnotationUsageType -import com.android.tools.lint.detector.api.ConstantEvaluator -import com.android.tools.lint.detector.api.Category -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Implementation -import com.android.tools.lint.detector.api.Issue -import com.android.tools.lint.detector.api.JavaContext -import com.android.tools.lint.detector.api.Scope -import com.android.tools.lint.detector.api.Severity -import com.android.tools.lint.detector.api.SourceCodeScanner -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UElement - -/** - * Lint Detector that ensures that any method overriding a method annotated - * with @EnforcePermission is also annotated with the exact same annotation. - * The intent is to surface the effective permission checks to the service - * implementations. - */ -class EnforcePermissionDetector : Detector(), SourceCodeScanner { - - val ENFORCE_PERMISSION = "android.annotation.EnforcePermission" - - override fun applicableAnnotations(): List<String> { - return listOf(ENFORCE_PERMISSION) - } - - private fun areAnnotationsEquivalent( - context: JavaContext, - anno1: PsiAnnotation, - anno2: PsiAnnotation - ): Boolean { - if (anno1.qualifiedName != anno2.qualifiedName) { - return false - } - val attr1 = anno1.parameterList.attributes - val attr2 = anno2.parameterList.attributes - if (attr1.size != attr2.size) { - return false - } - for (i in attr1.indices) { - if (attr1[i].name != attr2[i].name) { - return false - } - val value1 = attr1[i].value - val value2 = attr2[i].value - if (value1 == null && value2 == null) { - continue - } - if (value1 == null || value2 == null) { - return false - } - val v1 = ConstantEvaluator.evaluate(context, value1) - val v2 = ConstantEvaluator.evaluate(context, value2) - if (v1 != v2) { - return false - } - } - return true - } - - override fun visitAnnotationUsage( - context: JavaContext, - element: UElement, - annotationInfo: AnnotationInfo, - usageInfo: AnnotationUsageInfo - ) { - if (usageInfo.type == AnnotationUsageType.EXTENDS) { - val newClass = element.sourcePsi?.parent?.parent as PsiClass - val extendedClass: PsiClass = usageInfo.referenced as PsiClass - val newAnnotation = newClass.getAnnotation(ENFORCE_PERMISSION) - val extendedAnnotation = extendedClass.getAnnotation(ENFORCE_PERMISSION)!! - - val location = context.getLocation(element) - val newClassName = newClass.qualifiedName - val extendedClassName = extendedClass.qualifiedName - if (newAnnotation == null) { - val msg = "The class $newClassName extends the class $extendedClassName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $newClassName." - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (!areAnnotationsEquivalent(context, newAnnotation, extendedAnnotation)) { - val msg = "The class $newClassName is annotated with ${newAnnotation.text} " + - "which differs from the parent class $extendedClassName: " + - "${extendedAnnotation.text}. The same annotation must be used for " + - "both classes." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && - annotationInfo.origin == AnnotationOrigin.METHOD) { - val overridingMethod = element.sourcePsi as PsiMethod - val overriddenMethod = usageInfo.referenced as PsiMethod - val overridingAnnotation = overridingMethod.getAnnotation(ENFORCE_PERMISSION) - val overriddenAnnotation = overriddenMethod.getAnnotation(ENFORCE_PERMISSION)!! - - val location = context.getLocation(element) - val overridingClass = overridingMethod.parent as PsiClass - val overriddenClass = overriddenMethod.parent as PsiClass - val overridingName = "${overridingClass.name}.${overridingMethod.name}" - val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" - if (overridingAnnotation == null) { - val msg = "The method $overridingName overrides the method $overriddenName which " + - "is annotated with @EnforcePermission. The same annotation must be used " + - "on $overridingName" - context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) - } else if (!areAnnotationsEquivalent( - context, overridingAnnotation, overriddenAnnotation)) { - val msg = "The method $overridingName is annotated with " + - "${overridingAnnotation.text} which differs from the overridden " + - "method $overriddenName: ${overriddenAnnotation.text}. The same " + - "annotation must be used for both methods." - context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) - } - } - } - - companion object { - val EXPLANATION = """ - The @EnforcePermission annotation is used to indicate that the underlying binder code - has already verified the caller's permissions before calling the appropriate method. The - verification code is usually generated by the AIDL compiler, which also takes care of - annotating the generated Java code. - - In order to surface that information to platform developers, the same annotation must be - used on the implementation class or methods. - """ - - val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MissingEnforcePermissionAnnotation", - briefDescription = "Missing @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - - val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( - id = "MismatchingEnforcePermissionAnnotation", - briefDescription = "Incorrect @EnforcePermission annotation on Binder method", - explanation = EXPLANATION, - category = Category.SECURITY, - priority = 6, - severity = Severity.ERROR, - implementation = Implementation( - EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - } -} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt deleted file mode 100644 index f5f4ebee24e0..000000000000 --- a/tools/lint/checks/src/test/java/com/google/android/lint/EnforcePermissionDetectorTest.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2022 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.google.android.lint - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.checks.infrastructure.TestFile -import com.android.tools.lint.checks.infrastructure.TestLintTask -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue - -@Suppress("UnstableApiUsage") -class EnforcePermissionDetectorTest : LintDetectorTest() { - override fun getDetector(): Detector = EnforcePermissionDetector() - - override fun getIssues(): List<Issue> = listOf( - EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION - ) - - override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) - - fun testDoesNotDetectIssuesCorrectAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public class TestClass1 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - import android.annotation.EnforcePermission; - public class TestClass2 extends IFooMethod.Stub { - @Override - @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expectClean() - } - - fun testDetectIssuesMismatchingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public class TestClass3 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the parent class IFoo.Stub: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \ -same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation] -public class TestClass3 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMismatchingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass4 extends IFooMethod.Stub { - @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \ -annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ -which differs from the overridden method Stub.testMethod: \ -@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \ -annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnClass() { - lint().files(java( - """ - package test.pkg; - public class TestClass5 extends IFoo.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \ -the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \ -used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation] -public class TestClass5 extends IFoo.Stub { - ~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - fun testDetectIssuesMissingAnnotationOnMethod() { - lint().files(java( - """ - package test.pkg; - public class TestClass6 extends IFooMethod.Stub { - public void testMethod() {} - } - """).indented(), - *stubs - ) - .run() - .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \ -overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \ -annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] - public void testMethod() {} - ~~~~~~~~~~ -1 errors, 0 warnings""".addLineContinuation()) - } - - /* Stubs */ - - private val interfaceIFooStub: TestFile = java( - """ - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public interface IFoo { - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public static abstract class Stub implements IFoo { - @Override - public void testMethod() {} - } - public void testMethod(); - } - """ - ).indented() - - private val interfaceIFooMethodStub: TestFile = java( - """ - public interface IFooMethod { - public static abstract class Stub implements IFooMethod { - @Override - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod() {} - } - @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) - public void testMethod(); - } - """ - ).indented() - - private val manifestPermissionStub: TestFile = java( - """ - package android.Manifest; - class permission { - public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; - public static final String INTERNET = "android.permission.INTERNET"; - } - """ - ).indented() - - private val enforcePermissionAnnotationStub: TestFile = java( - """ - package android.annotation; - public @interface EnforcePermission {} - """ - ).indented() - - private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, - manifestPermissionStub, enforcePermissionAnnotationStub) - - // Substitutes "backslash + new line" with an empty string to imitate line continuation - private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") -} diff --git a/tools/lint/common/Android.bp b/tools/lint/common/Android.bp new file mode 100644 index 000000000000..898f88b8759c --- /dev/null +++ b/tools/lint/common/Android.bp @@ -0,0 +1,29 @@ +// Copyright (C) 2022 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "AndroidCommonLint", + srcs: ["src/main/java/**/*.kt"], + libs: ["lint_api"], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt new file mode 100644 index 000000000000..0ef165f1523b --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/Constants.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.google.android.lint + +import com.google.android.lint.model.Method + +const val CLASS_STUB = "Stub" +const val CLASS_CONTEXT = "android.content.Context" +const val CLASS_ACTIVITY_MANAGER_SERVICE = "com.android.server.am.ActivityManagerService" +const val CLASS_ACTIVITY_MANAGER_INTERNAL = "android.app.ActivityManagerInternal" + +// Enforce permission APIs +val ENFORCE_PERMISSION_METHODS = listOf( + Method(CLASS_CONTEXT, "checkPermission"), + Method(CLASS_CONTEXT, "checkCallingPermission"), + Method(CLASS_CONTEXT, "checkCallingOrSelfPermission"), + Method(CLASS_CONTEXT, "enforcePermission"), + Method(CLASS_CONTEXT, "enforceCallingPermission"), + Method(CLASS_CONTEXT, "enforceCallingOrSelfPermission"), + Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), + Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") +) + +const val ANNOTATION_PERMISSION_METHOD = "android.annotation.PermissionMethod" +const val ANNOTATION_PERMISSION_NAME = "android.annotation.PermissionName" +const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt new file mode 100644 index 000000000000..9a7f8fa53d87 --- /dev/null +++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.google.android.lint + +import com.android.tools.lint.detector.api.getUMethod +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.UQualifiedReferenceExpression + +fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { + val method = callExpression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(method) +} + +fun hasPermissionMethodAnnotation(method: UMethod): Boolean = + getPermissionMethodAnnotation(method) != null + +fun getPermissionMethodAnnotation(method: UMethod?): UAnnotation? = method?.uAnnotations + ?.firstOrNull { it.qualifiedName == ANNOTATION_PERMISSION_METHOD } + +fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { + it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) +} + +/** + * Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly) + * @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression + * @return UCallExpression, if available + */ +fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? = + when (callOrReferenceCall) { + is UCallExpression -> callOrReferenceCall + is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression + else -> null + } diff --git a/core/java/android/nfc/BeamShareData.aidl b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt index a47e24057a54..3939b6109eaa 100644 --- a/core/java/android/nfc/BeamShareData.aidl +++ b/tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2022 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. @@ -14,6 +14,13 @@ * limitations under the License. */ -package android.nfc; +package com.google.android.lint.model -parcelable BeamShareData; +/** + * Data class to represent a Method + */ +data class Method(val clazz: String, val name: String) { + override fun toString(): String { + return "$clazz#$name" + } +} diff --git a/tools/lint/fix/Android.bp b/tools/lint/fix/Android.bp new file mode 100644 index 000000000000..43f21221ae5a --- /dev/null +++ b/tools/lint/fix/Android.bp @@ -0,0 +1,33 @@ +// Copyright (C) 2022 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +python_binary_host { + name: "lint_fix", + main: "soong_lint_fix.py", + srcs: ["soong_lint_fix.py"], +} + +python_library_host { + name: "soong_lint_fix", + srcs: ["soong_lint_fix.py"], +} diff --git a/tools/lint/fix/README.md b/tools/lint/fix/README.md new file mode 100644 index 000000000000..a5ac2be1c18a --- /dev/null +++ b/tools/lint/fix/README.md @@ -0,0 +1,30 @@ +# Refactoring the platform with lint +Inspiration: go/refactor-the-platform-with-lint\ +**Special Thanks: brufino@, azharaa@, for the prior work that made this all possible** + +## What is this? + +It's a python script that runs the framework linter, +and then (optionally) copies modified files back into the source tree.\ +Why python, you ask? Because python is cool ¯\_(ツ)_/¯. + +Incidentally, this exposes a much simpler way to run individual lint checks +against individual modules, so it's useful beyond applying fixes. + +## Why? + +Lint is not allowed to modify source files directly via lint's `--apply-suggestions` flag. +As a compromise, soong zips up the (potentially) modified sources and leaves them in an intermediate +directory. This script runs the lint, unpacks those files, and copies them back into the tree. + +## How do I run it? +**WARNING: You probably want to commit/stash any changes to your working tree before doing this...** + +``` +source build/envsetup.sh +lunch cf_x86_64_phone-userdebug # or any lunch target +m lint_fix +lint_fix -h +``` + +The script's help output explains things that are omitted here. diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py new file mode 100644 index 000000000000..cd4d778d1dec --- /dev/null +++ b/tools/lint/fix/soong_lint_fix.py @@ -0,0 +1,173 @@ +# Copyright (C) 2022 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 argparse +import json +import os +import shutil +import subprocess +import sys +import zipfile + +ANDROID_BUILD_TOP = os.environ.get("ANDROID_BUILD_TOP") +ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT") +PRODUCT_OUT = ANDROID_PRODUCT_OUT.removeprefix(f"{ANDROID_BUILD_TOP}/") + +SOONG_UI = "build/soong/soong_ui.bash" +PATH_PREFIX = "out/soong/.intermediates" +PATH_SUFFIX = "android_common/lint" +FIX_ZIP = "suggested-fixes.zip" + +class SoongLintFix: + """ + This class creates a command line tool that will + apply lint fixes to the platform via the necessary + combination of soong and shell commands. + + It breaks up these operations into a few "private" methods + that are intentionally exposed so experimental code can tweak behavior. + + The entry point, `run`, will apply lint fixes using the + intermediate `suggested-fixes` directory that soong creates during its + invocation of lint. + + Basic usage: + ``` + from soong_lint_fix import SoongLintFix + + SoongLintFix().run() + ``` + """ + def __init__(self): + self._parser = _setup_parser() + self._args = None + self._kwargs = None + self._path = None + self._target = None + + + def run(self, additional_setup=None, custom_fix=None): + """ + Run the script + """ + self._setup() + self._find_module() + self._lint() + + if not self._args.no_fix: + self._fix() + + if self._args.print: + self._print() + + def _setup(self): + self._args = self._parser.parse_args() + env = os.environ.copy() + if self._args.check: + env["ANDROID_LINT_CHECK"] = self._args.check + if self._args.lint_module: + env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._args.lint_module + + self._kwargs = { + "env": env, + "executable": "/bin/bash", + "shell": True, + } + + os.chdir(ANDROID_BUILD_TOP) + + + def _find_module(self): + print("Refreshing soong modules...") + try: + os.mkdir(ANDROID_PRODUCT_OUT) + except OSError: + pass + subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs) + print("done.") + + with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f: + module_info = json.load(f) + + if self._args.module not in module_info: + sys.exit(f"Module {self._args.module} not found!") + + module_path = module_info[self._args.module]["path"][0] + print(f"Found module {module_path}/{self._args.module}.") + + self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}" + self._target = f"{self._path}/lint-report.txt" + + + def _lint(self): + print("Cleaning up any old lint results...") + try: + os.remove(f"{self._target}") + os.remove(f"{self._path}/{FIX_ZIP}") + except FileNotFoundError: + pass + print("done.") + + print(f"Generating {self._target}") + subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs) + print("done.") + + + def _fix(self): + print("Copying suggested fixes to the tree...") + with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip: + for name in zip.namelist(): + if name.startswith("out") or not name.endswith(".java"): + continue + with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst: + shutil.copyfileobj(src, dst) + print("done.") + + + def _print(self): + print("### lint-report.txt ###", end="\n\n") + with open(self._target, "r") as f: + print(f.read()) + + +def _setup_parser(): + parser = argparse.ArgumentParser(description=""" + This is a python script that applies lint fixes to the platform: + 1. Set up the environment, etc. + 2. Run lint on the specified target. + 3. Copy the modified files, from soong's intermediate directory, back into the tree. + + **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first. + """, formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument('module', + help='The soong build module to run ' + '(e.g. framework-minus-apex or services.core.unboosted)') + + parser.add_argument('--check', + help='Which lint to run. Passed to the ANDROID_LINT_CHECK environment variable.') + + parser.add_argument('--lint-module', + help='Specific lint module to run. Passed to the ANDROID_LINT_CHECK_EXTRA_MODULES environment variable.') + + parser.add_argument('--no-fix', action='store_true', + help='Just build and run the lint, do NOT apply the fixes.') + + parser.add_argument('--print', action='store_true', + help='Print the contents of the generated lint-report.txt at the end.') + + return parser + +if __name__ == "__main__": + SoongLintFix().run()
\ No newline at end of file diff --git a/tools/lint/Android.bp b/tools/lint/framework/Android.bp index 260104145505..30a6daaef2a4 100644 --- a/tools/lint/Android.bp +++ b/tools/lint/framework/Android.bp @@ -1,4 +1,4 @@ -// Copyright (C) 2021 The Android Open Source Project +// Copyright (C) 2022 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. @@ -29,17 +29,14 @@ java_library_host { "auto_service_annotations", "lint_api", ], + static_libs: [ + "AndroidCommonLint", + ], kotlincflags: ["-Xjvm-default=all"], } java_test_host { name: "AndroidFrameworkLintCheckerTest", - // TODO(b/239881504): Since this test was written, Android - // Lint was updated, and now includes classes that were - // compiled for java 15. The soong build doesn't support - // java 15 yet, so we can't compile against "lint". Disable - // the test until java 15 is supported. - enabled: false, srcs: ["checks/src/test/java/**/*.kt"], static_libs: [ "AndroidFrameworkLintChecker", @@ -49,5 +46,19 @@ java_test_host { ], test_options: { unit_test: true, + tradefed_options: [ + { + // lint bundles in some classes that were built with older versions + // of libraries, and no longer load. Since tradefed tries to load + // all classes in the jar to look for tests, it crashes loading them. + // Exclude these classes from tradefed's search. + name: "exclude-paths", + value: "org/apache", + }, + { + name: "exclude-paths", + value: "META-INF", + }, + ], }, } diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt new file mode 100644 index 000000000000..935badecf8d5 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 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.google.android.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.parcel.SaferParcelChecker +import com.google.auto.service.AutoService + +@AutoService(IssueRegistry::class) +@Suppress("UnstableApiUsage") +class AndroidFrameworkIssueRegistry : IssueRegistry() { + override val issues = listOf( + CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, + CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, + CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE, + CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, + // TODO: Currently crashes due to OOM issue + // PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, + PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE, + PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD, + ) + + override val api: Int + get() = CURRENT_API + + override val minApi: Int + get() = 8 + + override val vendor: Vendor = Vendor( + vendorName = "Android", + feedbackUrl = "http://b/issues/new?component=315013", + contact = "brufino@google.com" + ) +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt index 930378b168b2..0c375c358e61 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt @@ -33,6 +33,7 @@ import org.jetbrains.uast.UBlockExpression import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UDeclarationsExpression import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIfExpression import org.jetbrains.uast.ULocalVariable import org.jetbrains.uast.USimpleNameReferenceExpression import org.jetbrains.uast.UTryExpression @@ -52,10 +53,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { private val tokensMap = mutableMapOf<String, Token>() override fun getApplicableUastTypes(): List<Class<out UElement?>> = - listOf(ULocalVariable::class.java, UCallExpression::class.java) + listOf(ULocalVariable::class.java, UCallExpression::class.java) override fun createUastHandler(context: JavaContext): UElementHandler = - TokenUastHandler(context) + TokenUastHandler(context) /** File analysis starts with a clear map */ override fun beforeCheckFile(context: Context) { @@ -70,9 +71,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { override fun afterCheckFile(context: Context) { for (token in tokensMap.values) { context.report( - ISSUE_UNUSED_TOKEN, - token.location, - getIncidentMessageUnusedToken(token.variableName) + ISSUE_UNUSED_TOKEN, + token.location, + getIncidentMessageUnusedToken(token.variableName) ) } tokensMap.clear() @@ -96,9 +97,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val variableName = node.getName() if (!node.isFinal) { context.report( - ISSUE_NON_FINAL_TOKEN, - location, - getIncidentMessageNonFinalToken(variableName) + ISSUE_NON_FINAL_TOKEN, + location, + getIncidentMessageNonFinalToken(variableName) ) } // If there exists an unused variable with the same name in the map, we can imply that @@ -106,9 +107,9 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val oldToken = tokensMap[variableName] if (oldToken != null) { context.report( - ISSUE_UNUSED_TOKEN, - oldToken.location, - getIncidentMessageUnusedToken(oldToken.variableName) + ISSUE_UNUSED_TOKEN, + oldToken.location, + getIncidentMessageUnusedToken(oldToken.variableName) ) } // If there exists a token in the same scope as the current new token, it means that @@ -117,56 +118,84 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { val firstCallToken = findFirstTokenInScope(node) if (firstCallToken != null) { context.report( - ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - createNestedLocation(firstCallToken, location), - getIncidentMessageNestedClearIdentityCallsPrimary( - firstCallToken.variableName, - variableName - ) + ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + createNestedLocation(firstCallToken, location), + getIncidentMessageNestedClearIdentityCallsPrimary( + firstCallToken.variableName, + variableName + ) ) } // If the next statement in the tree is not a try-finally statement, we need to report // the "clearCallingIdentity() is not followed by try-finally" issue val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression) - ?.finallyClause + ?.finallyClause if (finallyClause == null) { context.report( - ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, - location, - getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName) + ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + location, + getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName) ) } tokensMap[variableName] = Token( - variableName, - node.sourcePsi?.getUseScope(), - location, - finallyClause + variableName, + node.sourcePsi?.getUseScope(), + location, + finallyClause ) } - /** - * For every method(): - * - Checks use of caller-aware methods issue - * For every call of Binder.restoreCallingIdentity(token): - * - Checks for restoreCallingIdentity() not in the finally block issue - * - Removes token from tokensMap if token is within the scope of the method - */ override fun visitCallExpression(node: UCallExpression) { + when { + isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> { + checkClearCallingIdentityCall(node) + } + isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> { + checkRestoreCallingIdentityCall(node) + } + isCallerAwareMethod(node) -> checkCallerAwareMethod(node) + } + } + + private fun checkClearCallingIdentityCall(node: UCallExpression) { + var firstNonQualifiedParent = getFirstNonQualifiedParent(node) + // if the call expression is inside a ternary, and the ternary is assigned + // to a variable, then we are still technically assigning + // any result of clearCallingIdentity to a variable + if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) { + firstNonQualifiedParent = firstNonQualifiedParent.uastParent + } + if (firstNonQualifiedParent !is ULocalVariable) { + context.report( + ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE, + context.getLocation(node), + getIncidentMessageResultOfClearIdentityCallNotStoredInVariable( + node.getQualifiedParentOrThis().asRenderString() + ) + ) + } + } + + private fun checkCallerAwareMethod(node: UCallExpression) { val token = findFirstTokenInScope(node) - if (isCallerAwareMethod(node) && token != null) { + if (token != null) { context.report( - ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - context.getLocation(node), - getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( - token.variableName, - node.asRenderString() - ) + ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + context.getLocation(node), + getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( + token.variableName, + node.asRenderString() + ) ) - return } - if (!isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY)) return - val first = node.valueArguments[0].skipParenthesizedExprDown() - val arg = first as? USimpleNameReferenceExpression ?: return + } + + /** + * - Checks for restoreCallingIdentity() not in the finally block issue + * - Removes token from tokensMap if token is within the scope of the method + */ + private fun checkRestoreCallingIdentityCall(node: UCallExpression) { + val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return val variableName = arg.identifier val originalScope = tokensMap[variableName]?.scope ?: return val psi = arg.sourcePsi ?: return @@ -174,26 +203,31 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { // token declaration. If not within the scope, no action is needed because the token is // irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity() if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return - // - We do not report "restore identity call not in finally" issue when there is no + // We do not report "restore identity call not in finally" issue when there is no // finally block because that case is already handled by "clear identity call not // followed by try-finally" issue - // - UCallExpression can be a child of UQualifiedReferenceExpression, i.e. - // receiver.selector, so to get the call's immediate parent we need to get the topmost - // parent qualified reference expression and access its parent if (tokensMap[variableName]?.finallyBlock != null && - skipParenthesizedExprUp(node.getQualifiedParentOrThis().uastParent) != - tokensMap[variableName]?.finallyBlock) { + getFirstNonQualifiedParent(node) != + tokensMap[variableName]?.finallyBlock + ) { context.report( - ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - context.getLocation(node), - getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName) + ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + context.getLocation(node), + getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName) ) } tokensMap.remove(variableName) } + private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? { + // UCallExpression can be a child of UQualifiedReferenceExpression, i.e. + // receiver.selector, so to get the call's immediate parent we need to get the topmost + // parent qualified reference expression and access its parent + return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent) + } + private fun isCallerAwareMethod(expression: UCallExpression): Boolean = - callerAwareMethods.any { method -> isMethodCall(expression, method) } + callerAwareMethods.any { method -> isMethodCall(expression, method) } private fun isMethodCall( expression: UCallExpression, @@ -201,12 +235,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { ): Boolean { val psiMethod = expression.resolve() ?: return false return psiMethod.getName() == method.methodName && - context.evaluator.methodMatches( - psiMethod, - method.className, - /* allowInherit */ true, - *method.args - ) + context.evaluator.methodMatches( + psiMethod, + method.className, + /* allowInherit */ true, + *method.args + ) } /** @@ -255,7 +289,7 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { return declarations[indexInDeclarations + 1] } val enclosingBlock = node - .getParentOfType<UBlockExpression>(strict = true) ?: return null + .getParentOfType<UBlockExpression>(strict = true) ?: return null val expressions = enclosingBlock.expressions val indexInBlock = expressions.indexOf(declarationsExpression as UElement) return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1) @@ -301,12 +335,12 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { secondCallTokenLocation: Location ): Location { return cloneLocation(secondCallTokenLocation) - .withSecondary( - cloneLocation(firstCallToken.location), - getIncidentMessageNestedClearIdentityCallsSecondary( - firstCallToken.variableName - ) + .withSecondary( + cloneLocation(firstCallToken.location), + getIncidentMessageNestedClearIdentityCallsSecondary( + firstCallToken.variableName ) + ) } private fun cloneLocation(location: Location): Location { @@ -347,20 +381,20 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { const val CLASS_USER_HANDLE = "android.os.UserHandle" private val callerAwareMethods = listOf( - Method.BINDER_GET_CALLING_PID, - Method.BINDER_GET_CALLING_UID, - Method.BINDER_GET_CALLING_UID_OR_THROW, - Method.BINDER_GET_CALLING_USER_HANDLE, - Method.USER_HANDLE_GET_CALLING_APP_ID, - Method.USER_HANDLE_GET_CALLING_USER_ID + Method.BINDER_GET_CALLING_PID, + Method.BINDER_GET_CALLING_UID, + Method.BINDER_GET_CALLING_UID_OR_THROW, + Method.BINDER_GET_CALLING_USER_HANDLE, + Method.USER_HANDLE_GET_CALLING_APP_ID, + Method.USER_HANDLE_GET_CALLING_USER_ID ) /** Issue: unused token from Binder.clearCallingIdentity() */ @JvmField val ISSUE_UNUSED_TOKEN: Issue = Issue.create( - id = "UnusedTokenOfOriginalCallingIdentity", - briefDescription = "Unused token of Binder.clearCallingIdentity()", - explanation = """ + id = "UnusedTokenOfOriginalCallingIdentity", + briefDescription = "Unused token of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but have not used the returned token to \ restore the identity. @@ -370,26 +404,26 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { `token` is the result of `Binder.clearCallingIdentity()` """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " + - "not been used to restore the calling identity. Introduce a `try`-`finally` " + - "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " + - "in `finally` or remove `$variableName`." + "not been used to restore the calling identity. Introduce a `try`-`finally` " + + "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " + + "in `finally` or remove `$variableName`." /** Issue: non-final token from Binder.clearCallingIdentity() */ @JvmField val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create( - id = "NonFinalTokenOfOriginalCallingIdentity", - briefDescription = "Non-final token of Binder.clearCallingIdentity()", - explanation = """ + id = "NonFinalTokenOfOriginalCallingIdentity", + briefDescription = "Non-final token of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but have not made the returned token `final`. @@ -397,47 +431,47 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { which can cause problems when restoring the identity with \ `Binder.restoreCallingIdentity(token)`. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is " + - "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " + - "`$variableName`." + "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " + + "`$variableName`." /** Issue: nested calls of Binder.clearCallingIdentity() */ @JvmField val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create( - id = "NestedClearCallingIdentityCalls", - briefDescription = "Nested calls of Binder.clearCallingIdentity()", - explanation = """ + id = "NestedClearCallingIdentityCalls", + briefDescription = "Nested calls of Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()` twice without restoring identity with the \ result of the first call. Make sure to restore the identity after each clear identity call. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageNestedClearIdentityCallsPrimary( firstCallVariableName: String, secondCallVariableName: String ): String = "The calling identity has already been cleared and returned into " + - "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " + - "restoring the calling identity with " + - "`Binder.restoreCallingIdentity($firstCallVariableName)`." + "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " + + "restoring the calling identity with " + + "`Binder.restoreCallingIdentity($firstCallVariableName)`." private fun getIncidentMessageNestedClearIdentityCallsSecondary( firstCallVariableName: String @@ -446,10 +480,10 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { /** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */ @JvmField val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create( - id = "ClearIdentityCallNotFollowedByTryFinally", - briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " + - "statement", - explanation = """ + id = "ClearIdentityCallNotFollowedByTryFinally", + briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " + + "statement", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but the next statement is not a `try` \ statement. @@ -472,30 +506,30 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { code with your identity that was originally intended to run with the calling \ application's identity. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally( variableName: String ): String = "You cleared the calling identity and returned the result into " + - "`$variableName`, but the next statement is not a `try`-`finally` statement. " + - "Define a `try`-`finally` block after `$variableName` declaration to ensure a " + - "safe restore of the calling identity by calling " + - "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " + - "of the `finally` block." + "`$variableName`, but the next statement is not a `try`-`finally` statement. " + + "Define a `try`-`finally` block after `$variableName` declaration to ensure a " + + "safe restore of the calling identity by calling " + + "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " + + "of the `finally` block." /** Issue: Binder.restoreCallingIdentity() is not in finally block */ @JvmField val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create( - id = "RestoreIdentityCallNotInFinallyBlock", - briefDescription = "Binder.restoreCallingIdentity() is not in finally block", - explanation = """ + id = "RestoreIdentityCallNotInFinallyBlock", + briefDescription = "Binder.restoreCallingIdentity() is not in finally block", + explanation = """ You are restoring the original calling identity with \ `Binder.restoreCallingIdentity()`, but the call is not an immediate child of \ the `finally` block of the `try` statement. @@ -516,28 +550,28 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { `finally` block, you may run code with your identity that was originally \ intended to run with the calling application's identity. """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock( variableName: String ): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " + - "the `finally` block of the try statement after `$variableName` declaration. " + - "Surround the call with `finally` block and call it unconditionally." + "the `finally` block of the try statement after `$variableName` declaration. " + + "Surround the call with `finally` block and call it unconditionally." /** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */ @JvmField val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create( - id = "UseOfCallerAwareMethodsWithClearedIdentity", - briefDescription = "Use of caller-aware methods after " + - "Binder.clearCallingIdentity()", - explanation = """ + id = "UseOfCallerAwareMethodsWithClearedIdentity", + briefDescription = "Use of caller-aware methods after " + + "Binder.clearCallingIdentity()", + explanation = """ You cleared the original calling identity with \ `Binder.clearCallingIdentity()`, but used one of the methods below before \ restoring the identity. These methods will use your own identity instead of \ @@ -556,22 +590,59 @@ class CallingIdentityTokenDetector : Detector(), SourceCodeScanner { UserHandle.getCallingUserId() ``` """, - category = Category.SECURITY, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - CallingIdentityTokenDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) ) private fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity( variableName: String, methodName: String ): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " + - "and returned into `$variableName`, so `$methodName` will be using your own " + - "identity instead of the caller's. Either explicitly query your own identity or " + - "move it after restoring the identity with " + - "`Binder.restoreCallingIdentity($variableName)`." + "and returned into `$variableName`, so `$methodName` will be using your own " + + "identity instead of the caller's. Either explicitly query your own identity or " + + "move it after restoring the identity with " + + "`Binder.restoreCallingIdentity($variableName)`." + + /** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */ + @JvmField + val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create( + id = "ResultOfClearIdentityCallNotStoredInVariable", + briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " + + "variable", + explanation = """ + You cleared the original calling identity with \ + `Binder.clearCallingIdentity()`, but did not store the result of the method \ + call in a variable. You need to store the result in a variable and restore it later. + + Use the following pattern for running operations with your own identity: + + ``` + final long token = Binder.clearCallingIdentity(); + try { + // Code using your own identity + } finally { + Binder.restoreCallingIdentity(token); + } + ``` + """, + category = Category.SECURITY, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + CallingIdentityTokenDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable( + methodName: String + ): String = "You cleared the original identity with `$methodName` but did not store the " + + "result in a variable. You need to store the result in a variable and restore it " + + "later." } } diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt index fe567da7c017..fe567da7c017 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt new file mode 100644 index 000000000000..48540b1da565 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2022 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.google.android.lint + +import com.android.tools.lint.client.api.UastParser +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Context +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.interprocedural.CallGraph +import com.android.tools.lint.detector.api.interprocedural.CallGraphResult +import com.android.tools.lint.detector.api.interprocedural.searchForPaths +import com.intellij.psi.PsiAnonymousClass +import com.intellij.psi.PsiMethod +import java.util.LinkedList +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UParameter +import org.jetbrains.uast.USimpleNameReferenceExpression +import org.jetbrains.uast.visitor.AbstractUastVisitor + +/** + * A lint checker to detect potential package visibility issues for system's APIs. APIs working + * in the system_server and taking the package name as a parameter may have chance to reveal + * package existence status on the device, and break the + * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility"> + * Package Visibility</a> that we introduced in Android 11. + * <p> + * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance + * to detect package existence state on the device from the result of the API, if there is no + * package visibility filtering rule or uid identify checks applying to the parameter of the + * package name. + */ +class PackageVisibilityDetector : Detector(), SourceCodeScanner { + + // Enables call graph analysis + override fun isCallGraphRequired(): Boolean = true + + override fun analyzeCallGraph( + context: Context, + callGraph: CallGraphResult + ) { + val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi) + val sinkMethodNodes = callGraph.callGraph.nodes.filter { + // TODO(b/228285232): Remove enforce permission sink methods + isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS) + } + val parser = context.client.getUastParser(context.project) + analyzeApisContainPackageNameParameters( + context, parser, systemServerApiNodes, sinkMethodNodes) + } + + /** + * Looking for API contains package name parameters, report the lint issue if the API does not + * invoke any sink methods. + */ + private fun analyzeApisContainPackageNameParameters( + context: Context, + parser: UastParser, + systemServerApiNodes: List<CallGraph.Node>, + sinkMethodNodes: List<CallGraph.Node> + ) { + for (apiNode in systemServerApiNodes) { + val apiMethod = apiNode.getUMethod() ?: continue + val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param -> + if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) { + index + } else { + null + } + }.takeIf(List<Int>::isNotEmpty) ?: continue + + for (pkgNameParamIndex in pkgNameParamIndexes) { + // Trace the call path of the method's argument, pass the lint checks if a sink + // method is found + if (traceArgumentCallPath( + apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) { + continue + } + // Pass the check if one of the sink methods is invoked + if (hasValidPath( + searchForPaths( + sources = listOf(apiNode), + isSink = { it in sinkMethodNodes }, + getNeighbors = { node -> node.edges.map { it.node!! } } + ) + ) + ) continue + + // Report issue + val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement + val location = parser.createLocation(reportElement) + context.report( + ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, + location, + getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex) + ) + } + } + } + + /** + * Returns {@code true} if the method associated with the given node is a system server's + * public API that extends from Stub class. + */ + private fun isSystemServerApi( + node: CallGraph.Node + ): Boolean { + val method = node.getUMethod() ?: return false + if (!method.hasModifierProperty("public") || + method.uastBody == null || + method.containingClass is PsiAnonymousClass) { + return false + } + val className = method.containingClass?.qualifiedName ?: return false + if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) { + return false + } + return (method.containingClass ?: return false).supers + .filter { it.name == CLASS_STUB } + .filter { it.qualifiedName !in BYPASS_STUBS } + .any { it.findMethodBySignature(method, /* checkBases */ true) != null } + } + + /** + * Returns {@code true} if the list contains the node of the call graph. + */ + private fun isNodeInList( + node: CallGraph.Node, + filters: List<Method> + ): Boolean { + val method = node.getUMethod() ?: return false + return Method(method) in filters + } + + /** + * Trace the call paths of the argument of the method in the start entry. Return {@code true} + * if one of methods in the sink call list is invoked. + * Take an example of the call path: + * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess() + * It returns {@code true} if the filterAppAccess() is in the sink call list. + */ + private fun traceArgumentCallPath( + apiNode: CallGraph.Node, + pkgNameParamIndex: Int, + sinkList: List<Method> + ): Boolean { + val startEntry = TraceEntry(apiNode, pkgNameParamIndex) + val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) } + val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) } + while (!traceQueue.isEmpty()) { + val entry = traceQueue.poll() + val entryNode = entry.node + val entryMethod = entryNode.getUMethod() ?: continue + val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name + for (outEdge in entryNode.edges) { + val outNode = outEdge.node ?: continue + val outMethod = outNode.getUMethod() ?: continue + val outArgumentIndex = + outEdge.call?.findArgumentIndex( + entryArgumentName, outMethod.uastParameters.size) + val sinkMethod = findInSinkList(outMethod, sinkList) + if (sinkMethod == null) { + if (outArgumentIndex == null) { + // Path is not relevant to the sink method and argument + continue + } + // Path is relevant to the argument, add a new trace entry if never visit before + val newEntry = TraceEntry(outNode, outArgumentIndex) + if (newEntry !in allVisits) { + traceQueue.add(newEntry) + allVisits.add(newEntry) + } + continue + } + if (sinkMethod.matchArgument && outArgumentIndex == null) { + // The sink call is required to match the argument, but not found + continue + } + if (sinkMethod.checkCaller && + entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) { + // The sink call is in the scope of Binder.clearCallingIdentify + continue + } + // A sink method is matched + return true + } + } + return false + } + + /** + * Returns the UMethod associated with the given node of call graph. + */ + private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod + + /** + * Returns the system module name (e.g. com.android.server.pm) of the method of the + * call graph node. + */ + private fun CallGraph.Node.getModuleName(): String? { + val method = getUMethod() ?: return null + val className = method.containingClass?.qualifiedName ?: return null + if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) { + return null + } + val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length) + if (dotPos == -1) { + return SYSTEM_PACKAGE_PREFIX + } + return className.substring(0, dotPos) + } + + /** + * Return {@code true} if the argument in the method's body is in-use. + */ + private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean { + val method = getUMethod() ?: return false + val argumentName = method.uastParameters[argIndex].name + var foundArg = false + val methodVisitor = object : AbstractUastVisitor() { + override fun visitSimpleNameReferenceExpression( + node: USimpleNameReferenceExpression + ): Boolean { + if (node.identifier == argumentName) { + foundArg = true + } + return true + } + } + method.uastBody?.accept(methodVisitor) + return foundArg + } + + /** + * Given an argument name, returns the index of argument in the call expression. + */ + private fun UCallExpression.findArgumentIndex( + argumentName: String, + parameterSize: Int + ): Int? { + if (valueArgumentCount == 0 || parameterSize == 0) { + return null + } + var match = false + val argVisitor = object : AbstractUastVisitor() { + override fun visitSimpleNameReferenceExpression( + node: USimpleNameReferenceExpression + ): Boolean { + if (node.identifier == argumentName) { + match = true + } + return true + } + override fun visitCallExpression(node: UCallExpression): Boolean { + return true + } + } + valueArguments.take(parameterSize).forEachIndexed { index, argument -> + argument.accept(argVisitor) + if (match) { + return index + } + } + return null + } + + /** + * Given a UMethod, returns a method from the sink method list. + */ + private fun findInSinkList( + uMethod: UMethod, + sinkCallList: List<Method> + ): Method? { + return sinkCallList.find { + it == Method(uMethod) || + it == Method(uMethod.containingClass?.qualifiedName ?: "", "*") + } + } + + /** + * Returns {@code true} if the call expression is in the scope of the + * Binder.clearCallingIdentify. + */ + private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean { + var isInScope = false + val methodVisitor = object : AbstractUastVisitor() { + private var clearCallingIdentity = 0 + override fun visitCallExpression(node: UCallExpression): Boolean { + if (call == node && clearCallingIdentity != 0) { + isInScope = true + return true + } + val visitMethod = Method(node.resolve() ?: return false) + if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) { + clearCallingIdentity++ + } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) { + clearCallingIdentity-- + } + return false + } + } + accept(methodVisitor) + return isInScope + } + + /** + * Checks the module name of the start node and the last node that invokes the sink method + * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same + * module name for both nodes. + */ + private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean { + for (pathNodes in paths) { + if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) { + continue + } + val startModule = pathNodes[0].getModuleName() ?: continue + val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue + if (startModule == lastCallModule) { + return true + } + } + return false + } + + /** + * A data class to represent the method. + */ + private data class Method( + val clazz: String, + val name: String + ) { + // Used by traceArgumentCallPath to indicate that the method is required to match the + // argument name + var matchArgument = true + + // Used by traceArgumentCallPath to indicate that the method is required to check whether + // the Binder.clearCallingIdentity is invoked. + var checkCaller = false + + constructor( + clazz: String, + name: String, + matchArgument: Boolean = true, + checkCaller: Boolean = false + ) : this(clazz, name) { + this.matchArgument = matchArgument + this.checkCaller = checkCaller + } + + constructor( + method: PsiMethod + ) : this(method.containingClass?.qualifiedName ?: "", method.name) + + constructor( + method: com.google.android.lint.model.Method + ) : this(method.clazz, method.name) + } + + /** + * A data class to represent the parameter of the method. The parameter name is converted to + * lower case letters for comparison. + */ + private data class Parameter private constructor( + val typeName: String, + val parameterName: String + ) { + constructor(uParameter: UParameter) : this( + uParameter.type.canonicalText, + uParameter.name.lowercase() + ) + + companion object { + fun create(typeName: String, parameterName: String) = + Parameter(typeName, parameterName.lowercase()) + } + } + + /** + * A data class wraps a method node of the call graph and an index that indicates an + * argument of the method to record call trace information. + */ + private data class TraceEntry( + val node: CallGraph.Node, + val argumentIndex: Int + ) + + companion object { + private const val SYSTEM_PACKAGE_PREFIX = "com.android.server." + // A valid call path list needs to contain a start node and a sink node + private const val VALID_CALL_PATH_NODES_SIZE = 2 + + private const val CLASS_STRING = "java.lang.String" + private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager" + private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager" + private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager" + private const val CLASS_BINDER = "android.os.Binder" + private const val CLASS_PACKAGE_MANAGER_INTERNAL = + "android.content.pm.PackageManagerInternal" + + // Patterns of package name parameter + private val PACKAGE_NAME_PATTERNS = setOf( + Parameter.create(CLASS_STRING, "packageName"), + Parameter.create(CLASS_STRING, "callingPackage"), + Parameter.create(CLASS_STRING, "callingPackageName"), + Parameter.create(CLASS_STRING, "pkgName"), + Parameter.create(CLASS_STRING, "callingPkg"), + Parameter.create(CLASS_STRING, "pkg") + ) + + // Package manager APIs + private val PACKAGE_NAME_SINK_METHOD_LIST = listOf( + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false), + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"), + Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"), + Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true), + Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true), + Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false), + Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false) + ) + + // AppOps APIs which include uid and package visibility filters checks + private val APPOPS_METHODS = listOf( + Method(CLASS_APPOPS_MANAGER, "noteOp"), + Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "noteOperation"), + Method(CLASS_APPOPS_MANAGER, "noteProxyOp"), + Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "startOp"), + Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"), + Method(CLASS_APPOPS_MANAGER, "FinishOp"), + Method(CLASS_APPOPS_MANAGER, "finishProxyOp"), + Method(CLASS_APPOPS_MANAGER, "checkPackage") + ) + + // Enforce permission APIs + private val ENFORCE_PERMISSION_METHODS = + com.google.android.lint.ENFORCE_PERMISSION_METHODS + .map(PackageVisibilityDetector::Method) + + private val BYPASS_STUBS = listOf( + "android.content.pm.IPackageDataObserver.Stub", + "android.content.pm.IPackageDeleteObserver.Stub", + "android.content.pm.IPackageDeleteObserver2.Stub", + "android.content.pm.IPackageInstallObserver2.Stub", + "com.android.internal.app.IAppOpsCallback.Stub", + + // TODO(b/228285637): Do not bypass PackageManagerService API + "android.content.pm.IPackageManager.Stub", + "android.content.pm.IPackageManagerNative.Stub" + ) + + private val METHOD_CLEAR_CALLING_IDENTITY = + Method(CLASS_BINDER, "clearCallingIdentity") + private val METHOD_RESTORE_CALLING_IDENTITY = + Method(CLASS_BINDER, "restoreCallingIdentity") + + private fun getMsgPackageNameNoPackageVisibilityFilters( + method: UMethod, + argumentIndex: Int + ): String = "Api: ${method.name} contains a package name parameter: " + + "${method.uastParameters[argumentIndex].name} does not apply " + + "package visibility filtering rules." + + private val EXPLANATION = """ + APIs working in the system_server and taking the package name as a parameter may have + chance to reveal package existence status on the device, and break the package + visibility that we introduced in Android 11. + (https://developer.android.com/about/versions/11/privacy/package-visibility) + + Take an example of the API `boolean setFoo(String packageName)`, a malicious app may + have chance to get package existence state on the device from the result of the API, + if there is no package visibility filtering rule or uid identify checks applying to + the parameter of the package name. + + To resolve it, you could apply package visibility filtering rules to the package name + via PackageManagerInternal.filterAppAccess API, before starting to use the package name. + If the parameter is a calling package name, use the PackageManager API such as + PackageManager.getPackagesForUid to verify the calling identify. + """ + + val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create( + id = "ApiMightLeakAppVisibility", + briefDescription = "Api takes package name parameter doesn't apply " + + "package visibility filters", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 1, + severity = Severity.WARNING, + implementation = Implementation( + PackageVisibilityDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt new file mode 100644 index 000000000000..e12ec3d4a77c --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2022 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.google.android.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.getUMethod +import com.intellij.psi.PsiType +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UReturnExpression +import org.jetbrains.uast.getContainingUMethod + +/** + * Stops incorrect usage of {@link PermissionMethod} + * TODO: add tests once re-enabled (b/240445172, b/247542171) + */ +class PermissionMethodDetector : Detector(), SourceCodeScanner { + + override fun getApplicableUastTypes(): List<Class<out UElement>> = + listOf(UAnnotation::class.java, UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = + PermissionMethodHandler(context) + + private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (hasPermissionMethodAnnotation(node)) return + if (onlyCallsPermissionMethod(node)) { + val location = context.getLocation(node.javaPsi.modifierList) + val fix = fix() + .annotate(ANNOTATION_PERMISSION_METHOD) + .range(location) + .autoFix() + .build() + + context.report( + ISSUE_CAN_BE_PERMISSION_METHOD, + location, + "Annotate method with @PermissionMethod", + fix + ) + } + } + + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return + val method = node.getContainingUMethod() ?: return + + if (!isPermissionMethodReturnType(method)) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` should return `void`, \ + `boolean`, or `@PackageManager.PermissionResult int`." + """.trimIndent() + ) + } + + if (method.returnType == PsiType.INT && + method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) } + ) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` that return `int` should \ + also be annotated with `@PackageManager.PermissionResult.`" + """.trimIndent() + ) + } + } + } + + companion object { + + private val EXPLANATION_PERMISSION_METHOD_USAGE = """ + `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \ + Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \ + `void` and potentially throw `SecurityException`. + """.trimIndent() + + @JvmField + val ISSUE_PERMISSION_METHOD_USAGE = Issue.create( + id = "PermissionMethodUsage", + briefDescription = "@PermissionMethod used incorrectly", + explanation = EXPLANATION_PERMISSION_METHOD_USAGE, + category = Category.CORRECTNESS, + priority = 5, + severity = Severity.ERROR, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = true + ) + + private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """ + Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \ + be annotated with @PermissionMethod. For example: + ``` + void wrapperHelper() { + // Context.enforceCallingPermission is annotated with @PermissionMethod + context.enforceCallingPermission(SOME_PERMISSION) + } + ``` + """.trimIndent() + + @JvmField + val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create( + id = "CanBePermissionMethod", + briefDescription = "Method can be annotated with @PermissionMethod", + explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = false + ) + + private fun isPermissionMethodReturnType(method: UMethod): Boolean = + listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType) + + /** + * Identifies methods that... + * DO call other methods annotated with @PermissionMethod + * DO NOT do anything else + */ + private fun onlyCallsPermissionMethod(method: UMethod): Boolean { + val body = method.uastBody as? UBlockExpression ?: return false + if (body.expressions.isEmpty()) return false + for (expression in body.expressions) { + when (expression) { + is UQualifiedReferenceExpression -> { + if (!isPermissionMethodCall(expression.selector)) return false + } + is UReturnExpression -> { + if (!isPermissionMethodCall(expression.returnExpression)) return false + } + is UCallExpression -> { + if (!isPermissionMethodCall(expression)) return false + } + is UIfExpression -> { + if (expression.thenExpression !is UReturnExpression) return false + if (!isPermissionMethodCall(expression.condition)) return false + } + else -> return false + } + } + return true + } + + private fun isPermissionMethodCall(expression: UExpression?): Boolean { + return when (expression) { + is UQualifiedReferenceExpression -> + return isPermissionMethodCall(expression.selector) + is UCallExpression -> { + val calledMethod = expression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(calledMethod) + } + else -> false + } + } + + private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations + .any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) } + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt new file mode 100644 index 000000000000..06c098df385d --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2022 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.google.android.lint.parcel + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiCallExpression +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiIntersectionType +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameter +import com.intellij.psi.PsiWildcardType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UVariable + +/** + * Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue + * with a fix that migrates towards the new safer API by appending an argument in the form of + * {@code com.package.ItemType.class} coming from the result of the overridden method. + */ +abstract class CallMigrator( + val method: Method, + private val rejects: Set<String> = emptySet(), +) { + open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) { + val location = context.getLocation(call) + val itemType = filter(getBoundingClass(context, call, method)) + val fix = (itemType as? PsiClassType)?.let { type -> + getParcelFix(location, this.method.name, getArgumentSuffix(type)) + } + val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage" + context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix) + } + + protected open fun getArgumentSuffix(type: PsiClassType) = + ", ${type.rawType().canonicalText}.class" + + protected open fun getBoundingClass( + context: JavaContext, + call: UCallExpression, + method: PsiMethod, + ): PsiType? = null + + protected fun getItemType(type: PsiType, container: String): PsiClassType? { + val supers = getParentTypes(type).mapNotNull { it as? PsiClassType } + val containerType = supers.firstOrNull { it.rawType().canonicalText == container } + ?: return null + val itemType = containerType.parameters.getOrNull(0) ?: return null + // TODO: Expand to other types, see PsiTypeVisitor + return when (itemType) { + is PsiClassType -> itemType + is PsiWildcardType -> itemType.bound as PsiClassType + else -> null + } + } + + /** + * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}. + * + * This could be an assignment, an argument passed to a method call, to a constructor call, a + * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned. + */ + protected fun getReceivingType(expression: UElement): PsiType? { + val parent = expression.uastParent + var type = when (parent) { + is UCallExpression -> { + val i = parent.valueArguments.indexOf(expression) + val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null + val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor + val method = psiCall.resolveMethod()!! + method.getSignature(typeSubstitutor).parameterTypes[i] + } + is UVariable -> parent.type + is UExpression -> parent.getExpressionType() + else -> null + } + if (type == null && expression is UExpression) { + type = expression.getExpressionType() + } + return type + } + + protected fun filter(type: PsiType?): PsiType? { + // It's important that PsiIntersectionType case is above the one that check the type in + // rejects, because for intersect types, the canonicalText is one of the terms. + if (type is PsiIntersectionType) { + return type.conjuncts.mapNotNull(this::filter).firstOrNull() + } + if (type == null || type.canonicalText in rejects) { + return null + } + if (type is PsiClassType && type.resolve() is PsiTypeParameter) { + return null + } + return type + } + + private fun getParentTypes(type: PsiType): Set<PsiType> = + type.superTypes.flatMap(::getParentTypes).toSet() + type + + protected fun getParcelFix(location: Location, method: String, arguments: String) = + LintFix + .create() + .name("Migrate to safer Parcel.$method() API") + .replace() + .range(location) + .pattern("$method\\s*\\(((?:.|\\n)*)\\)") + .with("\\k<1>$arguments") + .autoFix() + .build() +} + +/** + * This class derives the type to be appended by inferring the generic type of the {@code container} + * type (eg. "java.util.List") of the {@code argument}-th argument. + */ +class ContainerArgumentMigrator( + method: Method, + private val argument: Int, + private val container: String, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null + return getItemType(firstParamType, container)!! + } + + /** + * We need to insert a casting construct in the class parameter. For example: + * (Class<Foo<Bar>>) (Class<?>) Foo.class. + * This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and + * class type is Class<Foo?). + */ + override fun getArgumentSuffix(type: PsiClassType): String { + if (type.parameters.isNotEmpty()) { + val rawType = type.rawType() + return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class" + } + return super.getArgumentSuffix(type) + } +} + +/** + * This class derives the type to be appended by inferring the generic type of the {@code container} + * type (eg. "java.util.List") of the return type of the method. + */ +class ContainerReturnMigrator( + method: Method, + private val container: String, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val type = getReceivingType(call.uastParent!!) ?: return null + return getItemType(type, container) + } +} + +/** + * This class derives the type to be appended by inferring the expected type for the method result. + */ +class ReturnMigrator( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + return getReceivingType(call.uastParent!!) + } +} + +/** + * This class appends the class loader and the class object by deriving the type from the method + * result. + */ +class ReturnMigratorWithClassLoader( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + return getReceivingType(call.uastParent!!) + } + + override fun getArgumentSuffix(type: PsiClassType): String = + "${type.rawType().canonicalText}.class.getClassLoader(), " + + "${type.rawType().canonicalText}.class" + +} + +/** + * This class derives the type to be appended by inferring the expected array type + * for the method result. + */ +class ArrayReturnMigrator( + method: Method, + rejects: Set<String> = emptySet(), +) : CallMigrator(method, rejects) { + override fun getBoundingClass( + context: JavaContext, call: UCallExpression, method: PsiMethod + ): PsiType? { + val type = getReceivingType(call.uastParent!!) + return (type as? PsiArrayType)?.componentType + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt new file mode 100644 index 000000000000..0826e8e74431 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 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.google.android.lint.parcel + +data class Method( + val params: List<String>, + val clazz: String, + val name: String, + val parameters: List<String> +) { + constructor( + clazz: String, + name: String, + parameters: List<String> + ) : this( + listOf(), clazz, name, parameters + ) + + val signature: String + get() { + val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} " + return "$prefix$clazz.$name(${parameters.joinToString()})" + } + + val className: String by lazy { + clazz.split(".").last() + } +} diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt new file mode 100644 index 000000000000..f92826316be4 --- /dev/null +++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 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.google.android.lint.parcel + +import com.android.tools.lint.detector.api.* +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiSubstitutor +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeParameter +import org.jetbrains.uast.UCallExpression +import java.util.* + +@Suppress("UnstableApiUsage") +class SaferParcelChecker : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List<String> = + MIGRATORS + .map(CallMigrator::method) + .map(Method::name) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isAtLeastT(context)) return + val signature = getSignature(method) + val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return + migrator.report(context, node, method) + } + + private fun getSignature(method: PsiMethod): String { + val name = UastLintUtils.getQualifiedName(method) + val signature = method.getSignature(PsiSubstitutor.EMPTY) + val parameters = + signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText) + val types = signature.typeParameters.map(PsiTypeParameter::getName) + val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " " + return "$prefix$name($parameters)" + } + + private fun isAtLeastT(context: Context): Boolean { + val project = if (context.isGlobalAnalysis()) context.mainProject else context.project + return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33 + } + + companion object { + @JvmField + val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create( + id = "UnsafeParcelApi", + briefDescription = "Use of unsafe deserialization API", + explanation = """ + You are using a deprecated deserialization API that doesn't accept the expected class as\ + a parameter. This means that unexpected classes could be instantiated and\ + unexpected code executed. + + Please migrate to the safer alternative that takes an extra Class<T> parameter. + """, + category = Category.SECURITY, + priority = 8, + severity = Severity.WARNING, + + implementation = Implementation( + SaferParcelChecker::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + // Parcel + private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf()) + private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader")) + private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader")) + + // Bundle + private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String")) + private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String")) + + // Intent + private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String")) + private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String")) + + // TODO: Write migrators for methods below + private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader")) + + private val MIGRATORS = listOf( + ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")), + ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"), + ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"), + ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"), + ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"), + ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE), + ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")), + ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + + ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")), + ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")), + ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")), + + ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")), + ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")), + ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")), + ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")), + ) + } +} diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt index e1a5c613dee1..d90f3e31baf9 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt @@ -27,12 +27,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { override fun getDetector(): Detector = CallingIdentityTokenDetector() override fun getIssues(): List<Issue> = listOf( - CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, - CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, - CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY + CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, + CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, + CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, + CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, + CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, + CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, + CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE ) override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) @@ -41,8 +42,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { fun testDoesNotDetectIssuesInCorrectScenario() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -62,22 +63,29 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } finally { restoreCallingIdentity(token3); } + final Long token4 = true ? Binder.clearCallingIdentity() : null; + try { + } finally { + if (token4 != null) { + restoreCallingIdentity(token4); + } + } } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expectClean() + .run() + .expectClean() } /** Unused token issue tests */ fun testDetectsUnusedTokens() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -101,12 +109,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token1) in finally or \ @@ -127,13 +135,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } fun testDetectsUnusedTokensInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -152,12 +160,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token) in finally or \ @@ -166,13 +174,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """.addLineContinuation() - ) + ) } fun testDoesNotDetectUsedTokensInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -192,17 +200,17 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expectClean() + .run() + .expectClean() } fun testDetectsUnusedTokensWithSimilarNamesInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 { @@ -220,12 +228,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token has not been used to \ restore the calling identity. Introduce a try-finally after the \ declaration and call Binder.restoreCallingIdentity(token) in finally or \ @@ -240,15 +248,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 2 warnings """.addLineContinuation() - ) + ) } /** Non-final token issue tests */ fun testDetectsNonFinalTokens() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -271,12 +279,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: token1 is a non-final token from \ Binder.clearCallingIdentity(). Add final keyword to token1. \ [NonFinalTokenOfOriginalCallingIdentity] @@ -294,7 +302,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } /** Nested clearCallingIdentity() calls issue tests */ @@ -302,8 +310,8 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { fun testDetectsNestedClearCallingIdentityCalls() { // Pattern: clear - clear - clear - restore - restore - restore lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -326,12 +334,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \ been cleared and returned into token1. Move token2 declaration after \ restoring the calling identity with Binder.restoreCallingIdentity(token1). \ @@ -348,15 +356,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { src/test/pkg/TestClass1.java:5: Location of the token1 declaration. 0 errors, 2 warnings """.addLineContinuation() - ) + ) } /** clearCallingIdentity() not followed by try-finally issue tests */ fun testDetectsClearIdentityCallNotFollowedByTryFinally() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder{ @@ -397,12 +405,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: You cleared the calling identity \ and returned the result into token, but the next statement is not a \ try-finally statement. Define a try-finally block after token declaration \ @@ -445,15 +453,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 5 warnings """.addLineContinuation() - ) + ) } /** restoreCallingIdentity() call not in finally block issue tests */ fun testDetectsRestoreCallingIdentityCallNotInFinally() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -482,12 +490,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:10: Warning: \ Binder.restoreCallingIdentity(token) is not an immediate child of the \ finally block of the try statement after token declaration. Surround the c\ @@ -511,13 +519,13 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 3 warnings """.addLineContinuation() - ) + ) } fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; public class TestClass1 extends Binder { @@ -560,12 +568,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:11: Warning: \ Binder.restoreCallingIdentity(token1) is not an immediate child of the \ finally block of the try statement after token1 declaration. Surround the \ @@ -596,15 +604,15 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 4 warnings """.addLineContinuation() - ) + ) } /** Use of caller-aware methods after clearCallingIdentity() issue tests */ fun testDetectsUseOfCallerAwareMethodsWithClearedIdentityIssuesInScopes() { lint().files( - java( - """ + java( + """ package test.pkg; import android.os.Binder; import android.os.UserHandle; @@ -632,12 +640,12 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { } } """ - ).indented(), - *stubs + ).indented(), + *stubs ) - .run() - .expect( - """ + .run() + .expect( + """ src/test/pkg/TestClass1.java:8: Warning: You cleared the original identity \ with Binder.clearCallingIdentity() and returned into token, so \ getCallingPid() will be using your own identity instead of the \ @@ -736,13 +744,58 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 errors, 12 warnings """.addLineContinuation() - ) + ) + } + + /** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */ + + fun testDetectsResultOfClearIdentityCallNotStoredInVariable() { + lint().files( + java( + """ + package test.pkg; + import android.os.Binder; + public class TestClass1 extends Binder { + private void testMethod() { + Binder.clearCallingIdentity(); + android.os.Binder.clearCallingIdentity(); + clearCallingIdentity(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \ + with Binder.clearCallingIdentity() but did not store the result in a \ + variable. You need to store the result in a variable and restore it later. \ + [ResultOfClearIdentityCallNotStoredInVariable] + Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \ + with android.os.Binder.clearCallingIdentity() but did not store the result \ + in a variable. You need to store the result in a variable and restore it \ + later. [ResultOfClearIdentityCallNotStoredInVariable] + android.os.Binder.clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \ + with clearCallingIdentity() but did not store the result in a variable. \ + You need to store the result in a variable and restore it later. \ + [ResultOfClearIdentityCallNotStoredInVariable] + clearCallingIdentity(); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 3 warnings + """.addLineContinuation() + ) } /** Stubs for classes used for testing */ private val binderStub: TestFile = java( - """ + """ package android.os; public class Binder { public static final native long clearCallingIdentity() { @@ -767,7 +820,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val userHandleStub: TestFile = java( - """ + """ package android.os; import android.annotation.AppIdInt; import android.annotation.UserIdInt; @@ -792,7 +845,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val userIdIntStub: TestFile = java( - """ + """ package android.annotation; public @interface UserIdInt { } @@ -800,7 +853,7 @@ class CallingIdentityTokenDetectorTest : LintDetectorTest() { ).indented() private val appIdIntStub: TestFile = java( - """ + """ package android.annotation; public @interface AppIdInt { } diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt index e72f38416310..e72f38416310 100644 --- a/tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt new file mode 100644 index 000000000000..a70644ab8532 --- /dev/null +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2022 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.google.android.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class PackageVisibilityDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = PackageVisibilityDetector() + + override fun getIssues(): MutableList<Issue> = mutableListOf( + PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + @Override + public boolean hasPackage(String packageName) { + return packageName != null; + } + } + """).indented(), *stubs + ).run().expect( + """ + src/com/android/server/lint/test/TestClass.java:6: Warning: \ + Api: hasPackage contains a package name parameter: packageName does not apply \ + package visibility filtering rules. \ + [ApiMightLeakAppVisibility] + public boolean hasPackage(String packageName) { + ~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectIssuesApiInvokesAppOps() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.app.AppOpsManager; + import android.os.Binder; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private AppOpsManager mAppOpsManager; + + @Override + public boolean hasPackage(String packageName) { + checkPackage(packageName); + return packageName != null; + } + + private void checkPackage(String packageName) { + mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName); + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDoesNotDetectIssuesApiInvokesEnforcePermission() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.Context; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private Context mContext; + + @Override + public boolean hasPackage(String packageName) { + enforcePermission(); + return packageName != null; + } + + private void enforcePermission() { + mContext.checkCallingPermission( + android.Manifest.permission.ACCESS_INPUT_FLINGER); + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDoesNotDetectIssuesApiInvokesPackageManager() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.pm.PackageInfo; + import android.content.pm.PackageManager; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + private PackageManager mPackageManager; + + @Override + public boolean hasPackage(String packageName) { + return getPackageInfo(packageName) != null; + } + + private PackageInfo getPackageInfo(String packageName) { + try { + return mPackageManager.getPackageInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() { + lint().files(java( + """ + package com.android.server.lint.test; + import android.content.pm.PackageInfo; + import android.content.pm.PackageManager; + import android.internal.test.IFoo;import android.os.Binder; + + public class TestClass extends IFoo.Stub { + private PackageManager mPackageManager; + + @Override + public boolean hasPackage(String packageName) { + return getPackageInfo(packageName) != null; + } + + private PackageInfo getPackageInfo(String packageName) { + long token = Binder.clearCallingIdentity(); + try { + try { + return mPackageManager.getPackageInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } finally{ + Binder.restoreCallingIdentity(token); + } + } + } + """).indented(), *stubs + ).run().expect( + """ + src/com/android/server/lint/test/TestClass.java:10: Warning: \ + Api: hasPackage contains a package name parameter: packageName does not apply \ + package visibility filtering rules. \ + [ApiMightLeakAppVisibility] + public boolean hasPackage(String packageName) { + ~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() { + lint().files(java( + """ + package com.test.not.system.prefix; + import android.internal.test.IFoo; + + public class TestClass extends IFoo.Stub { + @Override + public boolean hasPackage(String packageName) { + return packageName != null; + } + } + """ + ).indented(), *stubs).run().expectClean() + } + + private val contextStub: TestFile = java( + """ + package android.content; + + public abstract class Context { + public abstract int checkCallingPermission(String permission); + } + """ + ).indented() + + private val appOpsManagerStub: TestFile = java( + """ + package android.app; + + public class AppOpsManager { + public void checkPackage(int uid, String packageName) { + } + } + """ + ).indented() + + private val packageManagerStub: TestFile = java( + """ + package android.content.pm; + import android.content.pm.PackageInfo; + + public abstract class PackageManager { + public static class NameNotFoundException extends AndroidException { + } + + public abstract PackageInfo getPackageInfo(String packageName, int flags) + throws NameNotFoundException; + } + """ + ).indented() + + private val packageInfoStub: TestFile = java( + """ + package android.content.pm; + public class PackageInfo {} + """ + ).indented() + + private val binderStub: TestFile = java( + """ + package android.os; + + public class Binder { + public static final native long clearCallingIdentity(); + public static final native void restoreCallingIdentity(long token); + public static final native int getCallingUid(); + } + """ + ).indented() + + private val interfaceIFooStub: TestFile = java( + """ + package android.internal.test; + import android.os.Binder; + + public interface IFoo { + boolean hasPackage(String packageName); + public abstract static class Stub extends Binder implements IFoo { + } + } + """ + ).indented() + + private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub, + packageInfoStub, binderStub, interfaceIFooStub) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt new file mode 100644 index 000000000000..e686695ca804 --- /dev/null +++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt @@ -0,0 +1,823 @@ +/* + * Copyright (C) 2022 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.google.android.lint.parcel + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class SaferParcelCheckerTest : LintDetectorTest() { + override fun getDetector(): Detector = SaferParcelChecker() + + override fun getIssues(): List<Issue> = listOf( + SaferParcelChecker.ISSUE_UNSAFE_API_USAGE + ) + + override fun lint(): TestLintTask = + super.lint() + .allowMissingSdk(true) + // We don't do partial analysis in the platform + .skipTestModes(TestMode.PARTIAL) + + /** Parcel Tests */ + + fun testParcelDetectUnsafeReadSerializable() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + import java.io.Serializable; + + public class TestClass { + private TestClass(Parcel p) { + Serializable ans = p.readSerializable(); + } + } + """ + ).indented(), + *includes + ) + .expectIdenticalTestModeOutput(false) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \ + API usage [UnsafeParcelApi] + Serializable ans = p.readSerializable(); + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadSerializable() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + import java.io.Serializable; + + public class TestClass { + private TestClass(Parcel p) { + String ans = p.readSerializable(null, String.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + ArrayList ans = p.readArrayList(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \ + usage [UnsafeParcelApi] + ArrayList ans = p.readArrayList(null); + ~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + ArrayList<Intent> ans = p.readArrayList(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + p.readList(list, null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \ + [UnsafeParcelApi] + p.readList(list, null); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testDParceloesNotDetectSafeReadList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + p.readList(list, null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent ans = p.readParcelable(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \ + usage [UnsafeParcelApi] + Intent ans = p.readParcelable(null); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent ans = p.readParcelable(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelableList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + List<Intent> ans = p.readParcelableList(list, null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \ + API usage [UnsafeParcelApi] + List<Intent> ans = p.readParcelableList(list, null); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelableList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import java.util.List; + + public class TestClass { + private TestClass(Parcel p) { + List<Intent> list = new ArrayList<Intent>(); + List<Intent> ans = + p.readParcelableList(list, null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadSparseArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import android.util.SparseArray; + + public class TestClass { + private TestClass(Parcel p) { + SparseArray<Intent> ans = p.readSparseArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\ + usage [UnsafeParcelApi] + SparseArray<Intent> ans = p.readSparseArray(null); + ~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadSparseArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + import android.util.SparseArray; + + public class TestClass { + private TestClass(Parcel p) { + SparseArray<Intent> ans = + p.readSparseArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readArray(null); + ~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testParcelDetectUnsafeReadParcelableSArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\ + usage [UnsafeParcelApi] + Intent[] ans = p.readParcelableArray(null); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testParcelDoesNotDetectSafeReadParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Parcel; + + public class TestClass { + private TestClass(Parcel p) { + Intent[] ans = p.readParcelableArray(null, Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Bundle Tests */ + + fun testBundleDetectUnsafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi] + Intent ans = b.getParcelable("key"); + ~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelable() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent ans = b.getParcelable("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi] + ArrayList<Intent> ans = b.getParcelableArrayList("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelableArrayList() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi] + Intent[] ans = b.getParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + Intent[] ans = b.getParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + fun testBundleDetectUnsafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi] + SparseArray<Intent> ans = b.getSparseParcelableArray("key"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testBundleDoesNotDetectSafeGetSparseParcelableArray() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + import android.os.Bundle; + + public class TestClass { + private TestClass(Bundle b) { + SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + /** Intent Tests */ + + fun testIntentDetectUnsafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name"); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi] + Intent ans = i.getParcelableExtra("name"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """.addLineContinuation() + ) + } + + fun testIntentDoesNotDetectSafeGetParcelableExtra() { + lint() + .files( + java( + """ + package test.pkg; + import android.content.Intent; + + public class TestClass { + private TestClass(Intent i) { + Intent ans = i.getParcelableExtra("name", Intent.class); + } + } + """ + ).indented(), + *includes + ) + .run() + .expect("No warnings.") + } + + + /** Stubs for classes used for testing */ + + + private val includes = + arrayOf( + manifest().minSdk("33"), + java( + """ + package android.os; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + import java.util.HashMap; + + public final class Parcel { + // Deprecated + public Object[] readArray(ClassLoader loader) { return null; } + public ArrayList readArrayList(ClassLoader loader) { return null; } + public HashMap readHashMap(ClassLoader loader) { return null; } + public void readList(List outVal, ClassLoader loader) {} + public void readMap(Map outVal, ClassLoader loader) {} + public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; } + public Parcelable[] readParcelableArray(ClassLoader loader) { return null; } + public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; } + public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; } + public Serializable readSerializable() { return null; } + public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; } + + // Replacements + public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; } + public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; } + public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; } + public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {} + public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {} + public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; } + public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; } + public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; } + public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; } + public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; } + public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; } + } + """ + ).indented(), + java( + """ + package android.os; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + import java.util.HashMap; + + public final class Bundle { + // Deprecated + public <T extends Parcelable> T getParcelable(String key) { return null; } + public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; } + public Parcelable[] getParcelableArray(String key) { return null; } + public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; } + + // Replacements + public <T> T getParcelable(String key, Class<T> clazz) { return null; } + public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; } + public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; } + public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; } + + } + """ + ).indented(), + java( + """ + package android.os; + public interface Parcelable {} + """ + ).indented(), + java( + """ + package android.content; + public class Intent implements Parcelable, Cloneable { + // Deprecated + public <T extends Parcelable> T getParcelableExtra(String name) { return null; } + + // Replacements + public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; } + + } + """ + ).indented(), + java( + """ + package android.util; + public class SparseArray<E> implements Cloneable {} + """ + ).indented(), + ) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/global/Android.bp b/tools/lint/global/Android.bp new file mode 100644 index 000000000000..bedb7bd78a29 --- /dev/null +++ b/tools/lint/global/Android.bp @@ -0,0 +1,65 @@ +// Copyright (C) 2022 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "AndroidGlobalLintChecker", + srcs: ["checks/src/main/java/**/*.kt"], + plugins: ["auto_service_plugin"], + libs: [ + "auto_service_annotations", + "lint_api", + ], + static_libs: ["AndroidCommonLint"], + kotlincflags: ["-Xjvm-default=all"], + dist: { + targets: ["droid"], + }, +} + +java_test_host { + name: "AndroidGlobalLintCheckerTest", + srcs: ["checks/src/test/java/**/*.kt"], + static_libs: [ + "AndroidGlobalLintChecker", + "junit", + "lint", + "lint_tests", + ], + test_options: { + unit_test: true, + tradefed_options: [ + { + // lint bundles in some classes that were built with older versions + // of libraries, and no longer load. Since tradefed tries to load + // all classes in the jar to look for tests, it crashes loading them. + // Exclude these classes from tradefed's search. + name: "exclude-paths", + value: "org/apache", + }, + { + name: "exclude-paths", + value: "META-INF", + }, + ], + }, +} diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt index a6fd9bba6192..a20266a9b140 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2022 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. @@ -19,21 +19,19 @@ package com.google.android.lint import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.aidl.EnforcePermissionDetector +import com.google.android.lint.aidl.EnforcePermissionHelperDetector +import com.google.android.lint.aidl.SimpleManualPermissionEnforcementDetector import com.google.auto.service.AutoService @AutoService(IssueRegistry::class) @Suppress("UnstableApiUsage") -class AndroidFrameworkIssueRegistry : IssueRegistry() { +class AndroidGlobalIssueRegistry : IssueRegistry() { override val issues = listOf( - CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN, - CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN, - CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS, - CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK, - CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY, - CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY, - CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED, EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, - EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION, + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + SimpleManualPermissionEnforcementDetector.ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT, ) override val api: Int @@ -45,6 +43,6 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() { override val vendor: Vendor = Vendor( vendorName = "Android", feedbackUrl = "http://b/issues/new?component=315013", - contact = "brufino@google.com" + contact = "repsonsible-apis@google.com" ) -} +}
\ No newline at end of file diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt new file mode 100644 index 000000000000..ab6d871d6ea6 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.SourceCodeScanner +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod + +/** + * Abstract class for detectors that look for methods implementing + * generated AIDL interface stubs + */ +abstract class AidlImplementationDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + val interfaceName = getContainingAidlInterface(context, node) + .takeUnless(EXCLUDED_CPP_INTERFACES::contains) ?: return + val body = (node.uastBody as? UBlockExpression) ?: return + visitAidlMethod(context, node, interfaceName, body) + } + } + + abstract fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression, + ) +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt new file mode 100644 index 000000000000..dcfbe953f955 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +const val ANNOTATION_ENFORCE_PERMISSION = "android.annotation.EnforcePermission" +const val ANNOTATION_REQUIRES_NO_PERMISSION = "android.annotation.RequiresNoPermission" +const val ANNOTATION_PERMISSION_MANUALLY_ENFORCED = "android.annotation.PermissionManuallyEnforced" + +val AIDL_PERMISSION_ANNOTATIONS = listOf( + ANNOTATION_ENFORCE_PERMISSION, + ANNOTATION_REQUIRES_NO_PERMISSION, + ANNOTATION_PERMISSION_MANUALLY_ENFORCED +) + +const val BINDER_CLASS = "android.os.Binder" +const val IINTERFACE_INTERFACE = "android.os.IInterface" + +const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission" + +/** + * If a non java (e.g. c++) backend is enabled, the @EnforcePermission + * annotation cannot be used. At time of writing, the mechanism + * is not implemented for non java backends. + * TODO: b/242564874 (have lint know which interfaces have the c++ backend enabled) + * rather than hard coding this list? + */ +val EXCLUDED_CPP_INTERFACES = listOf( + "AdbTransportType", + "FingerprintAndPairDevice", + "IAdbCallback", + "IAdbManager", + "PairDevice", + "IStatsBootstrapAtomService", + "StatsBootstrapAtom", + "StatsBootstrapAtomValue", + "FixedSizeArrayExample", + "PlaybackTrackMetadata", + "RecordTrackMetadata", + "SinkMetadata", + "SourceMetadata", + "IUpdateEngineStable", + "IUpdateEngineStableCallback", + "AudioCapabilities", + "ConfidenceLevel", + "ModelParameter", + "ModelParameterRange", + "Phrase", + "PhraseRecognitionEvent", + "PhraseRecognitionExtra", + "PhraseSoundModel", + "Properties", + "RecognitionConfig", + "RecognitionEvent", + "RecognitionMode", + "RecognitionStatus", + "SoundModel", + "SoundModelType", + "Status", + "IThermalService", + "IPowerManager", + "ITunerResourceManager" +) diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt new file mode 100644 index 000000000000..0baac2c7aacf --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.AnnotationInfo +import com.android.tools.lint.detector.api.AnnotationOrigin +import com.android.tools.lint.detector.api.AnnotationUsageInfo +import com.android.tools.lint.detector.api.AnnotationUsageType +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.ConstantEvaluator +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.toUElement + +/** + * Lint Detector that ensures that any method overriding a method annotated + * with @EnforcePermission is also annotated with the exact same annotation. + * The intent is to surface the effective permission checks to the service + * implementations. + * + * This is done with 2 mechanisms: + * 1. Visit any annotation usage, to ensure that any derived class will have + * the correct annotation on each methods. This is for the top to bottom + * propagation. + * 2. Visit any annotation, to ensure that if a method is annotated, it has + * its ancestor also annotated. This is to avoid having an annotation on a + * Java method without the corresponding annotation on the AIDL interface. + */ +class EnforcePermissionDetector : Detector(), SourceCodeScanner { + + override fun applicableAnnotations(): List<String> { + return listOf(ANNOTATION_ENFORCE_PERMISSION) + } + + override fun getApplicableUastTypes(): List<Class<out UElement>> { + return listOf(UAnnotation::class.java) + } + + private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> { + if (elem is PsiArrayInitializerMemberValue) + return elem.getInitializers().map { it as PsiElement }.toTypedArray() + return elem.getChildren() + } + + private fun areAnnotationsEquivalent( + context: JavaContext, + anno1: PsiAnnotation, + anno2: PsiAnnotation + ): Boolean { + if (anno1.qualifiedName != anno2.qualifiedName) { + return false + } + val attr1 = anno1.parameterList.attributes + val attr2 = anno2.parameterList.attributes + if (attr1.size != attr2.size) { + return false + } + for (i in attr1.indices) { + if (attr1[i].name != attr2[i].name) { + return false + } + val value1 = attr1[i].value ?: return false + val value2 = attr2[i].value ?: return false + // Try to compare values directly with each other. + val v1 = ConstantEvaluator.evaluate(context, value1) + val v2 = ConstantEvaluator.evaluate(context, value2) + if (v1 != null && v2 != null) { + if (v1 != v2) { + return false + } + } else { + val children1 = annotationValueGetChildren(value1) + val children2 = annotationValueGetChildren(value2) + if (children1.size != children2.size) { + return false + } + for (j in children1.indices) { + val c1 = ConstantEvaluator.evaluate(context, children1[j]) + val c2 = ConstantEvaluator.evaluate(context, children2[j]) + if (c1 != c2) { + return false + } + } + } + } + return true + } + + private fun compareMethods( + context: JavaContext, + element: UElement, + overridingMethod: PsiMethod, + overriddenMethod: PsiMethod, + checkEquivalence: Boolean = true + ) { + // If method is not from a Stub subclass, this method shouldn't use @EP at all. + // This is handled by EnforcePermissionHelperDetector. + if (!isContainedInSubclassOfStub(context, overridingMethod.toUElement() as? UMethod)) { + return + } + val overridingAnnotation = overridingMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val overriddenAnnotation = overriddenMethod.getAnnotation(ANNOTATION_ENFORCE_PERMISSION) + val location = context.getLocation(element) + val overridingClass = overridingMethod.parent as PsiClass + val overriddenClass = overriddenMethod.parent as PsiClass + val overridingName = "${overridingClass.name}.${overridingMethod.name}" + val overriddenName = "${overriddenClass.name}.${overriddenMethod.name}" + if (overridingAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is annotated with @EnforcePermission. The same annotation must be used " + + "on $overridingName" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (overriddenAnnotation == null) { + val msg = "The method $overridingName overrides the method $overriddenName which " + + "is not annotated with @EnforcePermission. The same annotation must be " + + "used on $overriddenName. Did you forget to annotate the AIDL definition?" + context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg) + } else if (checkEquivalence && !areAnnotationsEquivalent( + context, overridingAnnotation, overriddenAnnotation)) { + val msg = "The method $overridingName is annotated with " + + "${overridingAnnotation.text} which differs from the overridden " + + "method $overriddenName: ${overriddenAnnotation.text}. The same " + + "annotation must be used for both methods." + context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg) + } + } + + override fun visitAnnotationUsage( + context: JavaContext, + element: UElement, + annotationInfo: AnnotationInfo, + usageInfo: AnnotationUsageInfo + ) { + if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE && + annotationInfo.origin == AnnotationOrigin.METHOD) { + val overridingMethod = element.sourcePsi as PsiMethod + val overriddenMethod = usageInfo.referenced as PsiMethod + compareMethods(context, element, overridingMethod, overriddenMethod) + } + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_ENFORCE_PERMISSION) { + return + } + val method = node.uastParent as? UMethod ?: return + val overridingMethod = method as PsiMethod + val parents = overridingMethod.findSuperMethods() + for (overriddenMethod in parents) { + // The equivalence check can be skipped, if both methods are + // annotated, it will be verified by visitAnnotationUsage. + compareMethods(context, method, overridingMethod, + overriddenMethod, checkEquivalence = false) + } + } + } + } + + companion object { + val EXPLANATION = """ + The @EnforcePermission annotation is used to indicate that the underlying binder code + has already verified the caller's permissions before calling the appropriate method. The + verification code is usually generated by the AIDL compiler, which also takes care of + annotating the generated Java code. + + In order to surface that information to platform developers, the same annotation must be + used on the implementation class or methods. + """ + + val ISSUE_MISSING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MissingEnforcePermissionAnnotation", + briefDescription = "Missing @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val ISSUE_MISMATCHING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MismatchingEnforcePermissionAnnotation", + briefDescription = "Incorrect @EnforcePermission annotation on Binder method", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt new file mode 100644 index 000000000000..25d208db14ec --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue +import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues +import com.android.tools.lint.detector.api.findSelector +import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.findCallExpression +import com.google.android.lint.getPermissionMethodAnnotation +import com.google.android.lint.hasPermissionNameAnnotation +import com.google.android.lint.isPermissionMethodCall +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import org.jetbrains.kotlin.psi.psiUtil.parameterIndex +import org.jetbrains.uast.UBinaryExpression +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UExpressionList +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UThrowExpression +import org.jetbrains.uast.UastBinaryOperator +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.skipParenthesizedExprDown +import org.jetbrains.uast.visitor.AbstractUastVisitor + +/** + * Helper class that facilitates the creation of lint auto fixes + */ +data class EnforcePermissionFix( + val manualCheckLocations: List<Location>, + val permissionNames: List<String>, + val errorLevel: Boolean, + val anyOf: Boolean, +) { + fun toLintFix(context: JavaContext, node: UMethod): LintFix { + val methodLocation = context.getLocation(node) + val replaceOrRemoveFixes = manualCheckLocations.mapIndexed { index, manualCheckLocation -> + if (index == 0) { + // Replace the first manual check with a call to the helper method + getHelperMethodFix(node, manualCheckLocation, false) + } else { + // Remove all subsequent manual checks + LintFix.create() + .replace() + .reformat(true) + .range(manualCheckLocation) + .with("") + .autoFix() + .build() + } + } + + // Annotate the method with @EnforcePermission(...) + val annotateFix = LintFix.create() + .annotate(annotation) + .range(methodLocation) + .autoFix() + .build() + + return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray()) + } + + private val annotation: String + get() { + val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } + + val attributeName = + if (permissionNames.size > 1) { + if (anyOf) "anyOf" else "allOf" + } else null + + val annotationParameter = + if (attributeName != null) "$attributeName={$quotedPermissions}" + else quotedPermissions + + return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" + } + + companion object { + /** + * Walks the expressions in a block, looking for simple permission checks. + * + * As soon as something other than a permission check is encountered, stop looking, + * as some other business logic is happening that prevents an automated fix. + */ + fun fromBlockExpression( + context: JavaContext, + blockExpression: UBlockExpression + ): EnforcePermissionFix? { + try { + val singleFixes = mutableListOf<EnforcePermissionFix>() + for (expression in blockExpression.expressions) { + val fix = fromExpression(context, expression) ?: break + singleFixes.add(fix) + } + return compose(singleFixes) + } catch (e: AnyOfAllOfException) { + return null + } + } + + /** + * Conditionally constructs EnforcePermissionFix from any UExpression + * + * @return EnforcePermissionFix if the expression boils down to a permission check, + * else null + */ + fun fromExpression( + context: JavaContext, + expression: UExpression + ): EnforcePermissionFix? { + val trimmedExpression = expression.skipParenthesizedExprDown() + if (trimmedExpression is UIfExpression) { + return fromIfExpression(context, trimmedExpression) + } + findCallExpression(trimmedExpression)?.let { + return fromCallExpression(context, it) + } + return null + } + + /** + * Conditionally constructs EnforcePermissionFix from a UCallExpression + * + * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null + */ + fun fromCallExpression( + context: JavaContext, + callExpression: UCallExpression + ): EnforcePermissionFix? { + val method = callExpression.resolve()?.getUMethod() ?: return null + val annotation = getPermissionMethodAnnotation(method) ?: return null + val returnsVoid = method.returnType == PsiType.VOID + val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false + return EnforcePermissionFix( + listOf(getPermissionCheckLocation(context, callExpression)), + getPermissionCheckValues(callExpression), + errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf), + anyOf, + ) + } + + /** + * Conditionally constructs EnforcePermissionFix from a UCallExpression + * + * @return EnforcePermissionFix IF AND ONLY IF: + * * The condition of the if statement compares the return value of a + * PermissionMethod to one of the PackageManager.PermissionResult values + * * The expression inside the if statement does nothing but throw SecurityException + */ + fun fromIfExpression( + context: JavaContext, + ifExpression: UIfExpression + ): EnforcePermissionFix? { + val condition = ifExpression.condition.skipParenthesizedExprDown() + if (condition !is UBinaryExpression) return null + + val maybeLeftCall = findCallExpression(condition.leftOperand) + val maybeRightCall = findCallExpression(condition.rightOperand) + + val (callExpression, comparison) = + if (maybeLeftCall is UCallExpression) { + Pair(maybeLeftCall, condition.rightOperand) + } else if (maybeRightCall is UCallExpression) { + Pair(maybeRightCall, condition.leftOperand) + } else return null + + val permissionMethodAnnotation = getPermissionMethodAnnotation( + callExpression.resolve()?.getUMethod()) ?: return null + + val equalityCheck = + when (comparison.findSelector().asSourceString() + .filterNot(Char::isWhitespace)) { + "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS + "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS + else -> return null + } + + if (condition.operator != equalityCheck) return null + + val throwExpression: UThrowExpression? = + ifExpression.thenExpression as? UThrowExpression + ?: (ifExpression.thenExpression as? UBlockExpression) + ?.expressions?.firstOrNull() + as? UThrowExpression + + + val thrownClass = (throwExpression?.thrownExpression?.getExpressionType() + as? PsiClassType)?.resolve() ?: return null + if (!context.evaluator.inheritsFrom( + thrownClass, "java.lang.SecurityException")){ + return null + } + + val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false + + return EnforcePermissionFix( + listOf(context.getLocation(ifExpression)), + getPermissionCheckValues(callExpression), + errorLevel = isErrorLevel(throws = true, orSelf = orSelf), + anyOf = anyOf + ) + } + + + fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix? { + if (individuals.isEmpty()) return null + val anyOfs = individuals.filter(EnforcePermissionFix::anyOf) + // anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf + // and others that aren't, we don't know what to do. + if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) { + throw AnyOfAllOfException() + } + return EnforcePermissionFix( + individuals.flatMap(EnforcePermissionFix::manualCheckLocations), + individuals.flatMap(EnforcePermissionFix::permissionNames), + errorLevel = individuals.all(EnforcePermissionFix::errorLevel), + anyOf = anyOfs.isNotEmpty() + ) + } + + /** + * Given a permission check, get its proper location + * so that a lint fix can remove the entire expression + */ + private fun getPermissionCheckLocation( + context: JavaContext, + callExpression: UCallExpression + ): + Location { + val javaPsi = callExpression.javaPsi!! + return Location.create( + context.file, + javaPsi.containingFile?.text, + javaPsi.textRange.startOffset, + // unfortunately the element doesn't include the ending semicolon + javaPsi.textRange.endOffset + 1 + ) + } + + /** + * Given a @PermissionMethod, find arguments annotated with @PermissionName + * and pull out the permission value(s) being used. Also evaluates nested calls + * to @PermissionMethod(s) in the given method's body. + */ + @Throws(AnyOfAllOfException::class) + private fun getPermissionCheckValues( + callExpression: UCallExpression + ): List<String> { + if (!isPermissionMethodCall(callExpression)) return emptyList() + + val result = mutableSetOf<String>() // protect against duplicate permission values + val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice + val bfsQueue = ArrayDeque(listOf(callExpression)) + + var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL + + // Bread First Search - evaluating nested @PermissionMethod(s) in the available + // source code for @PermissionName(s). + while (bfsQueue.isNotEmpty()) { + val currentCallExpression = bfsQueue.removeFirst() + visitedCalls.add(currentCallExpression) + val currentPermissions = findPermissions(currentCallExpression) + result.addAll(currentPermissions) + + val currentAnnotation = getPermissionMethodAnnotation( + currentCallExpression.resolve()?.getUMethod()) + val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false + + // anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods + // where we start in an anyOf state and switch to allOf, or vice versa, + // we don't know what to do. + if (anyOfAllOfState == AnyOfAllOfState.INITIAL) { + if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF + else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF + } + + if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) { + throw AnyOfAllOfException() + } + + if (anyOfAllOfState == AnyOfAllOfState.ANY_OF && + !currentAnyOf && currentPermissions.size > 1) { + throw AnyOfAllOfException() + } + + currentCallExpression.resolve()?.getUMethod() + ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue)) + } + + return result.toList() + } + + private enum class AnyOfAllOfState { + INITIAL, + ANY_OF, + ALL_OF + } + + /** + * Adds visited permission method calls to the provided + * queue in support of the BFS traversal happening while + * this is used + */ + private class PermissionCheckValuesVisitor( + val visitedCalls: Set<UCallExpression>, + val bfsQueue: ArrayDeque<UCallExpression> + ) : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (isPermissionMethodCall(node) && node !in visitedCalls) { + bfsQueue.add(node) + } + return false + } + } + + private fun findPermissions( + callExpression: UCallExpression, + ): List<String> { + val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod()) + + val hardCodedPermissions = (getAnnotationStringValues(annotation, "value") + ?: emptyArray()) + .toList() + + val indices = callExpression.resolve()?.getUMethod() + ?.uastParameters + ?.filter(::hasPermissionNameAnnotation) + ?.mapNotNull { it.sourcePsi?.parameterIndex() } + ?: emptyList() + + val argPermissions = indices + .flatMap { i -> + when (val argument = callExpression.getArgumentForParameter(i)) { + null -> listOf(null) + is UExpressionList -> // varargs e.g. someMethod(String...) + argument.expressions.map(UExpression::evaluateString) + else -> listOf(argument.evaluateString()) + } + } + .filterNotNull() + + return hardCodedPermissions + argPermissions + } + + /** + * If we detect that the PermissionMethod enforces that permission is granted, + * AND is of the "orSelf" variety, we are very confident that this is a behavior + * preserving migration to @EnforcePermission. Thus, the incident should be ERROR + * level. + */ + private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf + } +} +/** + * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission - + * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf, + * we don't know which to apply. + */ +class AnyOfAllOfException : Exception() { + override val message: String = "anyOf/allOf permission methods cannot be mixed" +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt new file mode 100644 index 000000000000..df13af516514 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.google.android.lint.findCallExpression +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UDeclarationsExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.skipParenthesizedExprDown + +class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { + override fun getApplicableUastTypes(): List<Class<out UElement?>> = + listOf(UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context) + + private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (context.evaluator.isAbstract(node)) return + if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return + + if (!isContainedInSubclassOfStub(context, node)) { + context.report( + ISSUE_MISUSING_ENFORCE_PERMISSION, + node, + context.getLocation(node), + "The class of ${node.name} does not inherit from an AIDL generated Stub class" + ) + return + } + + val targetExpression = getHelperMethodCallSourceString(node) + val message = + "Method must start with $targetExpression or super.${node.name}(), if applicable" + + val firstExpression = (node.uastBody as? UBlockExpression) + ?.expressions?.firstOrNull() + + if (firstExpression == null) { + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + message, + ) + return + } + + val firstExpressionSource = firstExpression.skipParenthesizedExprDown() + .asSourceString() + .filterNot(Char::isWhitespace) + + if (firstExpressionSource != targetExpression && + firstExpressionSource != "super.$targetExpression") { + // calling super.<methodName>() is also legal + val directSuper = context.evaluator.getSuperMethod(node) + val firstCall = findCallExpression(firstExpression)?.resolve() + if (directSuper != null && firstCall == directSuper) return + + val locationTarget = getLocationTarget(firstExpression) + val expressionLocation = context.getLocation(locationTarget) + + context.report( + ISSUE_ENFORCE_PERMISSION_HELPER, + context.getLocation(node), + message, + getHelperMethodFix(node, expressionLocation), + ) + } + } + } + + companion object { + private const val HELPER_SUFFIX = "_enforcePermission" + + private const val EXPLANATION = """ + The @EnforcePermission annotation can only be used on methods whose class extends from + the Stub class generated by the AIDL compiler. When @EnforcePermission is applied, the + AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX. + + yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can + either call it directly, or call it indirectly via super.yourMethodName(). + """ + + val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( + id = "MissingEnforcePermissionHelper", + briefDescription = """Missing permission-enforcing method call in AIDL method + |annotated with @EnforcePermission""".trimMargin(), + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionHelperDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + val ISSUE_MISUSING_ENFORCE_PERMISSION: Issue = Issue.create( + id = "MisusingEnforcePermissionAnnotation", + briefDescription = "@EnforcePermission cannot be used here", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + EnforcePermissionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + /** + * handles an edge case with UDeclarationsExpression, where sourcePsi is null, + * resulting in an incorrect Location if used directly + */ + private fun getLocationTarget(firstExpression: UExpression): PsiElement? { + if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi + if (firstExpression is UDeclarationsExpression) { + return firstExpression.declarations.firstOrNull()?.sourcePsi + } + return null + } + } +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt new file mode 100644 index 000000000000..d41fee3fc0dc --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.Location +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiReferenceList +import org.jetbrains.uast.UMethod + +/** + * Given a UMethod, determine if this method is + * the entrypoint to an interface generated by AIDL, + * returning the interface name if so, otherwise returning null + */ +fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? { + if (!isContainedInSubclassOfStub(context, node)) return null + for (superMethod in node.findSuperMethods()) { + for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements + ?: continue) { + if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) { + return superMethod.containingClass?.name + } + } + } + return null +} + +fun isContainedInSubclassOfStub(context: JavaContext, node: UMethod?): Boolean { + var superClass = node?.containingClass?.superClass + while (superClass != null) { + if (isStub(context, superClass)) return true + superClass = superClass.superClass + } + return false +} + +fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean { + if (psiClass == null) return false + if (psiClass.name != "Stub") return false + if (!context.evaluator.isStatic(psiClass)) return false + if (!context.evaluator.isAbstract(psiClass)) return false + + if (!hasSingleAncestor(psiClass.extendsList, BINDER_CLASS)) return false + + val parent = psiClass.parent as? PsiClass ?: return false + if (!hasSingleAncestor(parent.extendsList, IINTERFACE_INTERFACE)) return false + + val parentName = parent.qualifiedName ?: return false + if (!hasSingleAncestor(psiClass.implementsList, parentName)) return false + + return true +} + +private fun hasSingleAncestor(references: PsiReferenceList?, qualifiedName: String) = + references != null && + references.referenceElements.size == 1 && + references.referenceElements[0].qualifiedName == qualifiedName + +fun getHelperMethodCallSourceString(node: UMethod) = "${node.name}$AIDL_PERMISSION_HELPER_SUFFIX()" + +fun getHelperMethodFix( + node: UMethod, + manualCheckLocation: Location, + prepend: Boolean = true +): LintFix { + val helperMethodSource = getHelperMethodCallSourceString(node) + val indent = " ".repeat(manualCheckLocation.start?.column ?: 0) + val newText = "$helperMethodSource;${if (prepend) "\n\n$indent" else ""}" + + val fix = LintFix.create() + .replace() + .range(manualCheckLocation) + .with(newText) + .reformat(true) + .autoFix() + + if (prepend) fix.beginning() + + return fix.build() +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt new file mode 100644 index 000000000000..c7be36efd991 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Incident +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UMethod + +/** + * Looks for methods implementing generated AIDL interface stubs + * that can have simple permission checks migrated to + * @EnforcePermission annotations + */ +@Suppress("UnstableApiUsage") +class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { + override fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression + ) { + val enforcePermissionFix = EnforcePermissionFix.fromBlockExpression(context, body) ?: return + val lintFix = enforcePermissionFix.toLintFix(context, node) + val message = + "$interfaceName permission check ${ + if (enforcePermissionFix.errorLevel) "should" else "can" + } be converted to @EnforcePermission annotation" + + val incident = Incident( + ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT, + enforcePermissionFix.manualCheckLocations.last(), + message, + lintFix + ) + + // TODO(b/265014041): turn on errors once all code that would cause one is fixed + // if (enforcePermissionFix.errorLevel) { + // incident.overrideSeverity(Severity.ERROR) + // } + + context.report(incident) + } + + companion object { + + private val EXPLANATION = """ + Whenever possible, method implementations of AIDL interfaces should use the @EnforcePermission + annotation to declare the permissions to be enforced. The verification code is then + generated by the AIDL compiler, which also takes care of annotating the generated java + code. + + This reduces the risk of bugs around these permission checks (that often become vulnerabilities). + It also enables easier auditing and review. + + Please migrate to an @EnforcePermission annotation. (See: go/aidl-enforce-howto) + """.trimIndent() + + @JvmField + val ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT = Issue.create( + id = "SimpleManualPermissionEnforcement", + briefDescription = "Manual permission check can be @EnforcePermission annotation", + explanation = EXPLANATION, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + SimpleManualPermissionEnforcementDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt new file mode 100644 index 000000000000..f2930d9faac7 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorCodegenTest.kt @@ -0,0 +1,557 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionDetectorCodegenTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun test_generated_IProtected() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtected extends android.os.IInterface + { + /** Default implementation for IProtected. */ + public static class Default implements android.aidl.tests.permission.IProtected + { + @Override public void PermissionProtected() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) { + return ((android.aidl.tests.permission.IProtected)iin); + } + return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_PermissionProtected: + { + return "PermissionProtected"; + } + case TRANSACTION_MultiplePermissionsAll: + { + return "MultiplePermissionsAll"; + } + case TRANSACTION_MultiplePermissionsAny: + { + return "MultiplePermissionsAny"; + } + case TRANSACTION_NonManifestPermission: + { + return "NonManifestPermission"; + } + case TRANSACTION_SetGranted: + { + return "SetGranted"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_PermissionProtected: + { + this.PermissionProtected(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAll: + { + this.MultiplePermissionsAll(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAny: + { + this.MultiplePermissionsAny(); + reply.writeNoException(); + break; + } + case TRANSACTION_NonManifestPermission: + { + this.NonManifestPermission(); + reply.writeNoException(); + break; + } + case TRANSACTION_SetGranted: + { + java.util.List<java.lang.String> _arg0; + _arg0 = data.createStringArrayList(); + data.enforceNoDataAvail(); + this.SetGranted(_arg0); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtected + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void PermissionProtected() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStringList(permissions); + boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for PermissionProtected */ + protected void PermissionProtected_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source); + } + static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for MultiplePermissionsAll */ + protected void MultiplePermissionsAll_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + /** Helper method to enforce permissions for MultiplePermissionsAny */ + protected void MultiplePermissionsAny_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + /** Helper method to enforce permissions for NonManifestPermission */ + protected void NonManifestPermission_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source); + } + static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + /** @hide */ + public int getMaxTransactionId() + { + return 4; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void PermissionProtected() throws android.os.RemoteException; + @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAll() throws android.os.RemoteException; + @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAny() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void NonManifestPermission() throws android.os.RemoteException; + // Used by the integration tests to dynamically set permissions that are considered granted. + @android.annotation.RequiresNoPermission + public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun test_generated_IProtectedInterface() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtectedInterface extends android.os.IInterface + { + /** Default implementation for IProtectedInterface. */ + public static class Default implements android.aidl.tests.permission.IProtectedInterface + { + @Override public void Method1() throws android.os.RemoteException + { + } + @Override public void Method2() throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) { + return ((android.aidl.tests.permission.IProtectedInterface)iin); + } + return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_Method1: + { + return "Method1"; + } + case TRANSACTION_Method2: + { + return "Method2"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_Method1: + { + this.Method1(); + reply.writeNoException(); + break; + } + case TRANSACTION_Method2: + { + this.Method2(); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtectedInterface + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void Method1() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void Method2() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for Method1 */ + protected void Method1_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for Method2 */ + protected void Method2_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + /** @hide */ + public int getMaxTransactionId() + { + return 1; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method1() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method2() throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /* Stubs */ + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub) +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt new file mode 100644 index 000000000000..75b00737a168 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION, + EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass2 extends IFooMethod.Stub { + @Override + @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass11 extends IFooMethod.Stub { + @Override + @EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass111 extends IFooMethod.Stub { + @Override + @EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAllLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass12 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass121 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDetectIssuesMismatchingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass4 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \ + which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesEmptyAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass41 extends IFooMethod.Stub { + @android.annotation.EnforcePermission + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \ + which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAnyAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass9 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) + public void testMethodAny() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \ + which differs from the overridden method Stub.testMethodAny: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAny() {} + ~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass91 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \ + @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAnyLiteral: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAllAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass10 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) + public void testMethodAll() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \ + which differs from the overridden method Stub.testMethodAll: \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAll() {} + ~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass101 extends IFooMethod.Stub { + @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \ + @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAllLiteral: \ + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAllLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass6 extends IFooMethod.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \ + The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesExtraAnnotationMethod() { + lint().files(java( + """ + package test.pkg; + public class TestClass7 extends IBar.Stub { + @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect(""" + src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \ + The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation()) + } + + fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() { + lint().files(java( + """ + package test.pkg; + public class Default extends IFooMethod.Stub { + public void testMethod() {} + } + """).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/Default.java:3: Error: The method Default.testMethod \ + overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation() + ) + } + + fun testDoesDetectIssuesShortStringsNotAllowed() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass121 extends IFooMethod.Stub { + @Override + @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + } + """).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/pkg/TestClass121.java:6: Error: The method \ + TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \ + which differs from the overridden method Stub.testMethodAnyLiteral: \ + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ + The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """.addLineContinuation() + ) + } + + /* Stubs */ + + // A service with permission annotation on the method. + private val interfaceIFooMethodStub: TestFile = java( + """ + public interface IFooMethod extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IFooMethod { + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod() {} + @Override + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + @Override + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + @Override + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + @Override + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethod(); + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAny() {} + @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAll() {} + @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) + public void testMethodAllLiteral() {} + } + """ + ).indented() + + // A service without any permission annotation. + private val interfaceIBarStub: TestFile = java( + """ + public interface IBar extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IBar { + @Override + public void testMethod() {} + } + public void testMethod(); + } + """ + ).indented() + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String NFC = "android.permission.NFC"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub, + manifestPermissionStub, enforcePermissionAnnotationStub) + + // Substitutes "backslash + new line" with an empty string to imitate line continuation + private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "") +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt new file mode 100644 index 000000000000..5a63bb4084d2 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorCodegenTest.kt @@ -0,0 +1,557 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestMode +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class EnforcePermissionHelperDetectorCodegenTest : LintDetectorTest() { + override fun getDetector(): Detector = EnforcePermissionHelperDetector() + + override fun getIssues(): List<Issue> = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun test_generated_IProtected() { + lint().testModes(TestMode.DEFAULT).files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtected extends android.os.IInterface + { + /** Default implementation for IProtected. */ + public static class Default implements android.aidl.tests.permission.IProtected + { + @Override public void PermissionProtected() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtected + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtected interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtected asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtected))) { + return ((android.aidl.tests.permission.IProtected)iin); + } + return new android.aidl.tests.permission.IProtected.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_PermissionProtected: + { + return "PermissionProtected"; + } + case TRANSACTION_MultiplePermissionsAll: + { + return "MultiplePermissionsAll"; + } + case TRANSACTION_MultiplePermissionsAny: + { + return "MultiplePermissionsAny"; + } + case TRANSACTION_NonManifestPermission: + { + return "NonManifestPermission"; + } + case TRANSACTION_SetGranted: + { + return "SetGranted"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_PermissionProtected: + { + this.PermissionProtected(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAll: + { + this.MultiplePermissionsAll(); + reply.writeNoException(); + break; + } + case TRANSACTION_MultiplePermissionsAny: + { + this.MultiplePermissionsAny(); + reply.writeNoException(); + break; + } + case TRANSACTION_NonManifestPermission: + { + this.NonManifestPermission(); + reply.writeNoException(); + break; + } + case TRANSACTION_SetGranted: + { + java.util.List<java.lang.String> _arg0; + _arg0 = data.createStringArrayList(); + data.enforceNoDataAvail(); + this.SetGranted(_arg0); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtected + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void PermissionProtected() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_PermissionProtected, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAll() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAll, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void MultiplePermissionsAny() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_MultiplePermissionsAny, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void NonManifestPermission() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_NonManifestPermission, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + // Used by the integration tests to dynamically set permissions that are considered granted. + @Override public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStringList(permissions); + boolean _status = mRemote.transact(Stub.TRANSACTION_SetGranted, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_PermissionProtected = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for PermissionProtected */ + protected void PermissionProtected_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.READ_PHONE_STATE, source); + } + static final int TRANSACTION_MultiplePermissionsAll = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for MultiplePermissionsAll */ + protected void MultiplePermissionsAll_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAllOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_MultiplePermissionsAny = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + /** Helper method to enforce permissions for MultiplePermissionsAny */ + protected void MultiplePermissionsAny_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermissionAnyOf(new String[]{android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}, source); + } + static final int TRANSACTION_NonManifestPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + /** Helper method to enforce permissions for NonManifestPermission */ + protected void NonManifestPermission_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, source); + } + static final int TRANSACTION_SetGranted = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + /** @hide */ + public int getMaxTransactionId() + { + return 4; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void PermissionProtected() throws android.os.RemoteException; + @android.annotation.EnforcePermission(allOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAll() throws android.os.RemoteException; + @android.annotation.EnforcePermission(anyOf = {android.Manifest.permission.INTERNET, android.Manifest.permission.VIBRATE}) + public void MultiplePermissionsAny() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void NonManifestPermission() throws android.os.RemoteException; + // Used by the integration tests to dynamically set permissions that are considered granted. + @android.annotation.RequiresNoPermission + public void SetGranted(java.util.List<java.lang.String> permissions) throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun test_generated_IProtectedInterface() { + lint().files( + java( + """ + /* + * This file is auto-generated. DO NOT MODIFY. + */ + package android.aidl.tests.permission; + public interface IProtectedInterface extends android.os.IInterface + { + /** Default implementation for IProtectedInterface. */ + public static class Default implements android.aidl.tests.permission.IProtectedInterface + { + @Override public void Method1() throws android.os.RemoteException + { + } + @Override public void Method2() throws android.os.RemoteException + { + } + @Override + public android.os.IBinder asBinder() { + return null; + } + } + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends android.os.Binder implements android.aidl.tests.permission.IProtectedInterface + { + private final android.os.PermissionEnforcer mEnforcer; + /** Construct the stub using the Enforcer provided. */ + public Stub(android.os.PermissionEnforcer enforcer) + { + this.attachInterface(this, DESCRIPTOR); + if (enforcer == null) { + throw new IllegalArgumentException("enforcer cannot be null"); + } + mEnforcer = enforcer; + } + @Deprecated + /** Default constructor. */ + public Stub() { + this(android.os.PermissionEnforcer.fromContext( + android.app.ActivityThread.currentActivityThread().getSystemContext())); + } + /** + * Cast an IBinder object into an android.aidl.tests.permission.IProtectedInterface interface, + * generating a proxy if needed. + */ + public static android.aidl.tests.permission.IProtectedInterface asInterface(android.os.IBinder obj) + { + if ((obj==null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof android.aidl.tests.permission.IProtectedInterface))) { + return ((android.aidl.tests.permission.IProtectedInterface)iin); + } + return new android.aidl.tests.permission.IProtectedInterface.Stub.Proxy(obj); + } + @Override public android.os.IBinder asBinder() + { + return this; + } + /** @hide */ + public static java.lang.String getDefaultTransactionName(int transactionCode) + { + switch (transactionCode) + { + case TRANSACTION_Method1: + { + return "Method1"; + } + case TRANSACTION_Method2: + { + return "Method2"; + } + default: + { + return null; + } + } + } + /** @hide */ + public java.lang.String getTransactionName(int transactionCode) + { + return this.getDefaultTransactionName(transactionCode); + } + @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException + { + java.lang.String descriptor = DESCRIPTOR; + if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) { + data.enforceInterface(descriptor); + } + switch (code) + { + case INTERFACE_TRANSACTION: + { + reply.writeString(descriptor); + return true; + } + } + switch (code) + { + case TRANSACTION_Method1: + { + this.Method1(); + reply.writeNoException(); + break; + } + case TRANSACTION_Method2: + { + this.Method2(); + reply.writeNoException(); + break; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + return true; + } + private static class Proxy implements android.aidl.tests.permission.IProtectedInterface + { + private android.os.IBinder mRemote; + Proxy(android.os.IBinder remote) + { + mRemote = remote; + } + @Override public android.os.IBinder asBinder() + { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() + { + return DESCRIPTOR; + } + @Override public void Method1() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method1, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void Method2() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(asBinder()); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_Method2, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_Method1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + /** Helper method to enforce permissions for Method1 */ + protected void Method1_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + static final int TRANSACTION_Method2 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + /** Helper method to enforce permissions for Method2 */ + protected void Method2_enforcePermission() throws SecurityException { + android.content.AttributionSource source = new android.content.AttributionSource(getCallingUid(), null, null); + mEnforcer.enforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION, source); + } + /** @hide */ + public int getMaxTransactionId() + { + return 1; + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method1() throws android.os.RemoteException; + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_FINE_LOCATION) + public void Method2() throws android.os.RemoteException; + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + /* Stubs */ + + private val manifestPermissionStub: TestFile = java( + """ + package android.Manifest; + class permission { + public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String INTERNET = "android.permission.INTERNET"; + } + """ + ).indented() + + private val enforcePermissionAnnotationStub: TestFile = java( + """ + package android.annotation; + public @interface EnforcePermission {} + """ + ).indented() + + private val stubs = arrayOf(manifestPermissionStub, enforcePermissionAnnotationStub) +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt new file mode 100644 index 000000000000..10a6e1da91dc --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt @@ -0,0 +1,443 @@ +/* +* Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask + +class EnforcePermissionHelperDetectorTest : LintDetectorTest() { + override fun getDetector() = EnforcePermissionHelperDetector() + override fun getIssues() = listOf( + EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER, + EnforcePermissionHelperDetector.ISSUE_MISUSING_ENFORCE_PERMISSION + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testFirstExpressionIsFunctionCall() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...: + @@ -8 +8 + + test_enforcePermission(); + + + """ + ) + } + + fun testFirstExpressionIsVariableDeclaration() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + String foo = "bar"; + Binder.getCallingUid(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Autofix for src/Foo.java line 5: Replace with test_enforcePermission();...: + @@ -8 +8 + + test_enforcePermission(); + + + """ + ) + } + + fun testMethodIsEmpty() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException {} + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testOkay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testHelperWithoutSuperPrefix_Okay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInterfaceDefaultMethod_notStubAncestor_error() { + lint().files( + java( + """ + public interface IProtected extends android.os.IInterface { + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + default void PermissionProtected() throws android.os.RemoteException { + String foo = "bar"; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/IProtected.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation] + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testInheritance_callSuper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_callHelper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_missingCallInChain_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Bar.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testInheritance_missingCall_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Baz.java:4: Error: Method must start with test_enforcePermission() or super.test(), if applicable [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testRandomClass_notStubAncestor_error() { + lint().files( + java( + """ + public class Foo { + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + void PermissionProtected() throws android.os.RemoteException { + String foo = "bar"; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:2: Error: The class of PermissionProtected does not inherit from an AIDL generated Stub class [MisusingEnforcePermissionAnnotation] + @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + ^ + 1 errors, 0 warnings + """ + ) + } + + companion object { + val stubs = arrayOf(aidlStub, contextStub, binderStub) + } +} + + + diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt new file mode 100644 index 000000000000..6b8e72cf9222 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2022 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +@Suppress("UnstableApiUsage") +class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = SimpleManualPermissionEnforcementDetector() + override fun getIssues(): List<Issue> = listOf( + SimpleManualPermissionEnforcementDetector + .ISSUE_SIMPLE_MANUAL_PERMISSION_ENFORCEMENT + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk() + + fun testClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testClass_orSelfFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.enforceCallingPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testClass_enforcesFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAnonClass() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testConstantEvaluation() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + } + } + """ + ).indented(), + *stubs, + manifestStub + ) + .run() + .expect( + """ + src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 8: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingOrSelfPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf_mixedOrSelf_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.enforceCallingPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testAllOf_mixedEnforces_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo { + private Context mContext; + private ITest itest = new ITest.Stub() { + @Override + public void test() throws android.os.RemoteException { + mContext.enforceCallingOrSelfPermission( + "android.permission.READ_CONTACTS", "foo"); + mContext.checkCallingOrSelfPermission( + "android.permission.WRITE_CONTACTS", "foo"); + } + }; + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:10: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.checkCallingOrSelfPermission( + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 10: Annotate with @EnforcePermission: + @@ -6 +6 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"}) + @@ -8 +9 + - mContext.enforceCallingOrSelfPermission( + - "android.permission.READ_CONTACTS", "foo"); + - mContext.checkCallingOrSelfPermission( + - "android.permission.WRITE_CONTACTS", "foo"); + + test_enforcePermission(); + """ + ) + } + + fun testPrecedingExpressions() { + lint().files( + java( + """ + import android.os.Binder; + import android.test.ITest; + public class Foo extends ITest.Stub { + private mContext Context; + @Override + public void test() throws android.os.RemoteException { + long uid = Binder.getCallingUid(); + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testPermissionHelper() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + + test_enforcePermission(); + """ + ) + } + + fun testPermissionHelper_orSelfNotBubbledUp_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helper(); + ~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 14: Annotate with @EnforcePermission: + @@ -12 +12 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -14 +15 + - helper(); + + test_enforcePermission(); + """ + ) + } + + fun testPermissionHelperAllOf() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helper() { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helper(); + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:16: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + mContext.enforceCallingOrSelfPermission("FOO", "foo"); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 16: Annotate with @EnforcePermission: + @@ -13 +13 + + @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"}) + @@ -15 +16 + - helper(); + - mContext.enforceCallingOrSelfPermission("FOO", "foo"); + + test_enforcePermission(); + """ + ) + } + + + fun testPermissionHelperNested() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(orSelf = true) + private void helperHelper() { + helper("android.permission.WRITE_CONTACTS"); + } + + @android.annotation.PermissionMethod(orSelf = true) + private void helper(@android.annotation.PermissionName String extraPermission) { + mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); + } + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:19: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 19: Annotate with @EnforcePermission: + @@ -17 +17 + + @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"}) + @@ -19 +20 + - helperHelper(); + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression_orSelfFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + + test_enforcePermission(); + """ + ) + } + + fun testIfExpression_otherSideEffect_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + doSomethingElse(); + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testIfExpression_inlinedWithSideEffect_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (somethingElse() && mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + + private boolean somethingElse() { + return true; + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOf_hardCodedAndVarArgs() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(anyOf = true) + private void helperHelper() { + helper("FOO", "BAR"); + } + + @android.annotation.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"}) + private void helper(@android.annotation.PermissionName String... extraPermissions) {} + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 17: Annotate with @EnforcePermission: + @@ -15 +15 + + @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"}) + @@ -17 +18 + - helperHelper(); + + test_enforcePermission(); + """ + ) + } + + + fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod + private void allOfhelper() { + mContext.enforceCallingOrSelfPermission("FOO"); + mContext.enforceCallingOrSelfPermission("BAR"); + } + + @android.annotation.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"}) + private void anyOfHelper() {} + + @Override + public void test() throws android.os.RemoteException { + allOfhelper(); + anyOfHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOfAllOf_mixedNestedCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.annotation.PermissionName; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.annotation.PermissionMethod(anyOf = true) + private void anyOfCheck(@PermissionName String... permissions) { + allOfCheck("BAZ", "BUZZ"); + } + + @android.annotation.PermissionMethod + private void allOfCheck(@PermissionName String... permissions) {} + + @Override + public void test() throws android.os.RemoteException { + anyOfCheck("FOO", "BAR"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + companion object { + val stubs = arrayOf( + aidlStub, + contextStub, + binderStub, + permissionMethodStub, + permissionNameStub + ) + } +} diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt new file mode 100644 index 000000000000..2ec8fddbb4e9 --- /dev/null +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -0,0 +1,88 @@ +package com.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java +import com.android.tools.lint.checks.infrastructure.TestFile + +val aidlStub: TestFile = java( + """ + package android.test; + public interface ITest extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements android.test.ITest { + protected void test_enforcePermission() throws SecurityException {} + } + public void test() throws android.os.RemoteException; + } + """ +).indented() + +val contextStub: TestFile = java( + """ + package android.content; + public class Context { + @android.annotation.PermissionMethod(orSelf = true) + public void enforceCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod + public void enforceCallingPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod(orSelf = true) + public int checkCallingOrSelfPermission(@android.annotation.PermissionName String permission, String message) {} + @android.annotation.PermissionMethod + public int checkCallingPermission(@android.annotation.PermissionName String permission, String message) {} + } + """ +).indented() + +val binderStub: TestFile = java( + """ + package android.os; + public class Binder { + public static int getCallingUid() {} + } + """ +).indented() + +val permissionMethodStub: TestFile = java( + """ + package android.annotation; + + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({METHOD}) + public @interface PermissionMethod {} + """ +).indented() + +val permissionNameStub: TestFile = java( + """ + package android.annotation; + + import static java.lang.annotation.ElementType.FIELD; + import static java.lang.annotation.ElementType.LOCAL_VARIABLE; + import static java.lang.annotation.ElementType.METHOD; + import static java.lang.annotation.ElementType.PARAMETER; + import static java.lang.annotation.RetentionPolicy.CLASS; + + import java.lang.annotation.Retention; + import java.lang.annotation.Target; + + @Retention(CLASS) + @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) + public @interface PermissionName {} + """ +).indented() + +val manifestStub: TestFile = java( + """ + package android; + + public final class Manifest { + public static final class permission { + public static final String READ_CONTACTS="android.permission.READ_CONTACTS"; + } + } + """.trimIndent() +)
\ No newline at end of file diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp index a0cc446cd42d..954b816a52bf 100644 --- a/tools/locked_region_code_injection/Android.bp +++ b/tools/locked_region_code_injection/Android.bp @@ -19,3 +19,20 @@ java_binary_host { "ow2-asm-tree", ], } + +java_library_host { + name: "lockedregioncodeinjection_input", + manifest: "test/manifest.txt", + srcs: ["test/*/*.java"], + static_libs: [ + "guava", + "ow2-asm", + "ow2-asm-analysis", + "ow2-asm-commons", + "ow2-asm-tree", + "hamcrest-library", + "hamcrest", + "platform-test-annotations", + "junit", + ], +} diff --git a/tools/locked_region_code_injection/OWNERS b/tools/locked_region_code_injection/OWNERS new file mode 100644 index 000000000000..bd43f1736ca5 --- /dev/null +++ b/tools/locked_region_code_injection/OWNERS @@ -0,0 +1,4 @@ +# Everyone in frameworks/base is included by default +shayba@google.com +shombert@google.com +timmurray@google.com diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java index 81a077324e6c..2067bb4ef2fe 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockFindingClassVisitor.java @@ -13,37 +13,51 @@ */ package lockedregioncodeinjection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.TryCatchBlockSorter; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.TryCatchBlockNode; import org.objectweb.asm.tree.analysis.Analyzer; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.Frame; -import static com.google.common.base.Preconditions.checkElementIndex; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; /** - * This visitor does two things: + * This visitor operates on two kinds of targets. For a legacy target, it does the following: * - * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and insert the corresponding pre + * 1. Finds all the MONITOR_ENTER / MONITOR_EXIT in the byte code and inserts the corresponding pre * and post methods calls should it matches one of the given target type in the Configuration. * * 2. Find all methods that are synchronized and insert pre method calls in the beginning and post * method calls just before all return instructions. + * + * For a scoped target, it does the following: + * + * 1. Finds all the MONITOR_ENTER instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the pre method is invoked ON THE TARGET immediately after + * MONITOR_ENTER opcode completes. + * + * 2. Finds all the MONITOR_EXIT instructions in the byte code. If the target of the opcode is + * named in a --scope switch, then the post method is invoked ON THE TARGET immediately before + * MONITOR_EXIT opcode completes. */ class LockFindingClassVisitor extends ClassVisitor { private String className = null; @@ -73,12 +87,16 @@ class LockFindingClassVisitor extends ClassVisitor { class LockFindingMethodVisitor extends MethodVisitor { private String owner; private MethodVisitor chain; + private final String className; + private final String methodName; public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) { super(Utils.ASM_VERSION, mn); assert owner != null; this.owner = owner; this.chain = chain; + className = owner; + methodName = mn.name; } @SuppressWarnings("unchecked") @@ -93,6 +111,12 @@ class LockFindingClassVisitor extends ClassVisitor { for (LockTarget t : targets) { if (t.getTargetDesc().equals("L" + owner + ";")) { ownerMonitor = t; + if (ownerMonitor.getScoped()) { + final String emsg = String.format( + "scoped targets do not support synchronized methods in %s.%s()", + className, methodName); + throw new RuntimeException(emsg); + } } } } @@ -118,9 +142,11 @@ class LockFindingClassVisitor extends ClassVisitor { AbstractInsnNode s = instructions.getFirst(); MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, 0, call); } + boolean anyDup = false; + for (int i = 0; i < instructions.size(); i++) { AbstractInsnNode s = instructions.get(i); @@ -131,9 +157,15 @@ class LockFindingClassVisitor extends ClassVisitor { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPreOwner(), target.getPreMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call); + MethodInsnNode call = methodCall(target, true); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeAcquire(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + i += insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + } } } } @@ -144,8 +176,9 @@ class LockFindingClassVisitor extends ClassVisitor { if (operand instanceof LockTargetState) { LockTargetState state = (LockTargetState) operand; for (int j = 0; j < state.getTargets().size(); j++) { - // The instruction after a monitor_exit should be a label for the end of the implicit - // catch block that surrounds the synchronized block to call monitor_exit when an exception + // The instruction after a monitor_exit should be a label for + // the end of the implicit catch block that surrounds the + // synchronized block to call monitor_exit when an exception // occurs. checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL, "Expected to find label after monitor exit"); @@ -161,9 +194,16 @@ class LockFindingClassVisitor extends ClassVisitor { "Expected label to be the end of monitor exit's try block"); LockTarget target = state.getTargets().get(j); - MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, - target.getPostOwner(), target.getPostMethod(), "()V", false); - insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call); + MethodInsnNode call = methodCall(target, false); + if (target.getScoped()) { + TypeInsnNode cast = typeCast(target); + i += insertInvokeRelease(mn, frameMap, handlersMap, s, i, + call, cast); + anyDup = true; + } else { + insertMethodCallAfter(mn, frameMap, handlersMap, label, + labelIndex, call); + } } } } @@ -174,16 +214,116 @@ class LockFindingClassVisitor extends ClassVisitor { MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(), ownerMonitor.getPostMethod(), "()V", false); - insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call); + insertMethodCallBeforeSync(mn, frameMap, handlersMap, s, i, call); i++; // Skip ahead. Otherwise, we will revisit this instruction again. } } + + if (anyDup) { + mn.maxStack++; + } + super.visitEnd(); mn.accept(chain); } + + // Insert a call to a monitor pre handler. The node and the index identify the + // monitorenter call itself. Insert DUP immediately prior to the MONITORENTER. + // Insert the typecast and call (in that order) after the MONITORENTER. + public int insertInvokeAcquire(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + // Insert a DUP right before MONITORENTER, to capture the object being locked. + // Note that the object will be typed as java.lang.Object. + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + // Insert the call right after the MONITORENTER. These entries are pushed after + // MONITORENTER so they are inserted in reverse order. MONITORENTER should be + // the target of a try/catch block, which means it must be immediately + // followed by a label (which is part of the try/catch block definition). + // Move forward past the label so the invocation in inside the proper block. + // Throw an error if the next instruction is not a label. + node = node.getNext(); + if (!(node instanceof LabelNode)) { + throw new RuntimeException(String.format("invalid bytecode sequence in %s.%s()", + className, methodName)); + } + node = node.getNext(); + index = instructions.indexOf(node); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + + // Insert instructions completely before the current opcode. This is slightly + // different from insertMethodCallBefore(), which inserts the call before MONITOREXIT + // but inserts the start and end labels after MONITOREXIT. + public int insertInvokeRelease(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call, TypeInsnNode cast) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, new InsnNode(Opcodes.DUP)); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, cast); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 3; + } + } + + public static MethodInsnNode methodCall(LockTarget target, boolean pre) { + String spec = "()V"; + if (!target.getScoped()) { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKESTATIC, target.getPostOwner(), target.getPostMethod(), spec); + } + } else { + if (pre) { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPreOwner(), target.getPreMethod(), spec); + } else { + return new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, target.getPostOwner(), target.getPostMethod(), spec); + } + } } - public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + public static TypeInsnNode typeCast(LockTarget target) { + if (!target.getScoped()) { + return null; + } else { + // preOwner and postOwner return the same string for scoped targets. + return new TypeInsnNode(Opcodes.CHECKCAST, target.getPreOwner()); + } + } + + /** + * Insert a method call before the beginning or end of a synchronized method. + */ + public static void insertMethodCallBeforeSync(MethodNode mn, List<Frame> frameMap, List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, MethodInsnNode call) { List<TryCatchBlockNode> handlers = handlersMap.get(index); @@ -226,6 +366,22 @@ class LockFindingClassVisitor extends ClassVisitor { updateCatchHandler(mn, handlers, start, end, handlersMap); } + // Insert instructions completely before the current opcode. This is slightly different from + // insertMethodCallBeforeSync(), which inserts the call before MONITOREXIT but inserts the + // start and end labels after MONITOREXIT. + public int insertMethodCallBefore(MethodNode mn, List<Frame> frameMap, + List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index, + MethodInsnNode call) { + InsnList instructions = mn.instructions; + + instructions.insertBefore(node, call); + frameMap.add(index, frameMap.get(index)); + handlersMap.add(index, handlersMap.get(index)); + + return 1; + } + + @SuppressWarnings("unchecked") public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers, LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) { diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java index c5e59e3dc64d..5f6240327a7f 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTarget.java @@ -21,14 +21,28 @@ package lockedregioncodeinjection; public class LockTarget { public static final LockTarget NO_TARGET = new LockTarget("", null, null); + // The lock which must be instrumented, in Java internal form (L<path>;). private final String targetDesc; + // The methods to be called when the lock is taken (released). For non-scoped locks, + // these are fully qualified static methods. For scoped locks, these are the + // unqualified names of a member method of the target lock. private final String pre; private final String post; + // If true, the pre and post methods are virtual on the target class. The pre and post methods + // are both called while the lock is held. If this field is false then the pre and post methods + // take no parameters and the post method is called after the lock is released. This is legacy + // behavior. + private final boolean scoped; - public LockTarget(String targetDesc, String pre, String post) { + public LockTarget(String targetDesc, String pre, String post, boolean scoped) { this.targetDesc = targetDesc; this.pre = pre; this.post = post; + this.scoped = scoped; + } + + public LockTarget(String targetDesc, String pre, String post) { + this(targetDesc, pre, post, false); } public String getTargetDesc() { @@ -40,7 +54,11 @@ public class LockTarget { } public String getPreOwner() { - return pre.substring(0, pre.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return pre.substring(0, pre.lastIndexOf('.')); + } } public String getPreMethod() { @@ -52,10 +70,23 @@ public class LockTarget { } public String getPostOwner() { - return post.substring(0, post.lastIndexOf('.')); + if (scoped) { + return targetDesc.substring(1, targetDesc.length() - 1); + } else { + return post.substring(0, post.lastIndexOf('.')); + } } public String getPostMethod() { return post.substring(post.lastIndexOf('.') + 1); } + + public boolean getScoped() { + return scoped; + } + + @Override + public String toString() { + return targetDesc + ":" + pre + ":" + post; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java index 99d841829132..5df0160abd8c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/LockTargetState.java @@ -13,10 +13,11 @@ */ package lockedregioncodeinjection; -import java.util.List; import org.objectweb.asm.Type; import org.objectweb.asm.tree.analysis.BasicValue; +import java.util.List; + public class LockTargetState extends BasicValue { private final List<LockTarget> lockTargets; @@ -31,4 +32,10 @@ public class LockTargetState extends BasicValue { public List<LockTarget> getTargets() { return lockTargets; } + + @Override + public String toString() { + return "LockTargetState(" + getType().getDescriptor() + + ", " + lockTargets.size() + ")"; + } } diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java index 828cce72dda9..d22ea2338ff7 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Main.java @@ -21,7 +21,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collections; +import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; @@ -36,6 +36,7 @@ public class Main { String legacyTargets = null; String legacyPreMethods = null; String legacyPostMethods = null; + List<LockTarget> targets = new ArrayList<>(); for (int i = 0; i < args.length; i++) { if ("-i".equals(args[i].trim())) { i++; @@ -52,23 +53,25 @@ public class Main { } else if ("--post".equals(args[i].trim())) { i++; legacyPostMethods = args[i].trim(); + } else if ("--scoped".equals(args[i].trim())) { + i++; + targets.add(Utils.getScopedTarget(args[i].trim())); } - } - // TODO(acleung): Better help message than asserts. - assert inJar != null; - assert outJar != null; + if (inJar == null) { + throw new RuntimeException("missing input jar path"); + } + if (outJar == null) { + throw new RuntimeException("missing output jar path"); + } assert legacyTargets == null || (legacyPreMethods != null && legacyPostMethods != null); ZipFile zipSrc = new ZipFile(inJar); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar)); - List<LockTarget> targets = null; if (legacyTargets != null) { - targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, - legacyPostMethods); - } else { - targets = Collections.emptyList(); + targets.addAll(Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods, + legacyPostMethods)); } Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries(); diff --git a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java index b44e8b42f052..bfef10611e6c 100644 --- a/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java +++ b/tools/locked_region_code_injection/src/lockedregioncodeinjection/Utils.java @@ -44,4 +44,27 @@ public class Utils { return config; } + + /** + * Returns a single {@link LockTarget} from a string. The target is a comma-separated list of + * the target class, the request method, the release method, and a boolean which is true if this + * is a scoped target and false if this is a legacy target. The boolean is optional and + * defaults to true. + */ + public static LockTarget getScopedTarget(String arg) { + String[] c = arg.split(","); + if (c.length == 3) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c.length == 4) { + if (c[3].equals("true")) { + return new LockTarget(c[0], c[1], c[2], true); + } else if (c[3].equals("false")) { + return new LockTarget(c[0], c[1], c[2], false); + } else { + System.err.println("illegal target parameter \"" + c[3] + "\""); + } + } + // Fall through + throw new RuntimeException("invalid scoped target format"); + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java index 31fa0bf63416..28f00b9c897a 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestMain.java @@ -17,7 +17,10 @@ import org.junit.Assert; import org.junit.Test; /** - * To run the unit tests: + * To run the unit tests, first build the two necessary artifacts. Do this explicitly as they are + * not generally retained by a normal "build all". After lunching a target: + * m lockedregioncodeinjection + * m lockedregioncodeinjection_input * * <pre> * <code> @@ -29,31 +32,25 @@ import org.junit.Test; * mkdir -p out * rm -fr out/* * - * # Make booster - * javac -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar src/*/*.java -d out/ - * pushd out - * jar cfe lockedregioncodeinjection.jar lockedregioncodeinjection.Main */*.class - * popd - * - * # Make unit tests. - * javac -cp lib/junit-4.12.jar test/*/*.java -d out/ - * - * pushd out - * jar cfe test_input.jar lockedregioncodeinjection.Test */*.class - * popd + * # Paths to the build artifacts. These assume linux-x86; YMMV. + * ROOT=$TOP/out/host/linux-x86 + * EXE=$ROOT/bin/lockedregioncodeinjection + * INPUT=$ROOT/frameworkd/lockedregioncodeinjection_input.jar * * # Run tool on unit tests. - * java -ea -cp lib/asm-7.0_BETA.jar:lib/asm-commons-7.0_BETA.jar:lib/asm-tree-7.0_BETA.jar:lib/asm-analysis-7.0_BETA.jar:lib/guava-21.0.jar:out/lockedregioncodeinjection.jar \ - * lockedregioncodeinjection.Main \ - * -i out/test_input.jar -o out/test_output.jar \ + * $EXE -i $INPUT -o out/test_output.jar \ * --targets 'Llockedregioncodeinjection/TestTarget;' \ * --pre 'lockedregioncodeinjection/TestTarget.boost' \ * --post 'lockedregioncodeinjection/TestTarget.unboost' * * # Run unit tests. - * java -ea -cp lib/hamcrest-core-1.3.jar:lib/junit-4.12.jar:out/test_output.jar \ + * java -ea -cp out/test_output.jar \ * org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain * </code> + * OR + * <code> + * bash test/unit-test.sh + * </code> * </pre> */ public class TestMain { @@ -64,7 +61,9 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); - Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.invokeCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); synchronized (t) { Assert.assertEquals(TestTarget.boostCount, 1); @@ -75,6 +74,8 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -84,12 +85,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCall(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -99,12 +104,16 @@ public class TestMain { Assert.assertEquals(TestTarget.boostCount, 0); Assert.assertEquals(TestTarget.unboostCount, 0); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); t.synchronizedCallReturnInt(); Assert.assertEquals(TestTarget.boostCount, 1); Assert.assertEquals(TestTarget.unboostCount, 1); Assert.assertEquals(TestTarget.invokeCount, 1); + Assert.assertEquals(TestTarget.boostCountLocked, 0); + Assert.assertEquals(TestTarget.unboostCountLocked, 0); } @Test @@ -253,4 +262,125 @@ public class TestMain { Assert.assertEquals(TestTarget.invokeCount, 1); } + @Test + public void testScopedTarget() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + Assert.assertEquals(1, target.scopedLock().mLevel); + } + Assert.assertEquals(0, target.scopedLock().mLevel); + + synchronized (target.scopedLock()) { + synchronized (target.scopedLock()) { + Assert.assertEquals(2, target.scopedLock().mLevel); + } + } + Assert.assertEquals(0, target.scopedLock().mLevel); + } + + @Test + public void testScopedExceptionHandling() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(0, target.scopedLock().mLevel); + + boolean handled; + + // 1: an exception inside the block properly releases the lock. + handled = false; + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + throw new RuntimeException(); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 2: An exception inside the monitor enter function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + // The exception was thrown inside monitorEnter(), so the code should + // never reach this point. + Assert.assertEquals(0, 1); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + + // 3: An exception inside the monitor exit function + handled = false; + target.throwOnEnter(true); + try { + synchronized (target.scopedLock()) { + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + Assert.assertEquals(1, target.scopedLock().mLevel); + } + } catch (RuntimeException e) { + Assert.assertEquals(0, target.scopedLock().mLevel); + handled = true; + } + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, handled); + // Just verify that the lock can still be taken + Assert.assertEquals(false, Thread.holdsLock(target.scopedLock())); + } + + // Provide an in-class type conversion for the scoped target. + private Object untypedLock(TestScopedTarget target) { + return target.scopedLock(); + } + + @Test + public void testScopedLockTyping() { + TestScopedTarget target = new TestScopedTarget(); + Assert.assertEquals(target.scopedLock().mLevel, 0); + + // Scoped lock injection works on the static type of an object. In general, it is + // a very bad idea to do type conversion on scoped locks, but the general rule is + // that conversions within a single method are recognized by the lock injection + // tool and injection occurs. Conversions outside a single method are not + // recognized and injection does not occur. + + // 1. Conversion occurs outside the class. The visible type of the lock is Object + // in this block, so no injection takes place on 'untypedLock', even though the + // dynamic type is TestScopedLock. + synchronized (target.untypedLock()) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 2. Conversion occurs inside the class but in another method. The visible type + // of the lock is Object in this block, so no injection takes place on + // 'untypedLock', even though the dynamic type is TestScopedLock. + synchronized (untypedLock(target)) { + Assert.assertEquals(0, target.scopedLock().mLevel); + Assert.assertEquals(true, target.untypedLock() instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + + // 3. Conversion occurs inside the method. The compiler can determine the type of + // the lock within a single function, so injection does take place here. + Object untypedLock = target.scopedLock(); + synchronized (untypedLock) { + Assert.assertEquals(1, target.scopedLock().mLevel); + Assert.assertEquals(true, untypedLock instanceof TestScopedLock); + Assert.assertEquals(true, Thread.holdsLock(target.scopedLock())); + } + } } diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java new file mode 100644 index 000000000000..7441d2b1da48 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedLock.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedLock { + public int mEntered = 0; + public int mExited = 0; + + public int mLevel = 0; + private final TestScopedTarget mTarget; + + TestScopedLock(TestScopedTarget target) { + mTarget = target; + } + + void monitorEnter() { + mLevel++; + mEntered++; + mTarget.enter(mLevel); + } + + void monitorExit() { + mLevel--; + mExited++; + mTarget.exit(mLevel); + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java new file mode 100644 index 000000000000..c80975e9c6b3 --- /dev/null +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestScopedTarget.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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 lockedregioncodeinjection; + +public class TestScopedTarget { + + public final TestScopedLock mLock;; + + private boolean mNextEnterThrows = false; + private boolean mNextExitThrows = false; + + TestScopedTarget() { + mLock = new TestScopedLock(this); + } + + TestScopedLock scopedLock() { + return mLock; + } + + Object untypedLock() { + return mLock; + } + + void enter(int level) { + if (mNextEnterThrows) { + mNextEnterThrows = false; + throw new RuntimeException(); + } + } + + void exit(int level) { + if (mNextExitThrows) { + mNextExitThrows = false; + throw new RuntimeException(); + } + } + + void throwOnEnter(boolean b) { + mNextEnterThrows = b; + } + + void throwOnExit(boolean b) { + mNextExitThrows = b; + } +} diff --git a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java index d1c8f340a598..e3ba6a77e3b7 100644 --- a/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java +++ b/tools/locked_region_code_injection/test/lockedregioncodeinjection/TestTarget.java @@ -19,8 +19,17 @@ public class TestTarget { public static int invokeCount = 0; public static boolean nextUnboostThrows = false; + // If this is not null, then this is the lock under test. The lock must not be held when boost() + // or unboost() are called. + public static Object mLock = null; + public static int boostCountLocked = 0; + public static int unboostCountLocked = 0; + public static void boost() { boostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + boostCountLocked++; + } } public static void unboost() { @@ -29,6 +38,9 @@ public class TestTarget { throw new RuntimeException(); } unboostCount++; + if (mLock != null && Thread.currentThread().holdsLock(mLock)) { + unboostCountLocked++; + } } public static void invoke() { diff --git a/tools/locked_region_code_injection/test/manifest.txt b/tools/locked_region_code_injection/test/manifest.txt new file mode 100644 index 000000000000..2314c188721c --- /dev/null +++ b/tools/locked_region_code_injection/test/manifest.txt @@ -0,0 +1 @@ +Main-Class: org.junit.runner.JUnitCore diff --git a/tools/locked_region_code_injection/test/unit-test.sh b/tools/locked_region_code_injection/test/unit-test.sh new file mode 100755 index 000000000000..9fa6f39af14a --- /dev/null +++ b/tools/locked_region_code_injection/test/unit-test.sh @@ -0,0 +1,98 @@ +#! /bin/bash +# + +# Copyright (C) 2023 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. + +# This script runs the tests for the lockedregioninjectioncode. See +# TestMain.java for the invocation. The script expects that a full build has +# already been done and artifacts are in $TOP/out. + +# Compute the default top of the workspace. The following code copies the +# strategy of croot. (croot cannot be usd directly because it is a function and +# functions are not carried over into subshells.) This gives the correct answer +# if run from inside a workspace. If run from outside a workspace, supply TOP +# on the command line. +TOPFILE=build/make/core/envsetup.mk +TOP=$(dirname $(realpath $0)) +while [[ ! $TOP = / && ! -f $TOP/$TOPFILE ]]; do + TOP=$(dirname $TOP) +done +# TOP is "/" if this script is located outside a workspace. + +# If the user supplied a top directory, use it instead +if [[ -n $1 ]]; then + TOP=$1 + shift +fi +if [[ -z $TOP || $TOP = / ]]; then + echo "usage: $0 <workspace-root>" + exit 1 +elif [[ ! -d $TOP ]]; then + echo "$TOP is not a directory" + exit 1 +elif [[ ! -d $TOP/prebuilts/misc/common ]]; then + echo "$TOP does not look like w workspace" + exit 1 +fi +echo "Using workspace $TOP" + +# Pick up the current java compiler. The lunch target is not very important, +# since most, if not all, will use the same host binaries. +pushd $TOP > /dev/null +. build/envsetup.sh > /dev/null 2>&1 +lunch redfin-userdebug > /dev/null 2>&1 +popd > /dev/null + +# Bail on any error +set -o pipefail +trap 'exit 1' ERR + +# Create the two sources +pushd $TOP > /dev/null +m lockedregioncodeinjection +m lockedregioncodeinjection_input +popd > /dev/null + +# Create a temporary directory outside of the workspace. +OUT=$TOP/out/host/test/lockedregioncodeinjection +echo + +# Clean the directory +if [[ -d $OUT ]]; then rm -r $OUT; fi +mkdir -p $OUT + +ROOT=$TOP/out/host/linux-x86 +EXE=$ROOT/bin/lockedregioncodeinjection +INP=$ROOT/framework/lockedregioncodeinjection_input.jar + +# Run tool on unit tests. +$EXE \ + -i $INP -o $OUT/test_output.jar \ + --targets 'Llockedregioncodeinjection/TestTarget;' \ + --pre 'lockedregioncodeinjection/TestTarget.boost' \ + --post 'lockedregioncodeinjection/TestTarget.unboost' \ + --scoped 'Llockedregioncodeinjection/TestScopedLock;,monitorEnter,monitorExit' + +# Run unit tests. +java -ea -cp $OUT/test_output.jar \ + org.junit.runner.JUnitCore lockedregioncodeinjection.TestMain + +# Extract the class files and decompile them for possible post-analysis. +pushd $OUT > /dev/null +jar -x --file test_output.jar lockedregioncodeinjection +for class in lockedregioncodeinjection/*.class; do + javap -c -v $class > ${class%.class}.asm +done +popd > /dev/null + +echo "artifacts are in $OUT" |