Merge "Include module dependencies under `data`"
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 6b3278f..4f5b620 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 @@
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 34c9c4d..404bcac 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 @@
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 0000000..e898091
--- /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 0000000..04092366
--- /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 6897a02..045af02 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 @@
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 6dfa7810..483b2f17 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 @@
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 a48ce0c..0ba24d1 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 60afdc7..c6fd0bf 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 @@
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 4cb52bc..9d20eca 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -41,23 +41,6 @@
}
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 9876abb..09c2383 100644
--- a/api/api.go
+++ b/api/api.go
@@ -194,55 +194,6 @@
}
}
-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 @@
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 9afd95a..0000000
--- 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 d2e5b7d..0000000
--- 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 1efdf77..6b6bc97 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -460,6 +460,7 @@
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 a8552f8..4c1594b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18727,8 +18727,8 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 1fa1e89..8b3696a 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -252,6 +252,22 @@
}
+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 9882a4f..821944f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9112,9 +9112,7 @@
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 @@
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 @@
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 2c5acf1..1c10356 100644
--- a/core/api/system-removed.txt
+++ b/core/api/system-removed.txt
@@ -140,6 +140,17 @@
}
+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 7cfb66a..6a5ef3a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -141,8 +141,9 @@
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 eadf758..d328b34 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -643,9 +643,17 @@
@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 5599893..5ba7a4c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -238,6 +238,7 @@
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 @@
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 bccbb38..2a854b2 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -37,8 +37,6 @@
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 @@
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 @@
"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 @@
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 @@
@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 @@
*/
@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 @@
* {@link #setDeliveryGroupMatchingKey(String, String)}.
*/
public void clearDeliveryGroupMatchingKey() {
- mDeliveryGroupMatchingKey = null;
+ mDeliveryGroupMatchingNamespaceFragment = null;
+ mDeliveryGroupMatchingKeyFragment = null;
}
/**
@@ -1101,8 +1136,11 @@
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 01b2cb9..1e41886 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 7c2e518..44144d9 100644
--- a/core/java/android/hardware/usb/UsbDeviceConnection.java
+++ b/core/java/android/hardware/usb/UsbDeviceConnection.java
@@ -238,7 +238,7 @@
* 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 @@
* 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 a20191c..74775a8 100644
--- a/core/java/android/net/Ikev2VpnProfile.java
+++ b/core/java/android/net/Ikev2VpnProfile.java
@@ -769,6 +769,19 @@
}
}
+ @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 38b3174..46cf016 100644
--- a/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java
+++ b/core/java/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java
@@ -354,6 +354,7 @@
}
/** @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 6f9c9dd..a27e923 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 @@
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 @@
+ 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 9235d09..edf2c09 100644
--- a/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java
+++ b/core/java/android/net/vcn/VcnUnderlyingNetworkTemplate.java
@@ -29,6 +29,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
import java.util.Objects;
/**
@@ -307,4 +308,7 @@
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 2544a6d..2e6b09f 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.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 java.util.ArrayList;
import java.util.Collections;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -162,6 +167,12 @@
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 6a40f98..0000000
--- 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 133146d..b06bf06 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 @@
*/
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 8a30ef4..a6d8caf 100644
--- a/core/java/android/nfc/INfcAdapter.aidl
+++ b/core/java/android/nfc/INfcAdapter.aidl
@@ -18,7 +18,6 @@
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 @@
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 911aaf3..8d75cac 100644
--- a/core/java/android/nfc/NfcActivityManager.java
+++ b/core/java/android/nfc/NfcActivityManager.java
@@ -19,9 +19,6 @@
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 @@
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 @@
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 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 @@
}
}
- /** 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 656cd99..cacde7f 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -343,8 +343,12 @@
*/
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 @@
// 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 @@
* 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 @@
* 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 @@
/**
- * @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 @@
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 @@
* @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 @@
* @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 @@
* 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,54 +1363,18 @@
* 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);
}
/**
@@ -1498,41 +1414,17 @@
* 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 @@
* @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 @@
*
* @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 @@
* <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 @@
* 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 @@
* @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 @@
}
/**
- * @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 089b159..0ba0c5a 100644
--- a/core/java/android/nfc/tech/IsoDep.java
+++ b/core/java/android/nfc/tech/IsoDep.java
@@ -88,6 +88,7 @@
* <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 @@
* <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 @@
* @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 @@
* 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 080e058..26f54e6 100644
--- a/core/java/android/nfc/tech/MifareClassic.java
+++ b/core/java/android/nfc/tech/MifareClassic.java
@@ -597,6 +597,7 @@
* <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 @@
* <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 dec2c65..c0416a3 100644
--- a/core/java/android/nfc/tech/MifareUltralight.java
+++ b/core/java/android/nfc/tech/MifareUltralight.java
@@ -236,6 +236,7 @@
* <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 @@
* <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 2256365..bb750cf 100644
--- a/core/java/android/nfc/tech/Ndef.java
+++ b/core/java/android/nfc/tech/Ndef.java
@@ -261,6 +261,7 @@
* @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 @@
* @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 @@
* <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 @@
* @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 4175cd0..f19d302 100644
--- a/core/java/android/nfc/tech/NdefFormatable.java
+++ b/core/java/android/nfc/tech/NdefFormatable.java
@@ -111,6 +111,7 @@
* @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 88730f9..7e66483 100644
--- a/core/java/android/nfc/tech/NfcA.java
+++ b/core/java/android/nfc/tech/NfcA.java
@@ -141,6 +141,7 @@
* <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 @@
* <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 4487121..2ccd388 100644
--- a/core/java/android/nfc/tech/NfcF.java
+++ b/core/java/android/nfc/tech/NfcF.java
@@ -145,6 +145,7 @@
* <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 @@
* <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 0e2c7c1..839fe42 100644
--- a/core/java/android/nfc/tech/TagTechnology.java
+++ b/core/java/android/nfc/tech/TagTechnology.java
@@ -176,6 +176,7 @@
* @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 @@
* @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 @@
* <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 735a068..244632a 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -806,12 +806,9 @@
* 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 221e89a..310ceb3 100644
--- a/core/java/android/os/PermissionEnforcer.java
+++ b/core/java/android/os/PermissionEnforcer.java
@@ -18,9 +18,11 @@
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 @@
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 @@
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 @@
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 @@
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 878e141..970f419 100644
--- a/core/java/android/os/RemoteException.java
+++ b/core/java/android/os/RemoteException.java
@@ -21,6 +21,12 @@
/**
* 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 3c1b4ba..0012572 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -8410,7 +8410,7 @@
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 @@
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 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 @@
* @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 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 85e3aee..fe0c1dd 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1596,7 +1596,6 @@
* 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 3dc805e..bb76b469 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4842,6 +4842,14 @@
*/
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 @@
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 13f7e5d..3a254c1 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -39,9 +39,13 @@
}
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 6d64022..a3c278f 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -15,6 +15,10 @@
# 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 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 02dafd4..7f0a651 100644
--- a/core/java/android/widget/OWNERS
+++ b/core/java/android/widget/OWNERS
@@ -13,3 +13,5 @@
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 ca57c84..fceee4e 100644
--- a/core/java/android/widget/Toast.java
+++ b/core/java/android/widget/Toast.java
@@ -189,6 +189,9 @@
/**
* 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 0d02683..a1d571f 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 afdbdc8..4a46d91 100644
--- a/core/java/com/android/internal/expresslog/Counter.java
+++ b/core/java/com/android/internal/expresslog/Counter.java
@@ -36,6 +36,16 @@
}
/**
+ * 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 @@
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 2f3b662..2fe784a 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 @@
/**
* 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 @@
/**
* Logs increment sample count for automatically calculated bin
+ *
* @param sample value
* @hide
*/
@@ -48,10 +54,24 @@
/*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 @@
* 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 @@
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 @@
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 @@
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 1505ccc..85cb15b 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -721,7 +721,8 @@
} 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 0000000..7755000
--- /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 1808bd5..9be8ea7 100644
--- a/core/java/com/android/internal/util/OWNERS
+++ b/core/java/com/android/internal/util/OWNERS
@@ -6,3 +6,4 @@
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 c99a27a..1e7a93c 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -5,6 +5,9 @@
# 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 a7c7d0b..b24dc8a 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -413,9 +413,13 @@
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 @@
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 57a196f..a5d979c 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -2296,7 +2296,9 @@
// 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 2df1d5d..1dcd5cc 100644
--- a/core/proto/android/nfc/nfc_service.proto
+++ b/core/proto/android/nfc/nfc_service.proto
@@ -60,7 +60,7 @@
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 @@
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 a5e9617..277bdf3 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 94cf1b2..504492b 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 d832745..dda11fb 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 0000000..baf92ec
--- /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 0000000..ee62d75
--- /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 9fa6d06..037dbb3 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.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 @@
}
@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 @@
}
@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 @@
}
@Test
- @SmallTest
public void testBinIndexForRangeEqual5() {
Histogram.UniformOptions options = new Histogram.UniformOptions(2, 0, 10);
assertEquals(4, options.getBinsCount());
@@ -87,7 +81,6 @@
}
@Test
- @SmallTest
public void testBinIndexForRangeEqual10() {
Histogram.UniformOptions options = new Histogram.UniformOptions(10, 1, 101);
assertEquals(0, options.getBinForSample(0));
@@ -101,7 +94,6 @@
}
@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 c877e89..c9c1af8 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 c2cd6ff..74597c5 100644
--- a/keystore/java/android/security/KeyStore2.java
+++ b/keystore/java/android/security/KeyStore2.java
@@ -161,6 +161,15 @@
}
/**
+ * 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 @@
});
}
+ /**
+ * 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 91f216f..045e318 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -79,13 +79,11 @@
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 @@
}
}
- 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 @@
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 @@
+ "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 6fa1a69..372e4cb 100644
--- a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java
+++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java
@@ -40,7 +40,6 @@
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 @@
abstract class KeyStoreCryptoOperationUtils {
private static volatile SecureRandom sRng;
- private static final Random sRandom = new Random();
private KeyStoreCryptoOperationUtils() {}
@@ -213,7 +211,7 @@
} 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 f96c39c8..1e1f68a 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.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 @@
@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 5a67eb9..738f1ab 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -98,7 +98,7 @@
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 5f50137..4a6648e 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 01b2cb9..1e41886 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 22033c6..6b29fc3 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -72,7 +72,7 @@
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 @@
*/
//
- // 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 e6d95eb6..cf3ba87 100644
--- a/media/java/android/media/ThumbnailUtils.java
+++ b/media/java/android/media/ThumbnailUtils.java
@@ -49,6 +49,7 @@
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 @@
// 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 01b2cb9..85f7a4d 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 fa04293..0b022e5 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 31cd760..baafe7b 100644
--- a/packages/CtsShim/Android.bp
+++ b/packages/CtsShim/Android.bp
@@ -44,6 +44,9 @@
arm64: {
apk: "apk/arm/CtsShimPriv.apk",
},
+ riscv64: {
+ apk: "apk/riscv64/CtsShimPriv.apk",
+ },
x86: {
apk: "apk/x86/CtsShimPriv.apk",
},
@@ -82,6 +85,9 @@
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
index fb09286..af306a5 100644
--- a/packages/CtsShim/apk/arm/CtsShim.apk
+++ b/packages/CtsShim/apk/arm/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/arm/CtsShimPriv.apk b/packages/CtsShim/apk/arm/CtsShimPriv.apk
index 07915ce..98c5351 100644
--- a/packages/CtsShim/apk/arm/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/arm/CtsShimPriv.apk
Binary files differ
diff --git a/packages/CtsShim/apk/riscv64/CtsShim.apk b/packages/CtsShim/apk/riscv64/CtsShim.apk
new file mode 100644
index 0000000..af306a5
--- /dev/null
+++ b/packages/CtsShim/apk/riscv64/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/riscv64/CtsShimPriv.apk b/packages/CtsShim/apk/riscv64/CtsShimPriv.apk
new file mode 100644
index 0000000..9a9997d
--- /dev/null
+++ b/packages/CtsShim/apk/riscv64/CtsShimPriv.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShim.apk b/packages/CtsShim/apk/x86/CtsShim.apk
index fb09286..af306a5 100644
--- a/packages/CtsShim/apk/x86/CtsShim.apk
+++ b/packages/CtsShim/apk/x86/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShimPriv.apk b/packages/CtsShim/apk/x86/CtsShimPriv.apk
index 20e94b6..29ad478 100644
--- a/packages/CtsShim/apk/x86/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/x86/CtsShimPriv.apk
Binary files differ
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 6669d6b..868867d 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 f305fd3..e92157e 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 @@
* 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 c61ebc0..0630a2e 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 @@
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));
+ 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: "
+ if (DEBUG) {
+ Log.d(TAG, "Converted qrCodeString result: "
+ " ,Type = " + mSourceAddressType
+ " ,Device = " + mSourceDevice
+ " ,AdSid = " + mSourceAdvertisingSid
@@ -282,11 +284,11 @@
+ " ,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 @@
.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 39b4b8e..35e3dd3 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 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 ab9b5dc..c01528fb 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 ab9b5dc..c01528fb 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 2e6ea0e..54946ee 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 @@
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 1fb6a98..c37b01f 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 @@
) : 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 @@
}
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 59db686..f149fda 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.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 @@
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 ce9c067..2daf04d 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.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 @@
}
}
+ 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 a69d3f0..77a54a5 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1957,7 +1957,8 @@
&& 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 f652cb0..e8c85ce 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -1074,9 +1074,10 @@
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 @@
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 @@
* 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 @@
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 d48723a..99f1863 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 @@
}
}
+ 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 @@
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 d080036..6ce70a1 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 @@
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 94eb076..cd119e7 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 @@
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 @@
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 @@
// 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 @@
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);
- }
- 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;
- }
-
- 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);
- }
- }
-
- // 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!");
+ 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);
+ }
+ 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. 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, 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 abddc43..256df98 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 @@
+ ", " + 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 a74f4154..210fc910 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 @@
"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 @@
@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 593acd6..a7c2ddf 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 @@
// 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 @@
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 @@
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 @@
//-----------------------------------------------------------------
// 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 @@
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 @@
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 6cd42f8..d3b7606 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 @@
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 5cdf58b..394e4af 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 @@
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 @@
* - 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 @@
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 @@
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 @@
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 8e8fd05..c9cdba9 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 @@
};
// 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 @@
.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 @@
// 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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
if (transform.length != 6) {
throw new IllegalArgumentException("invalid array size" + transform.length);
}
- if (!checkSpatForHeadTracking("setGlobalTransform")) {
+ if (!checkSpatializerForHeadTracking("setGlobalTransform")) {
return;
}
try {
@@ -1074,7 +1080,7 @@
}
synchronized void recenterHeadTracker() {
- if (!checkSpatForHeadTracking("recenterHeadTracker")) {
+ if (!checkSpatializerForHeadTracking("recenterHeadTracker")) {
return;
}
try {
@@ -1084,8 +1090,30 @@
}
}
+ 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 @@
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 @@
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 @@
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 @@
}
/*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 @@
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 @@
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 @@
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 @@
/*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 6d3f8fd..b25206d 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.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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
@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 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 @@
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 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 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 @@
// 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 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 @@
// 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 @@
@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 @@
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);
- mCachedKeepalivePerSubId.put(subId, natKeepalive);
- Log.d(TAG, "Get customized keepalive=" + natKeepalive);
- return natKeepalive;
+ 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);
+ }
+
+ 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;
+ }
+
+ 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);
}
}
- boolean maybeMigrateIkeSession(@NonNull Network underlyingNetwork) {
+ 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 @@
+ 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 @@
/** 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 @@
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 @@
Log.d(TAG, "Data stall suspected");
// Trigger MOBIKE.
- maybeMigrateIkeSession(mActiveNetwork);
+ maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork);
mDataStallSuspected = true;
}
}
@@ -4543,7 +4752,7 @@
// 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 @@
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();
+ pw.println("mCachedCarrierConfigInfoPerSubId=" + mCachedCarrierConfigInfoPerSubId);
- pw.println("mUnderlyNetworkChanges (most recent first):");
+ pw.println("mEventChanges (most recent first):");
pw.increaseIndent();
- mUnderlyNetworkChanges.reverseDump(pw);
- pw.decreaseIndent();
-
- pw.println("mVpnManagerEvent (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 8b579ac..6cc89b8 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 @@
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 195101d..571afa8 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 @@
mAppOps,
new SysUiStatsEvent.BuilderFactory(),
mShowReviewPermissionsNotification);
- mPreferencesHelper.updateFixedImportance(mUm.getUsers());
mRankingHelper = new RankingHelper(getContext(),
mRankingHandler,
mPreferencesHelper,
@@ -2760,6 +2759,9 @@
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 78dad12..65bd3f1 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 @@
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 @@
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 @@
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 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 3816b07..0de44bc 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 @@
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 e146135..76a714c 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 @@
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 01b2cb9..1e41886 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 0000000..5b04d88
--- /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 2141eba..7f129ea 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 @@
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 @@
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 75d84ea..bb2a2cf 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 @@
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 b9739f03..b64122d 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 @@
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 428eaff..3ad24de 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 @@
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.mockito.Mock;
import org.mockito.Spy;
+import java.util.ArrayList;
import java.util.List;
@MediumTest
@@ -49,14 +55,35 @@
@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());
+ }
- mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem,
- false /*headTrackingEnabledByDefault*/);
+ /**
+ * 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*/);
+
}
/**
@@ -66,6 +93,7 @@
*/
@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 @@
@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 @@
@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 @@
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 33385af..1e41886 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 d46530c..4fd91ba 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 @@
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 ef1061b..40e8d26 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 01b2cb9..1e41886 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 fdf69430..f90eabc 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -18,6 +18,7 @@
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 @@
}
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 c175fc6..17780af 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -4334,6 +4334,22 @@
"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 @@
"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 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 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 @@
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 @@
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 @@
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 1d6798b..f1f13bc 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -184,10 +184,11 @@
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 @@
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 @@
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 @@
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 @@
}
/**
- * @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 searching for service, {@code false} otherwise.
+ * @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 @@
}
/**
- * @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 @@
}
/**
- * @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 @@
.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 @@
@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 @@
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 @@
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 @@
private int mTransportType;
@RegistrationState
- private int mInitialRegistrationState;
+ private int mNetworkRegistrationState;
@NetworkType
private int mAccessNetworkTechnology;
@@ -856,6 +902,31 @@
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 @@
* @return The same instance of the builder.
*/
public @NonNull Builder setRegistrationState(@RegistrationState int registrationState) {
- mInitialRegistrationState = registrationState;
+ mNetworkRegistrationState = registrationState;
return this;
}
@@ -1006,7 +1077,7 @@
*/
@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 ac74016..03e019d 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -631,11 +631,17 @@
}
/**
- * 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 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 @@
}
/**
- * 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 @@
/**
* 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 @@
.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 void setStateOutOfService() {
- init();
+ init(true);
}
public void setStateOff() {
- init();
+ init(true);
mVoiceRegState = STATE_POWER_OFF;
mDataRegState = STATE_POWER_OFF;
}
@@ -1265,11 +1279,14 @@
/**
* 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 d670e55..1cf2969 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -2245,6 +2245,7 @@
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 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 0cfd96e..d590ce2 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1116,6 +1116,14 @@
*/
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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
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 f4e2ade..e01b10e 100644
--- a/telephony/java/android/telephony/TelephonyDisplayInfo.java
+++ b/telephony/java/android/telephony/TelephonyDisplayInfo.java
@@ -87,10 +87,12 @@
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 @@
* @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) {
+ @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 @@
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 @@
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 @@
@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 d23b75b..fd5ec25 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17027,4 +17027,44 @@
}
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 c5f6902..25a714a 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -326,4 +326,34 @@
* @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 9db19a3..a0fcfc5 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 816bc6b..1e41886 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 0000000..b94bb41
--- /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 0000000..3a9129e
--- /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 b313c9f..73a0a61 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 @@
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 075bc5e..4123f80 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.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 @@
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 @@
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 @@
false /* expectRestricted */);
}
- private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) {
+ private void setupTrackedNetwork(NetworkCapabilities caps, LinkProperties lp) {
mVcnMgmtSvc.systemReady();
final ArgumentCaptor<NetworkCallback> captor =
@@ -1269,7 +1278,10 @@
.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 @@
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 @@
getNetworkCapabilitiesBuilderForTransport(
TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI)
.build(),
- new LinkProperties());
+ TEST_LP_1);
assertTrue(policy.isTeardownRequested());
}
@@ -1298,7 +1310,7 @@
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 @@
.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 @@
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 @@
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 629e988..2266041 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 @@
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 @@
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 b534b62..b235ad6 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 @@
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 @@
[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 8f553ab..0000000
--- 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 f5f4ebe..0000000
--- 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 0000000..898f88b
--- /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 0000000..0ef165f
--- /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 0000000..9a7f8fa
--- /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
similarity index 67%
rename from core/java/android/nfc/BeamShareData.aidl
rename to tools/lint/common/src/main/java/com/google/android/lint/model/Method.kt
index a47e240..3939b61 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 0000000..43f2122
--- /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 0000000..a5ac2be
--- /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 0000000..cd4d778d
--- /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
similarity index 68%
rename from tools/lint/Android.bp
rename to tools/lint/framework/Android.bp
index 2601041..30a6daa 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 @@
"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 @@
],
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 0000000..935bade
--- /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
similarity index 64%
rename from tools/lint/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
rename to tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
index 930378b..0c375c3 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.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 @@
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 @@
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 @@
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 @@
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 @@
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
)
}
+ 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 (token != null) {
+ context.report(
+ ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+ context.getLocation(node),
+ getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
+ token.variableName,
+ node.asRenderString()
+ )
+ )
+ }
+ }
+
/**
- * 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) {
- val token = findFirstTokenInScope(node)
- if (isCallerAwareMethod(node) && token != null) {
- context.report(
- 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
+ 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 @@
// 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 @@
): 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 @@
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 @@
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 @@
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 @@
`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 @@
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 @@
/** 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 @@
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 @@
`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 @@
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
similarity index 100%
rename from tools/lint/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
rename to 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 0000000..48540b1d
--- /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 0000000..e12ec3d
--- /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 0000000..06c098d
--- /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 0000000..0826e8e
--- /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 0000000..f928263
--- /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
similarity index 88%
rename from tools/lint/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
rename to tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
index e1a5c61..d90f3e3 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 @@
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 @@
fun testDoesNotDetectIssuesInCorrectScenario() {
lint().files(
- java(
- """
+ java(
+ """
package test.pkg;
import android.os.Binder;
public class TestClass1 extends Binder {
@@ -62,22 +63,29 @@
} 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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 3 warnings
""".addLineContinuation()
- )
+ )
}
/** Nested clearCallingIdentity() calls issue tests */
@@ -302,8 +310,8 @@
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 @@
}
}
"""
- ).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 @@
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
}
}
"""
- ).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 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 @@
).indented()
private val userHandleStub: TestFile = java(
- """
+ """
package android.os;
import android.annotation.AppIdInt;
import android.annotation.UserIdInt;
@@ -792,7 +845,7 @@
).indented()
private val userIdIntStub: TestFile = java(
- """
+ """
package android.annotation;
public @interface UserIdInt {
}
@@ -800,7 +853,7 @@
).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
similarity index 100%
rename from tools/lint/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
rename to 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 0000000..a70644a
--- /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 0000000..e686695
--- /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/Android.bp b/tools/lint/global/Android.bp
similarity index 61%
copy from tools/lint/Android.bp
copy to tools/lint/global/Android.bp
index 2601041..bedb7bd 100644
--- a/tools/lint/Android.bp
+++ b/tools/lint/global/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.
@@ -22,32 +22,44 @@
}
java_library_host {
- name: "AndroidFrameworkLintChecker",
+ 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: "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,
+ name: "AndroidGlobalLintCheckerTest",
srcs: ["checks/src/test/java/**/*.kt"],
static_libs: [
- "AndroidFrameworkLintChecker",
+ "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
similarity index 63%
rename from tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
rename to tools/lint/global/checks/src/main/java/com/google/android/lint/AndroidGlobalIssueRegistry.kt
index a6fd9bb..a20266a 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 @@
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 @@
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 0000000..ab6d871
--- /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 0000000..dcfbe95
--- /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 0000000..0baac2c
--- /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 0000000..25d208d
--- /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 0000000..df13af5
--- /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 0000000..d41fee3
--- /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 0000000..c7be36e
--- /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 0000000..f2930d9
--- /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 0000000..75b0073
--- /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 0000000..5a63bb4
--- /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 0000000..10a6e1d
--- /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 0000000..6b8e72cf
--- /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 0000000..2ec8fdd
--- /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 a0cc446..954b816 100644
--- a/tools/locked_region_code_injection/Android.bp
+++ b/tools/locked_region_code_injection/Android.bp
@@ -19,3 +19,20 @@
"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 0000000..bd43f17
--- /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 81a0773..2067bb4 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 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 @@
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 @@
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 @@
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 @@
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 @@
"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 @@
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 void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
+ 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 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 @@
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 c5e59e3..5f62403 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 @@
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 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 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 99d8418..5df0160 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 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 828cce7..d22ea23 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.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 @@
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 @@
} 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 b44e8b4..bfef106 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 @@
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 31fa0bf..28f00b9 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.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 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 0000000..7441d2b
--- /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 0000000..c80975e
--- /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 d1c8f34..e3ba6a7 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 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 @@
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 0000000..2314c18
--- /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 0000000..9fa6f39
--- /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"