Merge "Migrate Cellbroadcast modules to use modules-utils"
diff --git a/Android.bp b/Android.bp
index a343308..1fbdc15 100644
--- a/Android.bp
+++ b/Android.bp
@@ -118,7 +118,6 @@
":libbluetooth-binder-aidl",
":libcamera_client_aidl",
":libcamera_client_framework_aidl",
- ":packagemanager_aidl",
":libupdate_engine_aidl",
":resourcemanager_aidl",
":storaged_aidl",
@@ -209,6 +208,7 @@
name: "framework-internal-utils",
static_libs: [
"apex_aidl_interface-java",
+ "packagemanager_aidl-java",
"framework-protos",
"updatable-driver-protos",
"ota_metadata_proto_java",
@@ -357,9 +357,6 @@
srcs: [
"core/java/android/net/annotations/PolicyDirection.java",
"core/java/com/android/internal/util/HexDump.java",
- "core/java/com/android/internal/util/IState.java",
- "core/java/com/android/internal/util/State.java",
- "core/java/com/android/internal/util/StateMachine.java",
"core/java/com/android/internal/util/WakeupMessage.java",
"services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java",
"telephony/java/android/telephony/Annotation.java",
@@ -411,7 +408,6 @@
"core/java/com/android/internal/util/AsyncChannel.java",
"core/java/com/android/internal/util/AsyncService.java",
"core/java/com/android/internal/util/Protocol.java",
- "core/java/com/android/internal/util/Preconditions.java",
"telephony/java/android/telephony/Annotation.java",
":net-utils-framework-wifi-common-srcs",
],
diff --git a/ApiDocs.bp b/ApiDocs.bp
index 65a6547..1767678 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -77,22 +77,16 @@
// Module sources
":art.module.public.api{.public.stubs.source}",
":conscrypt.module.public.api{.public.stubs.source}",
- ":framework-connectivity-sources",
- ":framework-mediaprovider-sources",
- ":framework-permission-sources",
- ":framework-sdkextensions-sources",
- ":framework-statsd-sources",
- ":framework-tethering-srcs",
- ":framework-wifi-updatable-sources",
":i18n.module.public.api{.public.stubs.source}",
- ":ike-srcs",
- ":updatable-media-srcs",
// No longer part of the stubs, but are included in the docs.
":android-test-base-sources",
":android-test-mock-sources",
":android-test-runner-sources",
],
+ arg_files: [
+ "core/res/AndroidManifest.xml",
+ ],
libs: framework_docs_only_libs,
create_doc_stubs: true,
annotations_enabled: true,
@@ -106,6 +100,7 @@
merge_annotations_dirs: [
"metalava-manual",
],
+ write_sdk_values: true,
}
droidstubs {
@@ -114,6 +109,24 @@
args: metalava_framework_docs_args,
}
+// Defaults module for doc-stubs targets that use module source code as input.
+// This is the default/normal.
+stubs_defaults {
+ name: "framework-doc-stubs-sources-default",
+ defaults: ["framework-doc-stubs-default"],
+ srcs: [
+ ":framework-connectivity-sources",
+ ":framework-mediaprovider-sources",
+ ":framework-permission-sources",
+ ":framework-sdkextensions-sources",
+ ":framework-statsd-sources",
+ ":framework-tethering-srcs",
+ ":framework-wifi-updatable-sources",
+ ":ike-srcs",
+ ":updatable-media-srcs",
+ ],
+}
+
droidstubs {
name: "android-non-updatable-doc-stubs-system",
defaults: ["android-non-updatable-doc-stubs-defaults"],
@@ -123,26 +136,46 @@
droidstubs {
name: "framework-doc-stubs",
- defaults: ["framework-doc-stubs-default"],
- arg_files: [
- "core/res/AndroidManifest.xml",
- ],
+ defaults: ["framework-doc-stubs-sources-default"],
args: metalava_framework_docs_args,
- write_sdk_values: true,
}
droidstubs {
name: "framework-doc-system-stubs",
- defaults: ["framework-doc-stubs-default"],
- arg_files: [
- "core/res/AndroidManifest.xml",
- ],
+ defaults: ["framework-doc-stubs-sources-default"],
args: metalava_framework_docs_args +
" --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ",
- write_sdk_values: true,
api_levels_sdk_type: "system",
}
+// Experimental target building doc stubs with module stub source code as input.
+// This is intended to eventually replace framework-doc-stubs, once all diffs
+// have been eliminated.
+droidstubs {
+ name: "framework-doc-stubs-module-stubs",
+ defaults: ["framework-doc-stubs-default"],
+ args: metalava_framework_docs_args,
+ srcs: [
+ ":android.net.ipsec.ike{.public.stubs.source}",
+ ":framework-connectivity{.public.stubs.source}",
+ ":framework-media{.public.stubs.source}",
+ ":framework-mediaprovider{.public.stubs.source}",
+ ":framework-permission{.public.stubs.source}",
+ ":framework-sdkextensions{.public.stubs.source}",
+ ":framework-statsd{.public.stubs.source}",
+ ":framework-tethering{.public.stubs.source}",
+ ":framework-wifi{.public.stubs.source}",
+ ],
+ aidl: {
+ local_include_dirs: [
+ "apex/media/aidl/stable",
+ ],
+ include_dirs: [
+ "packages/modules/Connectivity/framework/aidl-export",
+ ],
+ },
+}
+
/////////////////////////////////////////////////////////////////////
// API docs are created from the generated stub source files
// using droiddoc
diff --git a/apct-tests/perftests/multiuser/Android.bp b/apct-tests/perftests/multiuser/Android.bp
index 91e2074..c967e51 100644
--- a/apct-tests/perftests/multiuser/Android.bp
+++ b/apct-tests/perftests/multiuser/Android.bp
@@ -31,5 +31,6 @@
],
platform_apis: true,
test_suites: ["device-tests"],
+ data: ["trace_configs/*"],
certificate: "platform",
}
diff --git a/apct-tests/perftests/multiuser/AndroidTest.xml b/apct-tests/perftests/multiuser/AndroidTest.xml
index 9117561..bec3cc9 100644
--- a/apct-tests/perftests/multiuser/AndroidTest.xml
+++ b/apct-tests/perftests/multiuser/AndroidTest.xml
@@ -16,14 +16,51 @@
<configuration description="Runs MultiUserPerfTests metric instrumentation.">
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-metric-instrumentation" />
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="MultiUserPerfTests.apk" />
<option name="test-file-name" value="MultiUserPerfDummyApp.apk" />
</target_preparer>
+ <!-- Needed for pushing the trace config file -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file" key="trace_config_multi_user.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+ <!--Install the content provider automatically when we push some file in sdcard folder.-->
+ <!--Needed to avoid the installation during the test suite.-->
+ <option name="push-file" key="trace_config_multi_user.textproto" value="/sdcard/sample.textproto" />
+ </target_preparer>
+
+ <!-- Needed for pulling the collected trace config on to the host -->
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="pull-pattern-keys" value="perfetto_file_path" />
+ </metrics_collector>
+
+ <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+ <option name="isolated-storage" value="false" />
+
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.perftests.multiuser" />
<option name="hidden-api-checks" value="false"/>
+
+ <!-- Listener related args for collecting the traces and waiting for the device to stabilize. -->
+ <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" />
+ <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. -->
+ <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
+ <!-- ProcLoadListener related arguments -->
+ <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run -->
+ <option name="instrumentation-arg" key="procload-collector:per_run" value="true" />
+ <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" />
+ <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" />
+ <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" />
+
+ <!-- PerfettoListener related arguments -->
+ <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+ <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+
+ <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
</test>
</configuration>
diff --git a/apct-tests/perftests/multiuser/trace_configs/trace_config_multi_user.textproto b/apct-tests/perftests/multiuser/trace_configs/trace_config_multi_user.textproto
new file mode 100644
index 0000000..93b06e8
--- /dev/null
+++ b/apct-tests/perftests/multiuser/trace_configs/trace_config_multi_user.textproto
@@ -0,0 +1,154 @@
+# 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.
+
+# proto-message: TraceConfig
+
+# Enable periodic flushing of the trace buffer into the output file.
+write_into_file: true
+
+# Writes the userspace buffer into the file every 1s.
+file_write_period_ms: 1000
+
+# See b/126487238 - we need to guarantee ordering of events.
+flush_period_ms: 10000
+
+# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# trace data. The trace buffer sizing depends on the number of trace categories
+# enabled and the device activity.
+
+# RSS events
+buffers {
+ size_kb: 32768
+ fill_policy: RING_BUFFER
+}
+
+# procfs polling
+buffers {
+ size_kb: 8192
+ fill_policy: RING_BUFFER
+}
+
+data_sources {
+ config {
+ name: "linux.ftrace"
+ target_buffer: 0
+ ftrace_config {
+ # These parameters affect only the kernel trace buffer size and how
+ # frequently it gets moved into the userspace buffer defined above.
+ buffer_size_kb: 16384
+ drain_period_ms: 250
+
+ # Store certain high-volume "sched" ftrace events in a denser format
+ # (falling back to the default format if not supported by the tracer).
+ compact_sched {
+ enabled: true
+ }
+
+ # Enables symbol name resolution against /proc/kallsyms
+ symbolize_ksyms: true
+
+ # We need to do process tracking to ensure kernel ftrace events targeted at short-lived
+ # threads are associated correctly
+ ftrace_events: "task/task_newtask"
+ ftrace_events: "task/task_rename"
+ ftrace_events: "sched/sched_process_exit"
+ ftrace_events: "sched/sched_process_free"
+
+ # Memory events
+ ftrace_events: "rss_stat"
+ ftrace_events: "ion_heap_shrink"
+ ftrace_events: "ion_heap_grow"
+ ftrace_events: "ion/ion_stat"
+ ftrace_events: "dmabuf_heap/dma_heap_stat"
+ ftrace_events: "oom_score_adj_update"
+ ftrace_events: "gpu_mem/gpu_mem_total"
+
+ # Old (kernel) LMK
+ ftrace_events: "lowmemorykiller/lowmemory_kill"
+
+ atrace_apps: "*"
+
+ atrace_categories: "am"
+ atrace_categories: "binder_driver"
+ atrace_categories: "bionic"
+ atrace_categories: "dalvik"
+ atrace_categories: "input"
+ atrace_categories: "pm"
+ atrace_categories: "res"
+ atrace_categories: "rro"
+ atrace_categories: "ss"
+ atrace_categories: "view"
+ atrace_categories: "wm"
+
+ atrace_categories: "freq"
+ atrace_categories: "sched"
+ atrace_categories: "sync"
+ atrace_categories: "workq"
+
+ }
+ }
+}
+
+data_sources: {
+ config {
+ name: "android.gpu.memory"
+ target_buffer: 0
+ }
+}
+
+data_sources {
+ config {
+ name: "linux.process_stats"
+ target_buffer: 1
+ process_stats_config {
+ proc_stats_poll_ms: 10000
+ }
+ }
+}
+
+data_sources {
+ config {
+ name: "linux.sys_stats"
+ target_buffer: 1
+ sys_stats_config {
+ meminfo_period_ms: 1000
+ meminfo_counters: MEMINFO_MEM_TOTAL
+ meminfo_counters: MEMINFO_MEM_FREE
+ meminfo_counters: MEMINFO_MEM_AVAILABLE
+ meminfo_counters: MEMINFO_BUFFERS
+ meminfo_counters: MEMINFO_CACHED
+ meminfo_counters: MEMINFO_SWAP_CACHED
+ meminfo_counters: MEMINFO_ACTIVE
+ meminfo_counters: MEMINFO_INACTIVE
+ meminfo_counters: MEMINFO_ACTIVE_ANON
+ meminfo_counters: MEMINFO_INACTIVE_ANON
+ meminfo_counters: MEMINFO_ACTIVE_FILE
+ meminfo_counters: MEMINFO_INACTIVE_FILE
+ meminfo_counters: MEMINFO_UNEVICTABLE
+ meminfo_counters: MEMINFO_SWAP_TOTAL
+ meminfo_counters: MEMINFO_SWAP_FREE
+ meminfo_counters: MEMINFO_DIRTY
+ meminfo_counters: MEMINFO_WRITEBACK
+ meminfo_counters: MEMINFO_ANON_PAGES
+ meminfo_counters: MEMINFO_MAPPED
+ meminfo_counters: MEMINFO_SHMEM
+ }
+ }
+}
+
+data_sources: {
+ config: {
+ name: "android.surfaceflinger.frametimeline"
+ }
+}
diff --git a/api/Android.bp b/api/Android.bp
index fbfbc7c..0acd759 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -24,9 +24,8 @@
default_applicable_licenses: ["frameworks_base_license"],
}
-python_binary_host {
- name: "api_versions_trimmer",
- srcs: ["api_versions_trimmer.py"],
+python_defaults {
+ name: "python3_version_defaults",
version: {
py2: {
enabled: false,
@@ -38,6 +37,12 @@
},
}
+python_binary_host {
+ name: "api_versions_trimmer",
+ srcs: ["api_versions_trimmer.py"],
+ defaults: ["python3_version_defaults"],
+}
+
python_test_host {
name: "api_versions_trimmer_unittests",
main: "api_versions_trimmer_unittests.py",
@@ -45,24 +50,35 @@
"api_versions_trimmer_unittests.py",
"api_versions_trimmer.py",
],
+ defaults: ["python3_version_defaults"],
test_options: {
unit_test: true,
},
- version: {
- py2: {
- enabled: false,
- },
- py3: {
- enabled: true,
- embedded_launcher: false,
- },
+}
+
+python_binary_host {
+ name: "merge_annotation_zips",
+ srcs: ["merge_annotation_zips.py"],
+ defaults: ["python3_version_defaults"],
+}
+
+python_test_host {
+ name: "merge_annotation_zips_test",
+ main: "merge_annotation_zips_test.py",
+ srcs: [
+ "merge_annotation_zips.py",
+ "merge_annotation_zips_test.py",
+ ],
+ defaults: ["python3_version_defaults"],
+ test_options: {
+ unit_test: true,
},
}
metalava_cmd = "$(location metalava)"
// Silence reflection warnings. See b/168689341
metalava_cmd += " -J--add-opens=java.base/java.util=ALL-UNNAMED "
-metalava_cmd += " --no-banner --format=v2 "
+metalava_cmd += " --quiet --no-banner --format=v2 "
genrule {
name: "current-api-xml",
@@ -118,13 +134,13 @@
":android-incompatibilities.api.public.latest",
":frameworks-base-api-current.txt",
],
- out: ["stdout.txt"],
+ out: ["updated-baseline.txt"],
tools: ["metalava"],
cmd: metalava_cmd +
"--check-compatibility:api:released $(location :android.api.public.latest) " +
"--baseline:compatibility:released $(location :android-incompatibilities.api.public.latest) " +
- "$(location :frameworks-base-api-current.txt) " +
- "> $(genDir)/stdout.txt",
+ "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+ "$(location :frameworks-base-api-current.txt)",
}
genrule {
@@ -231,14 +247,14 @@
":frameworks-base-api-current.txt",
":frameworks-base-api-system-current.txt",
],
- out: ["stdout.txt"],
+ out: ["updated-baseline.txt"],
tools: ["metalava"],
cmd: metalava_cmd +
"--check-compatibility:api:released $(location :android.api.system.latest) " +
"--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
"--baseline:compatibility:released $(location :android-incompatibilities.api.system.latest) " +
- "$(location :frameworks-base-api-system-current.txt) " +
- "> $(genDir)/stdout.txt",
+ "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+ "$(location :frameworks-base-api-system-current.txt)",
}
genrule {
@@ -320,7 +336,7 @@
":frameworks-base-api-current.txt",
":frameworks-base-api-module-lib-current.txt",
],
- out: ["stdout.txt"],
+ out: ["updated-baseline.txt"],
tools: ["metalava"],
cmd: metalava_cmd +
"--check-compatibility:api:released $(location :android.api.module-lib.latest) " +
@@ -329,8 +345,8 @@
// MODULE_LIBS -> public.
"--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
"--baseline:compatibility:released $(location :android-incompatibilities.api.module-lib.latest) " +
- "$(location :frameworks-base-api-module-lib-current.txt) " +
- "> $(genDir)/stdout.txt",
+ "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+ "$(location :frameworks-base-api-module-lib-current.txt)",
}
genrule {
diff --git a/api/api_versions_trimmer_unittests.py b/api/api_versions_trimmer_unittests.py
index 4eb929e..d2e5b7d 100644
--- a/api/api_versions_trimmer_unittests.py
+++ b/api/api_versions_trimmer_unittests.py
@@ -304,4 +304,4 @@
if __name__ == "__main__":
- unittest.main()
+ unittest.main(verbosity=2)
diff --git a/api/merge_annotation_zips.py b/api/merge_annotation_zips.py
new file mode 100755
index 0000000..9c67d7b
--- /dev/null
+++ b/api/merge_annotation_zips.py
@@ -0,0 +1,71 @@
+#!/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 merge annotation XML files (created by e.g. metalava)."""
+
+from pathlib import Path
+import sys
+import xml.etree.ElementTree as ET
+import zipfile
+
+
+def validate_xml_assumptions(root):
+ """Verify the format of the annotations XML matches expectations"""
+ prevName = ""
+ assert root.tag == 'root'
+ for child in root:
+ assert child.tag == 'item', 'unexpected tag: %s' % child.tag
+ assert list(child.attrib.keys()) == ['name'], 'unexpected attribs: %s' % child.attrib.keys()
+ assert prevName < child.get('name'), 'items unexpectedly not strictly sorted (possibly duplicate entries)'
+ prevName = child.get('name')
+
+
+def merge_xml(a, b):
+ """Merge two annotation xml files"""
+ for xml in [a, b]:
+ validate_xml_assumptions(xml)
+ a.extend(b[:])
+ a[:] = sorted(a[:], key=lambda x: x.get('name'))
+ validate_xml_assumptions(a)
+
+
+def merge_zip_file(out_dir, zip_file):
+ """Merge the content of the zip_file into out_dir"""
+ for filename in zip_file.namelist():
+ path = Path(out_dir, filename)
+ if path.exists():
+ existing_xml = ET.parse(path)
+ with zip_file.open(filename) as other_file:
+ other_xml = ET.parse(other_file)
+ merge_xml(existing_xml.getroot(), other_xml.getroot())
+ existing_xml.write(path, encoding='UTF-8', xml_declaration=True)
+ else:
+ zip_file.extract(filename, out_dir)
+
+
+def main():
+ out_dir = Path(sys.argv[1])
+ zip_filenames = sys.argv[2:]
+
+ assert not out_dir.exists()
+ out_dir.mkdir()
+ for zip_filename in zip_filenames:
+ with zipfile.ZipFile(zip_filename) as zip_file:
+ merge_zip_file(out_dir, zip_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/api/merge_annotation_zips_test.py b/api/merge_annotation_zips_test.py
new file mode 100644
index 0000000..26795c47
--- /dev/null
+++ b/api/merge_annotation_zips_test.py
@@ -0,0 +1,124 @@
+#!/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
+from pathlib import Path
+import tempfile
+import unittest
+import zipfile
+
+import merge_annotation_zips
+
+
+zip_a = {
+ 'android/provider/annotations.xml':
+ """<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <item name="android.provider.BlockedNumberContract boolean isBlocked(android.content.Context, java.lang.String)">
+ <annotation name="androidx.annotation.WorkerThread"/>
+ </item>
+ <item name="android.provider.SimPhonebookContract.SimRecords android.net.Uri getItemUri(int, int, int) 2">
+ <annotation name="androidx.annotation.IntRange">
+ <val name="from" val="1" />
+ </annotation>
+ </item>
+</root>""",
+ 'android/os/annotations.xml':
+ """<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <item name="android.app.ActionBar void setCustomView(int) 0">
+ <annotation name="androidx.annotation.LayoutRes"/>
+ </item>
+</root>
+"""
+}
+
+zip_b = {
+ 'android/provider/annotations.xml':
+ """<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <item name="android.provider.MediaStore QUERY_ARG_MATCH_FAVORITE">
+ <annotation name="androidx.annotation.IntDef">
+ <val name="value" val="{android.provider.MediaStore.MATCH_DEFAULT, android.provider.MediaStore.MATCH_INCLUDE, android.provider.MediaStore.MATCH_EXCLUDE, android.provider.MediaStore.MATCH_ONLY}" />
+ <val name="flag" val="true" />
+ </annotation>
+ </item>
+ <item name="android.provider.MediaStore QUERY_ARG_MATCH_PENDING">
+ <annotation name="androidx.annotation.IntDef">
+ <val name="value" val="{android.provider.MediaStore.MATCH_DEFAULT, android.provider.MediaStore.MATCH_INCLUDE, android.provider.MediaStore.MATCH_EXCLUDE, android.provider.MediaStore.MATCH_ONLY}" />
+ <val name="flag" val="true" />
+ </annotation>
+ </item>
+</root>"""
+}
+
+zip_c = {
+ 'android/app/annotations.xml':
+ """<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <item name="android.app.ActionBar void setCustomView(int) 0">
+ <annotation name="androidx.annotation.LayoutRes"/>
+ </item>
+</root>"""
+}
+
+merged_provider = """<?xml version='1.0' encoding='UTF-8'?>
+<root>
+ <item name="android.provider.BlockedNumberContract boolean isBlocked(android.content.Context, java.lang.String)">
+ <annotation name="androidx.annotation.WorkerThread" />
+ </item>
+ <item name="android.provider.MediaStore QUERY_ARG_MATCH_FAVORITE">
+ <annotation name="androidx.annotation.IntDef">
+ <val name="value" val="{android.provider.MediaStore.MATCH_DEFAULT, android.provider.MediaStore.MATCH_INCLUDE, android.provider.MediaStore.MATCH_EXCLUDE, android.provider.MediaStore.MATCH_ONLY}" />
+ <val name="flag" val="true" />
+ </annotation>
+ </item>
+ <item name="android.provider.MediaStore QUERY_ARG_MATCH_PENDING">
+ <annotation name="androidx.annotation.IntDef">
+ <val name="value" val="{android.provider.MediaStore.MATCH_DEFAULT, android.provider.MediaStore.MATCH_INCLUDE, android.provider.MediaStore.MATCH_EXCLUDE, android.provider.MediaStore.MATCH_ONLY}" />
+ <val name="flag" val="true" />
+ </annotation>
+ </item>
+<item name="android.provider.SimPhonebookContract.SimRecords android.net.Uri getItemUri(int, int, int) 2">
+ <annotation name="androidx.annotation.IntRange">
+ <val name="from" val="1" />
+ </annotation>
+ </item>
+</root>"""
+
+
+
+class MergeAnnotationZipsTest(unittest.TestCase):
+
+ def test_merge_zips(self):
+ with tempfile.TemporaryDirectory() as out_dir:
+ for zip_content in [zip_a, zip_b, zip_c]:
+ f = io.BytesIO()
+ with zipfile.ZipFile(f, "w") as zip_file:
+ for filename, content in zip_content.items():
+ zip_file.writestr(filename, content)
+ merge_annotation_zips.merge_zip_file(out_dir, zip_file)
+
+ # Unchanged
+ self.assertEqual(zip_a['android/os/annotations.xml'], Path(out_dir, 'android/os/annotations.xml').read_text())
+ self.assertEqual(zip_c['android/app/annotations.xml'], Path(out_dir, 'android/app/annotations.xml').read_text())
+
+ # Merged
+ self.assertEqual(merged_provider, Path(out_dir, 'android/provider/annotations.xml').read_text())
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/core/api/current.txt b/core/api/current.txt
index e4e7d4e..93ef8f8 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -29998,12 +29998,15 @@
method public int readInt();
method public void readIntArray(@NonNull int[]);
method public void readList(@NonNull java.util.List, @Nullable ClassLoader);
+ method public <T> void readList(@NonNull java.util.List<? super T>, @Nullable ClassLoader, @NonNull Class<T>);
method public long readLong();
method public void readLongArray(@NonNull long[]);
method public void readMap(@NonNull java.util.Map, @Nullable ClassLoader);
method @Nullable public <T extends android.os.Parcelable> T readParcelable(@Nullable ClassLoader);
+ method @Nullable public <T extends android.os.Parcelable> T readParcelable(@Nullable ClassLoader, @NonNull Class<T>);
method @Nullable public android.os.Parcelable[] readParcelableArray(@Nullable ClassLoader);
method @Nullable public android.os.Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader);
+ method @Nullable public <T> android.os.Parcelable.Creator<T> readParcelableCreator(@Nullable ClassLoader, @NonNull Class<T>);
method @NonNull public <T extends android.os.Parcelable> java.util.List<T> readParcelableList(@NonNull java.util.List<T>, @Nullable ClassLoader);
method @Nullable public android.os.PersistableBundle readPersistableBundle();
method @Nullable public android.os.PersistableBundle readPersistableBundle(@Nullable ClassLoader);
@@ -30142,7 +30145,7 @@
public interface Parcelable {
method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field public static final int CONTENTS_FILE_DESCRIPTOR = 1; // 0x1
field public static final int PARCELABLE_WRITE_RETURN_VALUE = 1; // 0x1
}
diff --git a/core/java/Android.bp b/core/java/Android.bp
index a52e3b0..3b2d883 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -381,10 +381,7 @@
"android/util/Rational.java",
"com/android/internal/util/FastXmlSerializer.java",
"com/android/internal/util/HexDump.java",
- "com/android/internal/util/IState.java",
"com/android/internal/util/MessageUtils.java",
- "com/android/internal/util/State.java",
- "com/android/internal/util/StateMachine.java",
"com/android/internal/util/WakeupMessage.java",
],
visibility: [
diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java
index 5a25cfc..ae8d010 100644
--- a/core/java/android/net/nsd/NsdManager.java
+++ b/core/java/android/net/nsd/NsdManager.java
@@ -23,6 +23,9 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemService;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
@@ -126,6 +129,24 @@
private static final boolean DBG = false;
/**
+ * When enabled, apps targeting < Android 12 are considered legacy for
+ * the NSD native daemon.
+ * The platform will only keep the daemon running as long as there are
+ * any legacy apps connected.
+ *
+ * After Android 12, directly communicate with native daemon might not
+ * work since the native damon won't always stay alive.
+ * Use the NSD APIs from NsdManager as the replacement is recommended.
+ * An another alternative could be bundling your own mdns solutions instead of
+ * depending on the system mdns native daemon.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S)
+ public static final long RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS = 191844585L;
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -203,6 +224,9 @@
public static final int DAEMON_CLEANUP = BASE + 21;
/** @hide */
+ public static final int DAEMON_STARTUP = BASE + 22;
+
+ /** @hide */
public static final int ENABLE = BASE + 24;
/** @hide */
public static final int DISABLE = BASE + 25;
@@ -232,6 +256,8 @@
EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE");
EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED");
EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
+ EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
+ EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
EVENT_NAMES.put(ENABLE, "ENABLE");
EVENT_NAMES.put(DISABLE, "DISABLE");
EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT");
@@ -494,6 +520,12 @@
} catch (InterruptedException e) {
fatal("Interrupted wait at init");
}
+ if (CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) {
+ return;
+ }
+ // Only proactively start the daemon if the target SDK < S, otherwise the internal service
+ // would automatically start/stop the native daemon as needed.
+ mAsyncChannel.sendMessage(DAEMON_STARTUP);
}
private static void fatal(String msg) {
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 6da02f5..7ce8d72 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -260,6 +260,9 @@
/**
* Returns the value for key {@code key}.
*
+ * This call should always be made after {@link #unparcel()} or inside a lock after making sure
+ * {@code mMap} is not null.
+ *
* @hide
*/
final Object getValue(String key) {
@@ -270,15 +273,15 @@
/**
* Returns the value for a certain position in the array map.
*
+ * This call should always be made after {@link #unparcel()} or inside a lock after making sure
+ * {@code mMap} is not null.
+ *
* @hide
*/
final Object getValueAt(int i) {
Object object = mMap.valueAt(i);
if (object instanceof Supplier<?>) {
- Supplier<?> supplier = (Supplier<?>) object;
- synchronized (this) {
- object = supplier.get();
- }
+ object = ((Supplier<?>) object).get();
mMap.setValueAt(i, object);
}
return object;
@@ -428,7 +431,7 @@
*
* @hide
*/
- public static boolean kindofEquals(BaseBundle a, BaseBundle b) {
+ public static boolean kindofEquals(@Nullable BaseBundle a, @Nullable BaseBundle b) {
return (a == b) || (a != null && a.kindofEquals(b));
}
@@ -1045,7 +1048,7 @@
*/
char getChar(String key, char defaultValue) {
unparcel();
- Object o = getValue(key);
+ Object o = mMap.get(key);
if (o == null) {
return defaultValue;
}
@@ -1448,7 +1451,7 @@
@Nullable
short[] getShortArray(@Nullable String key) {
unparcel();
- Object o = getValue(key);
+ Object o = mMap.get(key);
if (o == null) {
return null;
}
@@ -1471,7 +1474,7 @@
@Nullable
char[] getCharArray(@Nullable String key) {
unparcel();
- Object o = getValue(key);
+ Object o = mMap.get(key);
if (o == null) {
return null;
}
@@ -1540,7 +1543,7 @@
@Nullable
float[] getFloatArray(@Nullable String key) {
unparcel();
- Object o = getValue(key);
+ Object o = mMap.get(key);
if (o == null) {
return null;
}
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index a870c04..c575c80e 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -69,3 +69,6 @@
# UpdateEngine
per-file *UpdateEngine* = file:/platform/system/update_engine:/OWNERS
+
+# VINTF
+per-file Vintf* = file:/platform/system/libvintf:/OWNERS
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index a2716d2..243dfb3 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -16,6 +16,8 @@
package android.os;
+import static java.util.Objects.requireNonNull;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -261,6 +263,10 @@
private static final int VAL_SIZE = 26;
private static final int VAL_SIZEF = 27;
private static final int VAL_DOUBLEARRAY = 28;
+ private static final int VAL_CHAR = 29;
+ private static final int VAL_SHORTARRAY = 30;
+ private static final int VAL_CHARARRAY = 31;
+ private static final int VAL_FLOATARRAY = 32;
// The initial int32 in a Binder call's reply Parcel header:
// Keep these in sync with libbinder's binder/Status.h.
@@ -446,6 +452,23 @@
}
/**
+ * Retrieve a new Parcel object from the pool for use with a specific binder.
+ *
+ * Associate this parcel with a binder object. This marks the parcel as being prepared for a
+ * transaction on this specific binder object. Based on this, the format of the wire binder
+ * protocol may change. For future compatibility, it is recommended to use this for all
+ * Parcels.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Parcel obtain(@NonNull IBinder binder) {
+ Parcel parcel = Parcel.obtain();
+ parcel.markForBinder(binder);
+ return parcel;
+ }
+
+ /**
* Put a Parcel object back into the pool. You must not touch
* the object after this call.
*/
@@ -517,16 +540,9 @@
}
/**
- * Associate this parcel with a binder object. This marks the parcel as being prepared for a
- * transaction on this specific binder object. Based on this, the format of the wire binder
- * protocol may change. This should be called before any data is written to the parcel. If this
- * is called multiple times, this will only be marked for the last binder. For future
- * compatibility, it is recommended to call this on all parcels which are being sent over
- * binder.
- *
* @hide
*/
- public void markForBinder(@NonNull IBinder binder) {
+ private void markForBinder(@NonNull IBinder binder) {
nativeMarkForBinder(mNativePtr, binder);
}
@@ -1307,6 +1323,46 @@
}
}
+ /** @hide */
+ public void writeShortArray(@Nullable short[] val) {
+ if (val != null) {
+ int n = val.length;
+ writeInt(n);
+ for (int i = 0; i < n; i++) {
+ writeInt(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ /** @hide */
+ @Nullable
+ public short[] createShortArray() {
+ int n = readInt();
+ if (n >= 0 && n <= (dataAvail() >> 2)) {
+ short[] val = new short[n];
+ for (int i = 0; i < n; i++) {
+ val[i] = (short) readInt();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ /** @hide */
+ public void readShortArray(@NonNull short[] val) {
+ int n = readInt();
+ if (n == val.length) {
+ for (int i = 0; i < n; i++) {
+ val[i] = (short) readInt();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
public final void writeCharArray(@Nullable char[] val) {
if (val != null) {
int N = val.length;
@@ -1972,6 +2028,14 @@
return VAL_SIZE;
} else if (v instanceof double[]) {
return VAL_DOUBLEARRAY;
+ } else if (v instanceof Character) {
+ return VAL_CHAR;
+ } else if (v instanceof short[]) {
+ return VAL_SHORTARRAY;
+ } else if (v instanceof char[]) {
+ return VAL_CHARARRAY;
+ } else if (v instanceof float[]) {
+ return VAL_FLOATARRAY;
} else {
Class<?> clazz = v.getClass();
if (clazz.isArray() && clazz.getComponentType() == Object.class) {
@@ -2074,6 +2138,18 @@
case VAL_DOUBLEARRAY:
writeDoubleArray((double[]) v);
break;
+ case VAL_CHAR:
+ writeInt((Character) v);
+ break;
+ case VAL_SHORTARRAY:
+ writeShortArray((short[]) v);
+ break;
+ case VAL_CHARARRAY:
+ writeCharArray((char[]) v);
+ break;
+ case VAL_FLOATARRAY:
+ writeFloatArray((float[]) v);
+ break;
case VAL_OBJECTARRAY:
writeArray((Object[]) v);
break;
@@ -2754,7 +2830,20 @@
*/
public final void readList(@NonNull List outVal, @Nullable ClassLoader loader) {
int N = readInt();
- readListInternal(outVal, N, loader);
+ readListInternal(outVal, N, loader, /* clazz */ null);
+ }
+
+ /**
+ * Same as {@link #readList(List, ClassLoader)} but accepts {@code clazz} parameter as
+ * the type required for each item. If the item to be deserialized is not an instance
+ * of that class or any of its children class
+ * a {@link BadParcelableException} will be thrown.
+ */
+ public <T> void readList(@NonNull List<? super T> outVal,
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ Objects.requireNonNull(clazz);
+ int n = readInt();
+ readListInternal(outVal, n, loader, clazz);
}
/**
@@ -2953,7 +3042,7 @@
return null;
}
ArrayList l = new ArrayList(N);
- readListInternal(l, N, loader);
+ readListInternal(l, N, loader, /* clazz */ null);
return l;
}
@@ -3353,20 +3442,29 @@
*/
@Nullable
public final Object readValue(@Nullable ClassLoader loader) {
+ return readValue(loader, /* clazz */ null);
+ }
+
+
+ /**
+ * @param clazz The type of the object expected or {@code null} for performing no checks.
+ */
+ @Nullable
+ private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz) {
int type = readInt();
- final Object object;
+ final T object;
if (isLengthPrefixed(type)) {
int length = readInt();
int start = dataPosition();
- object = readValue(type, loader);
+ object = readValue(type, loader, clazz);
int actual = dataPosition() - start;
if (actual != length) {
- Log.w(TAG,
+ Slog.wtfStack(TAG,
"Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
+ " consumed " + actual + " bytes, but " + length + " expected.");
}
} else {
- object = readValue(type, loader);
+ object = readValue(type, loader, clazz);
}
return object;
}
@@ -3403,7 +3501,7 @@
setDataPosition(MathUtils.addOrThrow(dataPosition(), length));
return new LazyValue(this, start, length, type, loader);
} else {
- return readValue(type, loader);
+ return readValue(type, loader, /* clazz */ null);
}
}
@@ -3412,12 +3510,19 @@
private final int mLength;
private final int mType;
@Nullable private final ClassLoader mLoader;
- @Nullable private Parcel mSource;
@Nullable private Object mObject;
- @Nullable private Parcel mValueParcel;
+ @Nullable private volatile Parcel mValueParcel;
+
+ /**
+ * This goes from non-null to null once. Always check the nullability of this object before
+ * performing any operations, either involving itself or mObject since the happens-before
+ * established by this volatile will guarantee visibility of either. We can assume this
+ * parcel won't change anymore.
+ */
+ @Nullable private volatile Parcel mSource;
LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) {
- mSource = source;
+ mSource = requireNonNull(source);
mPosition = position;
mLength = length;
mType = type;
@@ -3426,38 +3531,41 @@
@Override
public Object get() {
- if (mObject == null) {
- int restore = mSource.dataPosition();
- try {
- mSource.setDataPosition(mPosition);
- mObject = mSource.readValue(mLoader);
- } finally {
- mSource.setDataPosition(restore);
- }
- mSource = null;
- if (mValueParcel != null) {
- mValueParcel.recycle();
- mValueParcel = null;
+ Parcel source = mSource;
+ if (source != null) {
+ synchronized (source) {
+ int restore = source.dataPosition();
+ try {
+ source.setDataPosition(mPosition);
+ mObject = source.readValue(mLoader);
+ } finally {
+ source.setDataPosition(restore);
+ }
+ mSource = null;
}
}
return mObject;
}
public void writeToParcel(Parcel out) {
- if (mObject == null) {
- out.appendFrom(mSource, mPosition, mLength + 8);
+ Parcel source = mSource;
+ if (source != null) {
+ out.appendFrom(source, mPosition, mLength + 8);
} else {
out.writeValue(mObject);
}
}
public boolean hasFileDescriptors() {
- return getValueParcel().hasFileDescriptors();
+ Parcel source = mSource;
+ return (source != null)
+ ? getValueParcel(source).hasFileDescriptors()
+ : Parcel.hasFileDescriptors(mObject);
}
@Override
public String toString() {
- return mObject == null
+ return (mSource != null)
? "Supplier{" + valueTypeToString(mType) + "@" + mPosition + "+" + mLength + '}'
: "Supplier{" + mObject + "}";
}
@@ -3476,155 +3584,224 @@
return false;
}
LazyValue value = (LazyValue) other;
- // Check if they are either both serialized or both deserialized
- if ((mObject == null) != (value.mObject == null)) {
+ // Check if they are either both serialized or both deserialized.
+ Parcel source = mSource;
+ Parcel otherSource = value.mSource;
+ if ((source == null) != (otherSource == null)) {
return false;
}
- // If both are deserialized, compare the live objects
- if (mObject != null) {
- return mObject.equals(value.mObject);
+ // If both are deserialized, compare the live objects.
+ if (source == null) {
+ // Note that here it's guaranteed that both mObject references contain valid values
+ // (possibly null) since mSource will have provided the memory barrier for those and
+ // once deserialized we never go back to serialized state.
+ return Objects.equals(mObject, value.mObject);
}
- // Better safely fail here since this could mean we get different objects
+ // Better safely fail here since this could mean we get different objects.
if (!Objects.equals(mLoader, value.mLoader)) {
return false;
}
- // Otherwise compare metadata prior to comparing payload
+ // Otherwise compare metadata prior to comparing payload.
if (mType != value.mType || mLength != value.mLength) {
return false;
}
- // Finally we compare the payload
- return getValueParcel().compareData(value.getValueParcel()) == 0;
+ // Finally we compare the payload.
+ return getValueParcel(source).compareData(value.getValueParcel(otherSource)) == 0;
}
@Override
public int hashCode() {
- return Objects.hash(mObject, mLoader, mType, mLength);
+ // Accessing mSource first to provide memory barrier for mObject
+ return Objects.hash(mSource == null, mObject, mLoader, mType, mLength);
}
/** This extracts the parcel section responsible for the object and returns it. */
- private Parcel getValueParcel() {
- if (mValueParcel == null) {
- mValueParcel = Parcel.obtain();
+ private Parcel getValueParcel(Parcel source) {
+ Parcel parcel = mValueParcel;
+ if (parcel == null) {
+ parcel = Parcel.obtain();
// mLength is the length of object representation, excluding the type and length.
// mPosition is the position of the entire value container, right before the type.
// So, we add 4 bytes for the type + 4 bytes for the length written.
- mValueParcel.appendFrom(mSource, mPosition, mLength + 8);
+ parcel.appendFrom(source, mPosition, mLength + 8);
+ mValueParcel = parcel;
}
- return mValueParcel;
+ return parcel;
}
}
/**
* Reads a value from the parcel of type {@code type}. Does NOT read the int representing the
* type first.
+ * @param clazz The type of the object expected or {@code null} for performing no checks.
*/
+ @SuppressWarnings("unchecked")
@Nullable
- private Object readValue(int type, @Nullable ClassLoader loader) {
+ private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+ final Object object;
switch (type) {
- case VAL_NULL:
- return null;
+ case VAL_NULL:
+ object = null;
+ break;
- case VAL_STRING:
- return readString();
+ case VAL_STRING:
+ object = readString();
+ break;
- case VAL_INTEGER:
- return readInt();
+ case VAL_INTEGER:
+ object = readInt();
+ break;
- case VAL_MAP:
- return readHashMap(loader);
+ case VAL_MAP:
+ object = readHashMap(loader);
+ break;
- case VAL_PARCELABLE:
- return readParcelable(loader);
+ case VAL_PARCELABLE:
+ object = readParcelableInternal(loader, clazz);
+ break;
- case VAL_SHORT:
- return (short) readInt();
+ case VAL_SHORT:
+ object = (short) readInt();
+ break;
- case VAL_LONG:
- return readLong();
+ case VAL_LONG:
+ object = readLong();
+ break;
- case VAL_FLOAT:
- return readFloat();
+ case VAL_FLOAT:
+ object = readFloat();
+ break;
- case VAL_DOUBLE:
- return readDouble();
+ case VAL_DOUBLE:
+ object = readDouble();
+ break;
- case VAL_BOOLEAN:
- return readInt() == 1;
+ case VAL_BOOLEAN:
+ object = readInt() == 1;
+ break;
- case VAL_CHARSEQUENCE:
- return readCharSequence();
+ case VAL_CHARSEQUENCE:
+ object = readCharSequence();
+ break;
- case VAL_LIST:
- return readArrayList(loader);
+ case VAL_LIST:
+ object = readArrayList(loader);
+ break;
- case VAL_BOOLEANARRAY:
- return createBooleanArray();
+ case VAL_BOOLEANARRAY:
+ object = createBooleanArray();
+ break;
- case VAL_BYTEARRAY:
- return createByteArray();
+ case VAL_BYTEARRAY:
+ object = createByteArray();
+ break;
- case VAL_STRINGARRAY:
- return readStringArray();
+ case VAL_STRINGARRAY:
+ object = readStringArray();
+ break;
- case VAL_CHARSEQUENCEARRAY:
- return readCharSequenceArray();
+ case VAL_CHARSEQUENCEARRAY:
+ object = readCharSequenceArray();
+ break;
- case VAL_IBINDER:
- return readStrongBinder();
+ case VAL_IBINDER:
+ object = readStrongBinder();
+ break;
- case VAL_OBJECTARRAY:
- return readArray(loader);
+ case VAL_OBJECTARRAY:
+ object = readArray(loader);
+ break;
- case VAL_INTARRAY:
- return createIntArray();
+ case VAL_INTARRAY:
+ object = createIntArray();
+ break;
- case VAL_LONGARRAY:
- return createLongArray();
+ case VAL_LONGARRAY:
+ object = createLongArray();
+ break;
- case VAL_BYTE:
- return readByte();
+ case VAL_BYTE:
+ object = readByte();
+ break;
- case VAL_SERIALIZABLE:
- return readSerializable(loader);
+ case VAL_SERIALIZABLE:
+ object = readSerializable(loader);
+ break;
- case VAL_PARCELABLEARRAY:
- return readParcelableArray(loader);
+ case VAL_PARCELABLEARRAY:
+ object = readParcelableArray(loader);
+ break;
- case VAL_SPARSEARRAY:
- return readSparseArray(loader);
+ case VAL_SPARSEARRAY:
+ object = readSparseArray(loader);
+ break;
- case VAL_SPARSEBOOLEANARRAY:
- return readSparseBooleanArray();
+ case VAL_SPARSEBOOLEANARRAY:
+ object = readSparseBooleanArray();
+ break;
- case VAL_BUNDLE:
- return readBundle(loader); // loading will be deferred
+ case VAL_BUNDLE:
+ object = readBundle(loader); // loading will be deferred
+ break;
- case VAL_PERSISTABLEBUNDLE:
- return readPersistableBundle(loader);
+ case VAL_PERSISTABLEBUNDLE:
+ object = readPersistableBundle(loader);
+ break;
- case VAL_SIZE:
- return readSize();
+ case VAL_SIZE:
+ object = readSize();
+ break;
- case VAL_SIZEF:
- return readSizeF();
+ case VAL_SIZEF:
+ object = readSizeF();
+ break;
- case VAL_DOUBLEARRAY:
- return createDoubleArray();
+ case VAL_DOUBLEARRAY:
+ object = createDoubleArray();
+ break;
- default:
- int off = dataPosition() - 4;
- throw new RuntimeException(
- "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
+ case VAL_CHAR:
+ object = (char) readInt();
+ break;
+
+ case VAL_SHORTARRAY:
+ object = createShortArray();
+ break;
+
+ case VAL_CHARARRAY:
+ object = createCharArray();
+ break;
+
+ case VAL_FLOATARRAY:
+ object = createFloatArray();
+ break;
+
+ default:
+ int off = dataPosition() - 4;
+ throw new RuntimeException(
+ "Parcel " + this + ": Unmarshalling unknown type code " + type
+ + " at offset " + off);
}
+ if (clazz != null && !clazz.isInstance(object)) {
+ throw new BadParcelableException("Unparcelled object " + object
+ + " is not an instance of required class " + clazz.getName()
+ + " provided in the parameter");
+ }
+ return (T) object;
}
private boolean isLengthPrefixed(int type) {
+ // In general, we want custom types and containers of custom types to be length-prefixed,
+ // this allows clients (eg. Bundle) to skip their content during deserialization. The
+ // exception to this is Bundle, since Bundle is already length-prefixed and already copies
+ // the correspondent section of the parcel internally.
switch (type) {
+ case VAL_MAP:
case VAL_PARCELABLE:
- case VAL_PARCELABLEARRAY:
case VAL_LIST:
case VAL_SPARSEARRAY:
- case VAL_BUNDLE:
+ case VAL_PARCELABLEARRAY:
+ case VAL_OBJECTARRAY:
case VAL_SERIALIZABLE:
return true;
default:
@@ -3643,17 +3820,42 @@
* @throws BadParcelableException Throws BadParcelableException if there
* was an error trying to instantiate the Parcelable.
*/
- @SuppressWarnings("unchecked")
@Nullable
public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
- Parcelable.Creator<?> creator = readParcelableCreator(loader);
+ return readParcelableInternal(loader, /* clazz */ null);
+ }
+
+ /**
+ * Same as {@link #readParcelable(ClassLoader)} but accepts {@code clazz} parameter as the type
+ * required for each item. If the item to be deserialized is not an instance of that class or
+ * any of its children classes a {@link BadParcelableException} will be thrown.
+ */
+ @Nullable
+ public <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader,
+ @NonNull Class<T> clazz) {
+ Objects.requireNonNull(clazz);
+ return readParcelableInternal(loader, clazz);
+ }
+
+ /**
+ *
+ * @param clazz The type of the parcelable expected or {@code null} for performing no checks.
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private <T> T readParcelableInternal(@Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+ if (clazz != null && !Parcelable.class.isAssignableFrom(clazz)) {
+ throw new BadParcelableException("About to unparcel a parcelable object "
+ + " but class required " + clazz.getName() + " is not Parcelable");
+ }
+ Parcelable.Creator<?> creator = readParcelableCreatorInternal(loader, clazz);
if (creator == null) {
return null;
}
if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
- Parcelable.ClassLoaderCreator<?> classLoaderCreator =
- (Parcelable.ClassLoaderCreator<?>) creator;
- return (T) classLoaderCreator.createFromParcel(this, loader);
+ Parcelable.ClassLoaderCreator<?> classLoaderCreator =
+ (Parcelable.ClassLoaderCreator<?>) creator;
+ return (T) classLoaderCreator.createFromParcel(this, loader);
}
return (T) creator.createFromParcel(this);
}
@@ -3687,6 +3889,28 @@
*/
@Nullable
public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
+ return readParcelableCreatorInternal(loader, /* clazz */ null);
+ }
+
+ /**
+ * Same as {@link #readParcelableCreator(ClassLoader)} but accepts {@code clazz} parameter
+ * as the required type. If the item to be deserialized is not an instance of that class
+ * or any of its children classes a {@link BadParcelableException} will be thrown.
+ */
+ @Nullable
+ public <T> Parcelable.Creator<T> readParcelableCreator(
+ @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
+ Objects.requireNonNull(clazz);
+ return readParcelableCreatorInternal(loader, clazz);
+ }
+
+ /**
+ * @param clazz The type of the parcelable expected or {@code null} for performing no checks.
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private <T> Parcelable.Creator<T> readParcelableCreatorInternal(
+ @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
String name = readString();
if (name == null) {
return null;
@@ -3702,7 +3926,15 @@
creator = map.get(name);
}
if (creator != null) {
- return creator;
+ if (clazz != null) {
+ Class<?> parcelableClass = creator.getClass().getEnclosingClass();
+ if (!clazz.isAssignableFrom(parcelableClass)) {
+ throw new BadParcelableException("Parcelable creator " + name + " is not "
+ + "a subclass of required class " + clazz.getName()
+ + " provided in the parameter");
+ }
+ }
+ return (Parcelable.Creator<T>) creator;
}
try {
@@ -3718,6 +3950,14 @@
throw new BadParcelableException("Parcelable protocol requires subclassing "
+ "from Parcelable on class " + name);
}
+ if (clazz != null) {
+ if (!clazz.isAssignableFrom(parcelableClass)) {
+ throw new BadParcelableException("Parcelable creator " + name + " is not "
+ + "a subclass of required class " + clazz.getName()
+ + " provided in the parameter");
+ }
+ }
+
Field f = parcelableClass.getField("CREATOR");
if ((f.getModifiers() & Modifier.STATIC) == 0) {
throw new BadParcelableException("Parcelable protocol requires "
@@ -3755,7 +3995,7 @@
map.put(name, creator);
}
- return creator;
+ return (Parcelable.Creator<T>) creator;
}
/**
@@ -4001,13 +4241,21 @@
return result;
}
- private void readListInternal(@NonNull List outVal, int N,
+ private void readListInternal(@NonNull List outVal, int n,
@Nullable ClassLoader loader) {
- while (N > 0) {
- Object value = readValue(loader);
+ readListInternal(outVal, n, loader, null);
+ }
+
+ /**
+ * @param clazz The type of the object expected or {@code null} for performing no checks.
+ */
+ private <T> void readListInternal(@NonNull List<? super T> outVal, int n,
+ @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
+ while (n > 0) {
+ T value = readValue(loader, clazz);
//Log.d(TAG, "Unmarshalling value=" + value);
outVal.add(value);
- N--;
+ n--;
}
}
@@ -4086,6 +4334,10 @@
case VAL_SIZE: return "VAL_SIZE";
case VAL_SIZEF: return "VAL_SIZEF";
case VAL_DOUBLEARRAY: return "VAL_DOUBLEARRAY";
+ case VAL_CHAR: return "VAL_CHAR";
+ case VAL_SHORTARRAY: return "VAL_SHORTARRAY";
+ case VAL_CHARARRAY: return "VAL_CHARARRAY";
+ case VAL_FLOATARRAY: return "VAL_FLOATARRAY";
case VAL_OBJECTARRAY: return "VAL_OBJECTARRAY";
case VAL_SERIALIZABLE: return "VAL_SERIALIZABLE";
default: return "UNKNOWN(" + type + ")";
diff --git a/core/java/android/os/Parcelable.java b/core/java/android/os/Parcelable.java
index a537c98..a396211 100644
--- a/core/java/android/os/Parcelable.java
+++ b/core/java/android/os/Parcelable.java
@@ -16,6 +16,7 @@
package android.os;
+import android.annotation.NonNull;
import android.annotation.IntDef;
import android.annotation.SystemApi;
@@ -202,7 +203,7 @@
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
*/
- public void writeToParcel(Parcel dest, @WriteFlags int flags);
+ public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags);
/**
* Interface that must be implemented and provided as a public CREATOR
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 755c35f..3739040 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -98,6 +98,10 @@
return mServiceManager.updatableViaApex(name);
}
+ public ConnectionInfo getConnectionInfo(String name) throws RemoteException {
+ return mServiceManager.getConnectionInfo(name);
+ }
+
public void registerClientCallback(String name, IBinder service, IClientCallback cb)
throws RemoteException {
throw new RemoteException();
diff --git a/core/java/android/os/VintfObject.java b/core/java/android/os/VintfObject.java
index bf0b655..1f11197 100644
--- a/core/java/android/os/VintfObject.java
+++ b/core/java/android/os/VintfObject.java
@@ -97,8 +97,11 @@
* ["android.hidl.manager@1.0", "android.hardware.camera.device@1.0",
* "android.hardware.camera.device@3.2"]. There are no duplicates.
*
- * For AIDL HALs, the version is stripped away
- * (e.g. "android.hardware.light").
+ * For AIDL HALs, the version is a single number
+ * (e.g. "android.hardware.light@1"). Historically, this API strips the
+ * version number for AIDL HALs (e.g. "android.hardware.light"). Users
+ * of this API must be able to handle both for backwards compatibility.
+ *
* @hide
*/
@TestApi
diff --git a/core/java/android/permission/OWNERS b/core/java/android/permission/OWNERS
index 19a3a8b..b5466b6 100644
--- a/core/java/android/permission/OWNERS
+++ b/core/java/android/permission/OWNERS
@@ -1,11 +1,12 @@
# Bug component: 137825
-eugenesusla@google.com
evanseverson@google.com
evanxinchen@google.com
ewol@google.com
guojing@google.com
jaysullivan@google.com
+olekarg@google.com
+pyuli@google.com
ntmyren@google.com
svetoslavganov@android.com
svetoslavganov@google.com
diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java
index 4cf0a36..418d92c 100644
--- a/core/java/android/util/ArrayMap.java
+++ b/core/java/android/util/ArrayMap.java
@@ -645,7 +645,7 @@
e.fillInStackTrace();
Log.w(TAG, "New hash " + hash
+ " is before end of array hash " + mHashes[index-1]
- + " at index " + index + " key " + key, e);
+ + " at index " + index + (DEBUG ? " key " + key : ""), e);
put(key, value);
return;
}
diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java
index ea39f6d..fcaeeff 100644
--- a/core/java/android/widget/QuickContactBadge.java
+++ b/core/java/android/widget/QuickContactBadge.java
@@ -396,8 +396,9 @@
// Prompt user to add this person to contacts
final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri);
if (extras != null) {
- extras.remove(EXTRA_URI_CONTENT);
- intent.putExtras(extras);
+ Bundle bundle = new Bundle(extras);
+ bundle.remove(EXTRA_URI_CONTENT);
+ intent.putExtras(bundle);
}
getContext().startActivity(intent);
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 14d3147..ac19121 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3026,6 +3026,11 @@
and one pSIM) -->
<integer name="config_num_physical_slots">1</integer>
+ <!-- When a radio power off request is received, we will delay completing the request until
+ either IMS moves to the deregistered state or the timeout defined by this configuration
+ elapses. If 0, this feature is disabled and we do not delay radio power off requests.-->
+ <integer name="config_delay_for_ims_dereg_millis">0</integer>
+
<!--Thresholds for LTE dbm in status bar-->
<integer-array translatable="false" name="config_lteDbmThresholds">
<item>-140</item> <!-- SIGNAL_STRENGTH_NONE_OR_UNKNOWN -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 2be5152..aebad6a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -476,6 +476,7 @@
<java-symbol type="string" name="config_deviceSpecificDevicePolicyManagerService" />
<java-symbol type="string" name="config_deviceSpecificAudioService" />
<java-symbol type="integer" name="config_num_physical_slots" />
+ <java-symbol type="integer" name="config_delay_for_ims_dereg_millis" />
<java-symbol type="array" name="config_integrityRuleProviderPackages" />
<java-symbol type="bool" name="config_useAssistantVolume" />
<java-symbol type="string" name="config_bandwidthEstimateSource" />
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index 4cc70ba..9d2cab3 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -16,16 +16,24 @@
package android.os;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import android.util.Log;
+
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Objects;
+
/**
* Unit tests for bundle that requires accessing hidden APS. Tests that can be written only with
* public APIs should go in the CTS counterpart.
@@ -35,6 +43,14 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
public class BundleTest {
+ private Log.TerribleFailureHandler mWtfHandler;
+
+ @After
+ public void tearDown() throws Exception {
+ if (mWtfHandler != null) {
+ Log.setWtfHandler(mWtfHandler);
+ }
+ }
/**
* Take a bundle, write it to a parcel and return the parcel.
@@ -217,4 +233,193 @@
// return true
assertTrue(BaseBundle.kindofEquals(bundle1, bundle2));
}
+
+ @Test
+ public void kindofEquals_lazyValues() {
+ Parcelable p1 = new CustomParcelable(13, "Tiramisu");
+ Parcelable p2 = new CustomParcelable(13, "Tiramisu");
+
+ // 2 maps with live objects
+ Bundle a = new Bundle();
+ a.putParcelable("key1", p1);
+ Bundle b = new Bundle();
+ b.putParcelable("key1", p2);
+ assertTrue(Bundle.kindofEquals(a, b));
+
+ // 2 identical parcels
+ a.readFromParcel(getParcelledBundle(a));
+ a.setClassLoader(getClass().getClassLoader());
+ b.readFromParcel(getParcelledBundle(b));
+ b.setClassLoader(getClass().getClassLoader());
+ assertTrue(Bundle.kindofEquals(a, b));
+
+ // 2 lazy values with identical parcels inside
+ a.isEmpty();
+ b.isEmpty();
+ assertTrue(Bundle.kindofEquals(a, b));
+
+ // 1 lazy value vs 1 live object
+ a.getParcelable("key1");
+ assertFalse(Bundle.kindofEquals(a, b));
+
+ // 2 live objects
+ b.getParcelable("key1");
+ assertTrue(Bundle.kindofEquals(a, b));
+ }
+
+ @Test
+ public void kindofEquals_lazyValuesWithIdenticalParcels_returnsTrue() {
+ Parcelable p1 = new CustomParcelable(13, "Tiramisu");
+ Parcelable p2 = new CustomParcelable(13, "Tiramisu");
+ Bundle a = new Bundle();
+ a.putParcelable("key", p1);
+ a.readFromParcel(getParcelledBundle(a));
+ a.setClassLoader(getClass().getClassLoader());
+ Bundle b = new Bundle();
+ b.putParcelable("key", p2);
+ b.readFromParcel(getParcelledBundle(b));
+ b.setClassLoader(getClass().getClassLoader());
+ // 2 lazy values with identical parcels inside
+ a.isEmpty();
+ b.isEmpty();
+
+ assertTrue(Bundle.kindofEquals(a, b));
+ }
+
+ @Test
+ public void kindofEquals_lazyValuesAndDifferentClassLoaders_returnsFalse() {
+ Parcelable p1 = new CustomParcelable(13, "Tiramisu");
+ Parcelable p2 = new CustomParcelable(13, "Tiramisu");
+ Bundle a = new Bundle();
+ a.putParcelable("key", p1);
+ a.readFromParcel(getParcelledBundle(a));
+ a.setClassLoader(getClass().getClassLoader());
+ Bundle b = new Bundle();
+ b.putParcelable("key", p2);
+ b.readFromParcel(getParcelledBundle(b));
+ b.setClassLoader(Bundle.class.getClassLoader()); // BCP
+ // 2 lazy values with identical parcels inside
+ a.isEmpty();
+ b.isEmpty();
+
+ assertFalse(Bundle.kindofEquals(a, b));
+ }
+
+ @Test
+ public void kindofEquals_lazyValuesOfDifferentTypes_returnsFalse() {
+ Parcelable p = new CustomParcelable(13, "Tiramisu");
+ Parcelable[] ps = {p};
+ Bundle a = new Bundle();
+ a.putParcelable("key", p);
+ a.readFromParcel(getParcelledBundle(a));
+ a.setClassLoader(getClass().getClassLoader());
+ Bundle b = new Bundle();
+ b.putParcelableArray("key", ps);
+ b.readFromParcel(getParcelledBundle(b));
+ b.setClassLoader(getClass().getClassLoader());
+ a.isEmpty();
+ b.isEmpty();
+
+ assertFalse(Bundle.kindofEquals(a, b));
+ }
+
+ @Test
+ public void kindofEquals_lazyValuesWithDifferentLengths_returnsFalse() {
+ Parcelable p1 = new CustomParcelable(13, "Tiramisu");
+ Parcelable p2 = new CustomParcelable(13, "Tiramisuuuuuuuu");
+ Bundle a = new Bundle();
+ a.putParcelable("key", p1);
+ a.readFromParcel(getParcelledBundle(a));
+ a.setClassLoader(getClass().getClassLoader());
+ Bundle b = new Bundle();
+ b.putParcelable("key", p2);
+ b.readFromParcel(getParcelledBundle(b));
+ b.setClassLoader(getClass().getClassLoader());
+ a.isEmpty();
+ b.isEmpty();
+
+ assertFalse(Bundle.kindofEquals(a, b));
+ }
+
+ @Test
+ public void readWriteLengthMismatch_logsWtf() throws Exception {
+ mWtfHandler = Log.setWtfHandler((tag, e, system) -> {
+ throw new RuntimeException(e);
+ });
+ Parcelable parcelable = new CustomParcelable(13, "Tiramisu").setHasLengthMismatch(true);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("p", parcelable);
+ bundle.readFromParcel(getParcelledBundle(bundle));
+ bundle.setClassLoader(getClass().getClassLoader());
+ RuntimeException e = assertThrows(RuntimeException.class, () -> bundle.getParcelable("p"));
+ assertThat(e.getCause()).isInstanceOf(Log.TerribleFailure.class);
+ }
+
+ private static class CustomParcelable implements Parcelable {
+ public final int integer;
+ public final String string;
+ public boolean hasLengthMismatch;
+
+ CustomParcelable(int integer, String string) {
+ this.integer = integer;
+ this.string = string;
+ }
+
+ protected CustomParcelable(Parcel in) {
+ integer = in.readInt();
+ string = in.readString();
+ hasLengthMismatch = in.readBoolean();
+ }
+
+ public CustomParcelable setHasLengthMismatch(boolean hasLengthMismatch) {
+ this.hasLengthMismatch = hasLengthMismatch;
+ return this;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(integer);
+ out.writeString(string);
+ out.writeBoolean(hasLengthMismatch);
+ if (hasLengthMismatch) {
+ out.writeString("extra-write");
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof CustomParcelable)) {
+ return false;
+ }
+ CustomParcelable
+ that = (CustomParcelable) other;
+ return integer == that.integer
+ && hasLengthMismatch == that.hasLengthMismatch
+ && string.equals(that.string);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(integer, string, hasLengthMismatch);
+ }
+
+ public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() {
+ @Override
+ public CustomParcelable createFromParcel(Parcel in) {
+ return new CustomParcelable(in);
+ }
+ @Override
+ public CustomParcelable[] newArray(int size) {
+ return new CustomParcelable[size];
+ }
+ };
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index a086918..70baf1d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
@@ -125,6 +126,9 @@
addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
+ addHandler(BluetoothCsipSetCoordinator.ACTION_CSIS_SET_MEMBER_AVAILABLE,
+ new SetMemberAvailableHandler());
+
registerAdapterIntentReceiver();
}
@@ -338,6 +342,12 @@
}
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
+
+ if (mDeviceManager.onBondStateChangedIfProcess(device, bondState)) {
+ Log.d(TAG, "Should not update UI for the set member");
+ return;
+ }
+
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.w(TAG, "Got bonding state changed for " + device +
@@ -351,8 +361,10 @@
cachedDevice.onBondingStateChanged(bondState);
if (bondState == BluetoothDevice.BOND_NONE) {
- /* Check if we need to remove other Hearing Aid devices */
- if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
+ // Check if we need to remove other Coordinated set member devices / Hearing Aid
+ // devices
+ if (cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+ || cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
mDeviceManager.onDeviceUnpaired(cachedDevice);
}
int reason = intent.getIntExtra(BluetoothDevice.EXTRA_REASON,
@@ -499,4 +511,29 @@
dispatchAudioModeChanged();
}
}
+
+ private class SetMemberAvailableHandler implements Handler {
+ @Override
+ public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ final String action = intent.getAction();
+ if (device == null) {
+ Log.e(TAG, "SetMemberAvailableHandler: device is null");
+ return;
+ }
+
+ if (action == null) {
+ Log.e(TAG, "SetMemberAvailableHandler: action is null");
+ return;
+ }
+
+ final int groupId = intent.getIntExtra(BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_ID,
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+ if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ Log.e(TAG, "SetMemberAvailableHandler: Invalid group id");
+ return;
+ }
+
+ mDeviceManager.onSetMemberAppear(device, groupId);
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 6a590c2..78fc139 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
@@ -41,7 +42,9 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -66,6 +69,7 @@
private final Object mProfileLock = new Object();
BluetoothDevice mDevice;
private long mHiSyncId;
+ private int mGroupId;
// Need this since there is no method for getting RSSI
short mRssi;
// mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is
@@ -100,6 +104,8 @@
private boolean mIsA2dpProfileConnectedFail = false;
private boolean mIsHeadsetProfileConnectedFail = false;
private boolean mIsHearingAidProfileConnectedFail = false;
+ // Group member devices for the coordinated set
+ private Set<CachedBluetoothDevice> mMemberDevices = new HashSet<CachedBluetoothDevice>();
// Group second device for Hearing Aid
private CachedBluetoothDevice mSubDevice;
@@ -133,6 +139,7 @@
mDevice = device;
fillData();
mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
+ mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
/**
@@ -317,6 +324,24 @@
return mIsCoordinatedSetMember;
}
+ /**
+ * Get the coordinated set group id.
+ *
+ * @return the group id.
+ */
+ public int getGroupId() {
+ return mGroupId;
+ }
+
+ /**
+ * Set the coordinated set group id.
+ *
+ * @param id the group id from the CSIP.
+ */
+ public void setGroupId(int id) {
+ mGroupId = id;
+ }
+
void onBondingDockConnect() {
// Attempt to connect if UUIDs are available. Otherwise,
// we will connect when the ACTION_UUID intent arrives.
@@ -1191,4 +1216,52 @@
mSubDevice.mJustDiscovered = tmpJustDiscovered;
fetchActiveDevices();
}
+
+ /**
+ * @return a set of member devices that are in the same coordinated set with this device.
+ */
+ public Set<CachedBluetoothDevice> getMemberDevice() {
+ return mMemberDevices;
+ }
+
+ /**
+ * Store the member devices that are in the same coordinated set.
+ */
+ public void setMemberDevice(CachedBluetoothDevice memberDevice) {
+ mMemberDevices.add(memberDevice);
+ }
+
+ /**
+ * Remove a device from the member device sets.
+ */
+ public void removeMemberDevice(CachedBluetoothDevice memberDevice) {
+ mMemberDevices.remove(memberDevice);
+ }
+
+ /**
+ * In order to show the preference for the whole group, we always set the main device as the
+ * first connected device in the coordinated set, and then switch the content of the main
+ * device and member devices.
+ *
+ * @param prevMainDevice the previous Main device, it will be added into the member device set.
+ * @param newMainDevie the new Main device, it will be removed from the member device set.
+ */
+ public void switchMemberDeviceContent(CachedBluetoothDevice prevMainDevice,
+ CachedBluetoothDevice newMainDevie) {
+ // Backup from main device
+ final BluetoothDevice tmpDevice = mDevice;
+ final short tmpRssi = mRssi;
+ final boolean tmpJustDiscovered = mJustDiscovered;
+ // Set main device from sub device
+ mDevice = newMainDevie.mDevice;
+ mRssi = newMainDevie.mRssi;
+ mJustDiscovered = newMainDevie.mJustDiscovered;
+ setMemberDevice(prevMainDevice);
+ mMemberDevices.remove(newMainDevie);
+ // Set sub device from backup
+ newMainDevie.mDevice = tmpDevice;
+ newMainDevie.mRssi = tmpRssi;
+ newMainDevie.mJustDiscovered = tmpJustDiscovered;
+ fetchActiveDevices();
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index cca9cfa..0256615 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
@@ -26,6 +27,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
/**
* CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
@@ -41,11 +43,15 @@
final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>();
@VisibleForTesting
HearingAidDeviceManager mHearingAidDeviceManager;
+ @VisibleForTesting
+ CsipDeviceManager mCsipDeviceManager;
+ BluetoothDevice mOngoingSetMemberPair;
CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
mContext = context;
mBtManager = localBtManager;
mHearingAidDeviceManager = new HearingAidDeviceManager(localBtManager, mCachedDevices);
+ mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
}
public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
@@ -79,7 +85,16 @@
if (cachedDevice.getDevice().equals(device)) {
return cachedDevice;
}
- // Check sub devices if it exists
+ // Check the member devices for the coordinated set if it exists
+ final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ if (memberDevice.getDevice().equals(device)) {
+ return memberDevice;
+ }
+ }
+ }
+ // Check sub devices for hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return subDevice;
@@ -102,8 +117,10 @@
newDevice = findDevice(device);
if (newDevice == null) {
newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
+ mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice);
- if (!mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
+ if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
+ && !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
mCachedDevices.add(newDevice);
mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
}
@@ -114,13 +131,23 @@
}
/**
- * Returns device summary of the pair of the hearing aid passed as the parameter.
+ * Returns device summary of the pair of the hearing aid / CSIP passed as the parameter.
*
* @param CachedBluetoothDevice device
- * @return Device summary, or if the pair does not exist or if it is not a hearing aid,
- * then {@code null}.
+ * @return Device summary, or if the pair does not exist or if it is not a hearing aid or
+ * a CSIP set member, then {@code null}.
*/
public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) {
+ final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ if (!memberDevice.isConnected()) {
+ return null;
+ }
+ }
+
+ return device.getConnectionSummary();
+ }
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null && subDevice.isConnected()) {
return subDevice.getConnectionSummary();
@@ -132,12 +159,22 @@
* Search for existing sub device {@link CachedBluetoothDevice}.
*
* @param device the address of the Bluetooth device
- * @return true for found sub device or false.
+ * @return true for found sub / member device or false.
*/
public synchronized boolean isSubDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (!cachedDevice.getDevice().equals(device)) {
- // Check sub devices if it exists
+ // Check the member devices of the coordinated set if it exists
+ Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ if (memberDevice.getDevice().equals(device)) {
+ return true;
+ }
+ }
+ continue;
+ }
+ // Check sub devices of hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return true;
@@ -157,6 +194,14 @@
}
/**
+ * Updates the Csip devices; specifically the GroupId's. This routine is called when the
+ * CSIS is connected and the GroupId's are now available.
+ */
+ public synchronized void updateCsipDevices() {
+ mCsipDeviceManager.updateCsipDevices();
+ }
+
+ /**
* Attempts to get the name of a remote device, otherwise returns the address.
*
* @param device The remote device.
@@ -185,6 +230,16 @@
private void clearNonBondedSubDevices() {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
+ final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ // Member device exists and it is not bonded
+ if (memberDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
+ cachedDevice.removeMemberDevice(memberDevice);
+ }
+ }
+ return;
+ }
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null
&& subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
@@ -201,6 +256,13 @@
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
cachedDevice.setJustDiscovered(false);
+ final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ memberDevice.setJustDiscovered(false);
+ }
+ return;
+ }
final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null) {
subDevice.setJustDiscovered(false);
@@ -214,10 +276,19 @@
if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
- CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
- if (subDevice != null) {
- if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
- cachedDevice.setSubDevice(null);
+ final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
+ if (memberDevices != null) {
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ if (memberDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
+ cachedDevice.removeMemberDevice(memberDevice);
+ }
+ }
+ } else {
+ CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
+ if (subDevice != null) {
+ if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
+ cachedDevice.setSubDevice(null);
+ }
}
}
if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
@@ -229,13 +300,32 @@
}
public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice
- cachedDevice, int state) {
- return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
+ cachedDevice, int state, int profileId) {
+ if (profileId == BluetoothProfile.HEARING_AID) {
+ return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
+ }
+ if (profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
+ return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
+ state);
+ }
+ return false;
}
public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
- CachedBluetoothDevice mainDevice = mHearingAidDeviceManager.findMainDevice(device);
+ CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
+ final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
+ if (memberDevices != null) {
+ // Main device is unpaired, to unpair the member device
+ for (CachedBluetoothDevice memberDevice : memberDevices) {
+ memberDevice.unpair();
+ device.removeMemberDevice(memberDevice);
+ }
+ } else if (mainDevice != null) {
+ // the member device unpaired, to unpair main device
+ mainDevice.unpair();
+ }
+ mainDevice = mHearingAidDeviceManager.findMainDevice(device);
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null) {
// Main device is unpaired, to unpair sub device
@@ -248,6 +338,74 @@
}
}
+ /**
+ * Called when we found a set member of a group. The function will check the {@code groupId} if
+ * it exists and if there is a ongoing pair, the device would be ignored.
+ *
+ * @param device The found device
+ * @param groupId The group id of the found device
+ */
+ public synchronized void onSetMemberAppear(BluetoothDevice device, int groupId) {
+ Log.d(TAG, "onSetMemberAppear, groupId: " + groupId + " device: " + device.toString());
+
+ if (mOngoingSetMemberPair != null) {
+ Log.d(TAG, "Ongoing set memberPairing in process, drop it!");
+ return;
+ }
+
+ if (mCsipDeviceManager.onSetMemberAppear(device, groupId)) {
+ mOngoingSetMemberPair = device;
+ }
+ }
+
+ /**
+ * Called when the bond state change. If the bond state change is related with the
+ * ongoing set member pair, the cachedBluetoothDevice will be created but the UI
+ * would not be updated. For the other case, return {@code false} to go through the normal
+ * flow.
+ *
+ * @param device The device
+ * @param bondState The new bond state
+ *
+ * @return {@code true}, if the bond state change for the device is handled inside this
+ * function, and would not like to update the UI. If not, return {@code false}.
+ */
+ public synchronized boolean onBondStateChangedIfProcess(BluetoothDevice device, int bondState) {
+ if (mOngoingSetMemberPair == null || !mOngoingSetMemberPair.equals(device)) {
+ return false;
+ }
+
+ if (bondState == BluetoothDevice.BOND_BONDING) {
+ return true;
+ }
+
+ mOngoingSetMemberPair = null;
+ if (bondState != BluetoothDevice.BOND_NONE) {
+ if (findDevice(device) == null) {
+ final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+ CachedBluetoothDevice newDevice =
+ new CachedBluetoothDevice(mContext, profileManager, device);
+ mCachedDevices.add(newDevice);
+ findDevice(device).connect();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the device is the one which is initial paired locally by CSIP. The setting
+ * would depned on it to accept the pairing request automatically
+ *
+ * @param device The device
+ *
+ * @return {@code true}, if the device is ongoing pair by CSIP. Otherwise, return
+ * {@code false}.
+ */
+ public boolean isOngoingPairByCsip(BluetoothDevice device) {
+ return !(mOngoingSetMemberPair == null) && mOngoingSetMemberPair.equals(device);
+ }
+
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
new file mode 100644
index 0000000..347e14b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -0,0 +1,270 @@
+/*
+ * 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.android.settingslib.bluetooth;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
+ */
+public class CsipDeviceManager {
+ private static final String TAG = "CsipDeviceManager";
+ private static final boolean DEBUG = BluetoothUtils.D;
+
+ private final LocalBluetoothManager mBtManager;
+ private final List<CachedBluetoothDevice> mCachedDevices;
+
+ CsipDeviceManager(LocalBluetoothManager localBtManager,
+ List<CachedBluetoothDevice> cachedDevices) {
+ mBtManager = localBtManager;
+ mCachedDevices = cachedDevices;
+ };
+
+ void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
+ // Current it only supports the base uuid for CSIP and group this set in UI.
+ final int groupId = getBaseGroupId(newDevice.getDevice());
+ if (isValidGroupId(groupId)) {
+ log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
+ // Once groupId is valid, assign groupId
+ newDevice.setGroupId(groupId);
+ }
+ }
+
+ private int getBaseGroupId(BluetoothDevice device) {
+ final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+ final CsipSetCoordinatorProfile profileProxy = profileManager
+ .getCsipSetCoordinatorProfile();
+ if (profileProxy != null) {
+ final Map<Integer, ParcelUuid> groupIdMap = profileProxy
+ .getGroupUuidMapByDevice(device);
+ if (groupIdMap == null) {
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
+ for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
+ if (entry.getValue().equals(BluetoothUuid.BASE_UUID)) {
+ return entry.getKey();
+ }
+ }
+ }
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
+ boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
+ final int groupId = newDevice.getGroupId();
+ if (isValidGroupId(groupId)) {
+ final CachedBluetoothDevice CsipDevice = getCachedDevice(groupId);
+ log("setMemberDeviceIfNeeded, main: " + CsipDevice + ", member: " + newDevice);
+ // Just add one of the coordinated set from a pair in the list that is shown in the UI.
+ // Once there is other devices with the same groupId, to add new device as member
+ // devices.
+ if (CsipDevice != null) {
+ CsipDevice.setMemberDevice(newDevice);
+ newDevice.setName(CsipDevice.getName());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isValidGroupId(int groupId) {
+ return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
+ private CachedBluetoothDevice getCachedDevice(int groupId) {
+ for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
+ CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
+ if (cachedDevice.getGroupId() == groupId) {
+ return cachedDevice;
+ }
+ }
+ return null;
+ }
+
+ // To collect all set member devices and call #onGroupIdChanged to group device by GroupId
+ void updateCsipDevices() {
+ final Set<Integer> newGroupIdSet = new HashSet<Integer>();
+ for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
+ // Do nothing if GroupId has been assigned
+ if (!isValidGroupId(cachedDevice.getGroupId())) {
+ final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
+ // Do nothing if there is no GroupId on Bluetooth device
+ if (isValidGroupId(newGroupId)) {
+ cachedDevice.setGroupId(newGroupId);
+ newGroupIdSet.add(newGroupId);
+ }
+ }
+ }
+ for (int groupId : newGroupIdSet) {
+ onGroupIdChanged(groupId);
+ }
+ }
+
+ // Group devices by groupId
+ @VisibleForTesting
+ void onGroupIdChanged(int groupId) {
+ int firstMatchedIndex = -1;
+ CachedBluetoothDevice mainDevice = null;
+
+ for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
+ final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
+ if (cachedDevice.getGroupId() != groupId) {
+ continue;
+ }
+
+ if (firstMatchedIndex == -1) {
+ // Found the first one
+ firstMatchedIndex = i;
+ mainDevice = cachedDevice;
+ continue;
+ }
+
+ log("onGroupIdChanged: removed from UI device =" + cachedDevice
+ + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
+
+ mainDevice.setMemberDevice(cachedDevice);
+ mCachedDevices.remove(i);
+ mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
+ break;
+ }
+ }
+
+ // @return {@code true}, the event is processed inside the method. It is for updating
+ // le audio device on group relationship when receiving connected or disconnected.
+ // @return {@code false}, it is not le audio device or to process it same as other profiles
+ boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
+ int state) {
+ log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
+ switch (state) {
+ case BluetoothProfile.STATE_CONNECTED:
+ onGroupIdChanged(cachedDevice.getGroupId());
+ CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
+ if (mainDevice != null) {
+ if (mainDevice.isConnected()) {
+ // When main device exists and in connected state, receiving member device
+ // connection. To refresh main device UI
+ mainDevice.refresh();
+ return true;
+ } else {
+ // When both LE Audio devices are disconnected, receiving member device
+ // connection. To switch content and dispatch to notify UI change
+ mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
+ mainDevice.switchMemberDeviceContent(mainDevice, cachedDevice);
+ mainDevice.refresh();
+ // It is necessary to do remove and add for updating the mapping on
+ // preference and device
+ mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
+ return true;
+ }
+ }
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ mainDevice = findMainDevice(cachedDevice);
+ if (mainDevice != null) {
+ // When main device exists, receiving sub device disconnection
+ // To update main device UI
+ mainDevice.refresh();
+ return true;
+ }
+ final Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
+ if (memberSet == null) {
+ break;
+ }
+
+ for (CachedBluetoothDevice device: memberSet) {
+ if (device.isConnected()) {
+ // Main device is disconnected and sub device is connected
+ // To copy data from sub device to main device
+ mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
+ cachedDevice.switchMemberDeviceContent(device, cachedDevice);
+ cachedDevice.refresh();
+ // It is necessary to do remove and add for updating the mapping on
+ // preference and device
+ mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
+ return true;
+ }
+ }
+ break;
+ default:
+ // Do not handle this state.
+ }
+ return false;
+ }
+
+ CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
+ if (device == null || mCachedDevices == null) {
+ return null;
+ }
+
+ for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
+ if (isValidGroupId(cachedDevice.getGroupId())) {
+ Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
+ if (memberSet == null) {
+ continue;
+ }
+
+ for (CachedBluetoothDevice memberDevice: memberSet) {
+ if (memberDevice != null && memberDevice.equals(device)) {
+ return cachedDevice;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called when we found a set member of a group. The function will check bond state, and
+ * the {@code groupId} if it exists, and then create the bond.
+ *
+ * @param device The found device
+ * @param groupId The group id of the found device
+ *
+ * @return {@code true}, if the we create bond with the device. Otherwise, return
+ * {@code false}.
+ */
+ public boolean onSetMemberAppear(BluetoothDevice device, int groupId) {
+ if (device.getBondState() != BluetoothDevice.BOND_NONE) {
+ return false;
+ }
+
+ if (getCachedDevice(groupId) != null) {
+ device.createBond(BluetoothDevice.TRANSPORT_LE);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void log(String msg) {
+ if (DEBUG) {
+ Log.d(TAG, msg);
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipSetCoordinatorProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipSetCoordinatorProfile.java
index 754914f..6da249c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipSetCoordinatorProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipSetCoordinatorProfile.java
@@ -26,6 +26,7 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
+import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
@@ -34,6 +35,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
/**
* CSIP Set Coordinator handles Bluetooth CSIP Set Coordinator role profile.
@@ -80,6 +82,7 @@
device.refresh();
}
+ mDeviceManager.updateCsipDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
@@ -212,6 +215,18 @@
}
/**
+ * Get the device's groups and correspondsing uuids map.
+ * @param device the bluetooth device
+ * @return Map of groups ids and related UUIDs
+ */
+ public Map<Integer, ParcelUuid> getGroupUuidMapByDevice(BluetoothDevice device) {
+ if (mService == null || device == null) {
+ return null;
+ }
+ return mService.getGroupUuidMapByDevice(device);
+ }
+
+ /**
* Return the profile name as a string.
*/
public String toString() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 25fd430..24113c5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -318,11 +318,35 @@
}
}
}
+
+ if (getCsipSetCoordinatorProfile() != null
+ && mProfile instanceof CsipSetCoordinatorProfile
+ && newState == BluetoothProfile.STATE_CONNECTED) {
+ // Check if the GroupID has being initialized
+ if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile()
+ .getGroupUuidMapByDevice(cachedDevice.getDevice());
+ if (groupIdMap != null) {
+ for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
+ if (entry.getValue().equals(BluetoothUuid.BASE_UUID)) {
+ cachedDevice.setGroupId(entry.getKey());
+ break;
+ }
+ }
+ }
+ }
+ }
+
cachedDevice.onProfileStateChanged(mProfile, newState);
// Dispatch profile changed after device update
- if (!(cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
- && mDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
- newState))) {
+ boolean needDispatchProfileConnectionState = true;
+ if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
+ || cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ needDispatchProfileConnectionState = !mDeviceManager
+ .onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
+ mProfile.getProfileId());
+ }
+ if (needDispatchProfileConnectionState) {
cachedDevice.refresh();
mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState,
mProfile.getProfileId());
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
index fd5b053..4f8fa2f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
@@ -206,7 +206,7 @@
mContext.sendBroadcast(mIntent);
verify(mDeviceManager).onProfileConnectionStateChangedIfProcessed(mCachedBluetoothDevice,
- BluetoothProfile.STATE_CONNECTED);
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.HEARING_AID);
}
/**
diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java
index a9f3a1b..462ed5c 100644
--- a/services/core/java/com/android/server/NsdService.java
+++ b/services/core/java/com/android/server/NsdService.java
@@ -82,6 +82,8 @@
private static final int INVALID_ID = 0;
private int mUniqueId = 1;
+ // The count of the connected legacy clients.
+ private int mLegacyClientCount = 0;
private class NsdStateMachine extends StateMachine {
@@ -107,7 +109,9 @@
sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs);
}
private void maybeScheduleStop() {
- if (!isAnyRequestActive()) {
+ // The native daemon should stay alive and can't be cleanup
+ // if any legacy client connected.
+ if (!isAnyRequestActive() && mLegacyClientCount == 0) {
scheduleStop();
}
}
@@ -175,11 +179,11 @@
if (cInfo != null) {
cInfo.expungeAllRequests();
mClients.remove(msg.replyTo);
+ if (cInfo.isLegacy()) {
+ mLegacyClientCount -= 1;
+ }
}
- //Last client
- if (mClients.size() == 0) {
- scheduleStop();
- }
+ maybeScheduleStop();
break;
case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
AsyncChannel ac = new AsyncChannel();
@@ -208,6 +212,17 @@
case NsdManager.DAEMON_CLEANUP:
mDaemon.maybeStop();
break;
+ // This event should be only sent by the legacy (target SDK < S) clients.
+ // Mark the sending client as legacy.
+ case NsdManager.DAEMON_STARTUP:
+ cInfo = mClients.get(msg.replyTo);
+ if (cInfo != null) {
+ cancelStop();
+ cInfo.setLegacy();
+ mLegacyClientCount += 1;
+ maybeStartDaemon();
+ }
+ break;
case NsdManager.NATIVE_DAEMON_EVENT:
default:
Slog.e(TAG, "Unhandled " + msg);
@@ -863,6 +878,9 @@
/* A map from client id to the type of the request we had received */
private final SparseIntArray mClientRequests = new SparseIntArray();
+ // The target SDK of this client < Build.VERSION_CODES.S
+ private boolean mIsLegacy = false;
+
private ClientInfo(AsyncChannel c, Messenger m) {
mChannel = c;
mMessenger = m;
@@ -875,6 +893,7 @@
sb.append("mChannel ").append(mChannel).append("\n");
sb.append("mMessenger ").append(mMessenger).append("\n");
sb.append("mResolvedService ").append(mResolvedService).append("\n");
+ sb.append("mIsLegacy ").append(mIsLegacy).append("\n");
for(int i = 0; i< mClientIds.size(); i++) {
int clientID = mClientIds.keyAt(i);
sb.append("clientId ").append(clientID).
@@ -884,6 +903,14 @@
return sb.toString();
}
+ private boolean isLegacy() {
+ return mIsLegacy;
+ }
+
+ private void setLegacy() {
+ mIsLegacy = true;
+ }
+
// Remove any pending requests from the global map when we get rid of a client,
// and send cancellations to the daemon.
private void expungeAllRequests() {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index c24973d..8926e20 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -242,7 +242,7 @@
mValidationInfo.append(opcode, new ValidationInfo(validator, addrType));
}
- int isValid(HdmiCecMessage message) {
+ int isValid(HdmiCecMessage message, boolean isMessageReceived) {
int opcode = message.getOpcode();
ValidationInfo info = mValidationInfo.get(opcode);
if (info == null) {
@@ -256,6 +256,22 @@
HdmiLogger.warning("Unexpected source: " + message);
return ERROR_SOURCE;
}
+
+ if (isMessageReceived) {
+ // Check if the source's logical address and local device's logical
+ // address are the same.
+ for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
+ synchronized (device.mLock) {
+ if (message.getSource() == device.getDeviceInfo().getLogicalAddress()
+ && message.getSource() != Constants.ADDR_UNREGISTERED) {
+ HdmiLogger.warning(
+ "Unexpected source: message sent from device itself, " + message);
+ return ERROR_SOURCE;
+ }
+ }
+ }
+ }
+
// Check the destination field.
if (message.getDestination() == Constants.ADDR_BROADCAST) {
if ((info.addressType & DEST_BROADCAST) == 0) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index cca8be8..b049d01 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -556,6 +556,12 @@
// on boot, if device is interactive, set HDMI CEC state as powered on as well
if (mPowerManager.isInteractive() && isPowerStandbyOrTransient()) {
mPowerStatus = HdmiControlManager.POWER_STATUS_ON;
+ // Start all actions that were queued because the device was in standby
+ if (mAddressAllocated) {
+ for (HdmiCecLocalDevice localDevice : getAllLocalDevices()) {
+ localDevice.startQueuedActions();
+ }
+ }
}
}
@@ -1122,7 +1128,7 @@
@ServiceThreadOnly
void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
assertRunOnServiceThread();
- if (mMessageValidator.isValid(command) == HdmiCecMessageValidator.OK) {
+ if (mMessageValidator.isValid(command, false) == HdmiCecMessageValidator.OK) {
mCecController.sendCommand(command, callback);
} else {
HdmiLogger.error("Invalid message type:" + command);
@@ -1153,7 +1159,7 @@
@ServiceThreadOnly
boolean handleCecCommand(HdmiCecMessage message) {
assertRunOnServiceThread();
- int errorCode = mMessageValidator.isValid(message);
+ int errorCode = mMessageValidator.isValid(message, true);
if (errorCode != HdmiCecMessageValidator.OK) {
// We'll not response on the messages with the invalid source or destination
// or with parameter length shorter than specified in the standard.
@@ -3353,8 +3359,8 @@
invokeInputChangeListener(info);
}
- void setMhlInputChangeEnabled(boolean enabled) {
- mMhlController.setOption(OPTION_MHL_INPUT_SWITCHING, toInt(enabled));
+ void setMhlInputChangeEnabled(boolean enabled) {
+ mMhlController.setOption(OPTION_MHL_INPUT_SWITCHING, toInt(enabled));
synchronized (mLock) {
mMhlInputChangeEnabled = enabled;
diff --git a/services/net/java/android/net/ConnectivityModuleConnector.java b/services/net/java/android/net/ConnectivityModuleConnector.java
index 62f2c35..c6b15c1 100644
--- a/services/net/java/android/net/ConnectivityModuleConnector.java
+++ b/services/net/java/android/net/ConnectivityModuleConnector.java
@@ -278,7 +278,10 @@
// This code path is only run by the system server: only the system server binds
// to the NetworkStack as a service. Other processes get the NetworkStack from
// the ServiceManager.
- maybeCrashWithTerribleFailure("Lost network stack", mPackageName);
+ maybeCrashWithTerribleFailure(
+ "Lost network stack. This is not the root cause of any issue, it is a side "
+ + "effect of a crash that happened earlier. Earlier logs should point to the "
+ + "actual issue.", mPackageName);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index 3e5cbea..c45d084 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -125,7 +125,7 @@
mMessageValidator =
new HdmiCecMessageValidator(mHdmiControlService) {
@Override
- int isValid(HdmiCecMessage message) {
+ int isValid(HdmiCecMessage message, boolean isMessageReceived) {
return HdmiCecMessageValidator.OK;
}
};
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index ae7f422..ae99dab 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -629,7 +629,7 @@
}
private IntegerSubject assertMessageValidity(String message) {
- return assertThat(mHdmiCecMessageValidator.isValid(buildMessage(message)));
+ return assertThat(mHdmiCecMessageValidator.isValid(buildMessage(message), false));
}
/**
diff --git a/telephony/java/android/telephony/SignalStrengthUpdateRequest.java b/telephony/java/android/telephony/SignalStrengthUpdateRequest.java
index fe7e5976..41e24dd 100644
--- a/telephony/java/android/telephony/SignalStrengthUpdateRequest.java
+++ b/telephony/java/android/telephony/SignalStrengthUpdateRequest.java
@@ -26,8 +26,10 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -101,9 +103,11 @@
}
mSignalThresholdInfos = new ArrayList<>(signalThresholdInfos);
- // Sort the collection with RAN ascending order, make the ordering not matter for equals
+ // Sort the collection with RAN and then SignalMeasurementType ascending order, make the
+ // ordering not matter for equals
mSignalThresholdInfos.sort(
- Comparator.comparingInt(SignalThresholdInfo::getRadioAccessNetworkType));
+ Comparator.comparingInt(SignalThresholdInfo::getRadioAccessNetworkType)
+ .thenComparing(SignalThresholdInfo::getSignalMeasurementType));
return this;
}
@@ -144,7 +148,7 @@
* @return the SignalStrengthUpdateRequest object
*
* @throws IllegalArgumentException if the SignalThresholdInfo collection is empty size, the
- * radio access network type in the collection is not unique
+ * signal measurement type for the same RAN in the collection is not unique
*/
public @NonNull SignalStrengthUpdateRequest build() {
return new SignalStrengthUpdateRequest(mSignalThresholdInfos,
@@ -258,14 +262,23 @@
}
/**
- * Throw IAE when the RAN in the collection is not unique.
+ * Throw IAE if SignalThresholdInfo collection is null or empty,
+ * or the SignalMeasurementType for the same RAN in the collection is not unique.
*/
private static void validate(Collection<SignalThresholdInfo> infos) {
- Set<Integer> uniqueRan = new HashSet<>(infos.size());
+ if (infos == null || infos.isEmpty()) {
+ throw new IllegalArgumentException("SignalThresholdInfo collection is null or empty");
+ }
+
+ // Map from RAN to set of SignalMeasurementTypes
+ Map<Integer, Set<Integer>> ranToTypes = new HashMap<>(infos.size());
for (SignalThresholdInfo info : infos) {
final int ran = info.getRadioAccessNetworkType();
- if (!uniqueRan.add(ran)) {
- throw new IllegalArgumentException("RAN: " + ran + " is not unique");
+ final int type = info.getSignalMeasurementType();
+ ranToTypes.putIfAbsent(ran, new HashSet<>());
+ if (!ranToTypes.get(ran).add(type)) {
+ throw new IllegalArgumentException(
+ "SignalMeasurementType " + type + " for RAN " + ran + " is not unique");
}
}
}
diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp
index 840a588..086ef95 100644
--- a/tests/StagedInstallTest/Android.bp
+++ b/tests/StagedInstallTest/Android.bp
@@ -50,7 +50,7 @@
"cts-install-lib-host",
],
data: [
- ":com.android.apex.cts.shim.v2_prebuilt",
+ ":StagedInstallTestApexV2",
":TestAppAv1",
],
test_suites: ["general-tests"],