diff options
42 files changed, 1805 insertions, 284 deletions
diff --git a/.gitignore b/.gitignore index c47cc8bf2538..2dffb7a8697d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.project *.iml *.sw* gen/
\ No newline at end of file diff --git a/Android.bp b/Android.bp index 34b21d663dd6..1fbdc151867d 100644 --- a/Android.bp +++ b/Android.bp @@ -118,7 +118,6 @@ filegroup { ":libbluetooth-binder-aidl", ":libcamera_client_aidl", ":libcamera_client_framework_aidl", - ":packagemanager_aidl", ":libupdate_engine_aidl", ":resourcemanager_aidl", ":storaged_aidl", @@ -209,6 +208,7 @@ java_library { 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 @@ filegroup { 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", diff --git a/ApiDocs.bp b/ApiDocs.bp index 65a6547b4493..17676787375c 100644 --- a/ApiDocs.bp +++ b/ApiDocs.bp @@ -77,22 +77,16 @@ stubs_defaults { // 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 @@ stubs_defaults { merge_annotations_dirs: [ "metalava-manual", ], + write_sdk_values: true, } droidstubs { @@ -114,6 +109,24 @@ droidstubs { 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 { 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 91e207433f3d..c967e51e16f9 100644 --- a/apct-tests/perftests/multiuser/Android.bp +++ b/apct-tests/perftests/multiuser/Android.bp @@ -31,5 +31,6 @@ android_test { ], 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 9117561fe1e5..bec3cc9272c3 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 000000000000..93b06e84e130 --- /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 8dff60af8bbd..0acd759bc73e 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -24,9 +24,8 @@ package { 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 { }, } +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,17 +50,28 @@ python_test_host { "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, }, } diff --git a/api/api_versions_trimmer_unittests.py b/api/api_versions_trimmer_unittests.py index 4eb929ea1b5d..d2e5b7d1a07e 100644 --- a/api/api_versions_trimmer_unittests.py +++ b/api/api_versions_trimmer_unittests.py @@ -304,4 +304,4 @@ sultCallback;Landroid/os/Handler;)Z" since="24"/> 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 000000000000..9c67d7bded76 --- /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 000000000000..26795c47af9e --- /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 e4e7d4eec77d..5c072541f974 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -29976,6 +29976,7 @@ package android.os { method public boolean hasFileDescriptors(); method public byte[] marshall(); method @NonNull public static android.os.Parcel obtain(); + method @NonNull public static android.os.Parcel obtain(@NonNull android.os.IBinder); method @Nullable public Object[] readArray(@Nullable ClassLoader); method @Nullable public java.util.ArrayList readArrayList(@Nullable ClassLoader); method public void readBinderArray(@NonNull android.os.IBinder[]); @@ -29998,12 +29999,15 @@ package android.os { 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 +30146,7 @@ package android.os { 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 949ba6ac419a..3b2d88318b76 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -332,12 +332,7 @@ filegroup { filegroup { name: "framework-cellbroadcast-shared-srcs", srcs: [ - "android/os/HandlerExecutor.java", "android/util/LocalLog.java", - "com/android/internal/util/IState.java", - "com/android/internal/util/Preconditions.java", - "com/android/internal/util/State.java", - "com/android/internal/util/StateMachine.java", ], } diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index 5a25cfc00cd4..ae8d0101947d 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -23,6 +23,9 @@ import static com.android.internal.util.Preconditions.checkStringNotEmpty; 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 @@ public final class NsdManager { 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 final class NsdManager { 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 @@ public final class NsdManager { 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 @@ public final class NsdManager { } 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 6da02f5c9ff5..7ce8d7267de2 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -260,6 +260,9 @@ public class BaseBundle { /** * 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 @@ public class BaseBundle { /** * 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 @@ public class BaseBundle { * * @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 @@ public class BaseBundle { */ 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 @@ public class BaseBundle { @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 @@ public class BaseBundle { @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 @@ public class BaseBundle { @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 a870c04f9cd3..c575c80e41fc 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -69,3 +69,6 @@ per-file Bugreport* = file:/platform/frameworks/native:/cmds/dumpstate/OWNERS # 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 a2716d211bbf..358aa29eaea6 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 @@ public final class Parcel { 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,21 @@ public final class Parcel { } /** + * 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. + */ + @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 +538,9 @@ public final class Parcel { } /** - * 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 +1321,46 @@ public final class Parcel { } } + /** @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 +2026,14 @@ public final class Parcel { 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 +2136,18 @@ public final class Parcel { 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 +2828,20 @@ public final class Parcel { */ 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 +3040,7 @@ public final class Parcel { return null; } ArrayList l = new ArrayList(N); - readListInternal(l, N, loader); + readListInternal(l, N, loader, /* clazz */ null); return l; } @@ -3353,20 +3440,29 @@ public final class Parcel { */ @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 +3499,7 @@ public final class Parcel { 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 +3508,19 @@ public final class Parcel { 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 +3529,41 @@ public final class Parcel { @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 +3582,224 @@ public final class Parcel { 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: + object = readCharSequenceArray(); + break; + + case VAL_IBINDER: + object = readStrongBinder(); + break; + + case VAL_OBJECTARRAY: + object = readArray(loader); + break; - case VAL_CHARSEQUENCEARRAY: - return readCharSequenceArray(); + case VAL_INTARRAY: + object = createIntArray(); + break; - case VAL_IBINDER: - return readStrongBinder(); + case VAL_LONGARRAY: + object = createLongArray(); + break; - case VAL_OBJECTARRAY: - return readArray(loader); + case VAL_BYTE: + object = readByte(); + break; - case VAL_INTARRAY: - return createIntArray(); + case VAL_SERIALIZABLE: + object = readSerializable(loader); + break; + + case VAL_PARCELABLEARRAY: + object = readParcelableArray(loader); + break; - case VAL_LONGARRAY: - return createLongArray(); + case VAL_SPARSEARRAY: + object = readSparseArray(loader); + break; - case VAL_BYTE: - return readByte(); + case VAL_SPARSEBOOLEANARRAY: + object = readSparseBooleanArray(); + break; - case VAL_SERIALIZABLE: - return readSerializable(loader); + case VAL_BUNDLE: + object = readBundle(loader); // loading will be deferred + break; - case VAL_PARCELABLEARRAY: - return readParcelableArray(loader); + case VAL_PERSISTABLEBUNDLE: + object = readPersistableBundle(loader); + break; - case VAL_SPARSEARRAY: - return readSparseArray(loader); + case VAL_SIZE: + object = readSize(); + break; - case VAL_SPARSEBOOLEANARRAY: - return readSparseBooleanArray(); + case VAL_SIZEF: + object = readSizeF(); + break; - case VAL_BUNDLE: - return readBundle(loader); // loading will be deferred + case VAL_DOUBLEARRAY: + object = createDoubleArray(); + break; - case VAL_PERSISTABLEBUNDLE: - return readPersistableBundle(loader); + case VAL_CHAR: + object = (char) readInt(); + break; - case VAL_SIZE: - return readSize(); + case VAL_SHORTARRAY: + object = createShortArray(); + break; - case VAL_SIZEF: - return readSizeF(); + case VAL_CHARARRAY: + object = createCharArray(); + break; - case VAL_DOUBLEARRAY: - return createDoubleArray(); + case VAL_FLOATARRAY: + object = createFloatArray(); + break; - default: - int off = dataPosition() - 4; - throw new RuntimeException( - "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off); + 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 +3818,42 @@ public final class Parcel { * @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 +3887,28 @@ public final class Parcel { */ @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 +3924,15 @@ public final class Parcel { 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 +3948,14 @@ public final class Parcel { 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 +3993,7 @@ public final class Parcel { map.put(name, creator); } - return creator; + return (Parcelable.Creator<T>) creator; } /** @@ -4001,13 +4239,21 @@ public final class Parcel { 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 +4332,10 @@ public final class Parcel { 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 a537c98fc497..a3962117279e 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 @@ public interface Parcelable { * @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 755c35f4767a..3739040eef60 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -98,6 +98,10 @@ class ServiceManagerProxy implements IServiceManager { 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 bf0b655fe574..1f11197afeee 100644 --- a/core/java/android/os/VintfObject.java +++ b/core/java/android/os/VintfObject.java @@ -97,8 +97,11 @@ public class VintfObject { * ["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 19a3a8bd514a..b5466b6d3cb5 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 4cf0a36d24bb..418d92c44290 100644 --- a/core/java/android/util/ArrayMap.java +++ b/core/java/android/util/ArrayMap.java @@ -645,7 +645,7 @@ public final class ArrayMap<K, V> implements Map<K, V> { 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 ea39f6d71573..fcaeeffe8e99 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -396,8 +396,9 @@ public class QuickContactBadge extends ImageView implements OnClickListener { // 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 14d314775482..ac1912143dd7 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 2be5152ce2d9..aebad6afc042 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 4cc70ba6448d..9d2cab3f7026 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 @@ import org.junit.runner.RunWith; @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 @@ public class BundleTest { // 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/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index e08b99d52cdb..d897e94f969f 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -32,6 +32,8 @@ using namespace android::uirenderer::renderthread; +static constexpr bool sEnableExtraCropInset = true; + namespace android { namespace uirenderer { @@ -71,6 +73,20 @@ CopyResult Readback::copySurfaceInto(ANativeWindow* window, const Rect& inSrcRec ALOGW("Surface doesn't have any previously queued frames, nothing to readback from"); return CopyResult::SourceEmpty; } + + if (sEnableExtraCropInset && + (cropRect.right - cropRect.left != bitmap->width() || + cropRect.bottom - cropRect.top != bitmap->height())) { + /* + * When we need use filtering, we should also make border shrink here like gui. + * But we could not check format for YUV or RGB here... Just use 1 pix. + */ + cropRect.left += 0.5f; + cropRect.top += 0.5f; + cropRect.right -= 0.5f; + cropRect.bottom -= 0.5f; + } + UniqueAHardwareBuffer sourceBuffer{rawSourceBuffer}; AHardwareBuffer_Desc description; AHardwareBuffer_describe(sourceBuffer.get(), &description); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index a0869189a5bd..70baf1d84470 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 @@ package com.android.settingslib.bluetooth; 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 @@ public class BluetoothEventManager { 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 @@ public class BluetoothEventManager { } 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 @@ public class BluetoothEventManager { 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 @@ public class BluetoothEventManager { 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 6a590c2ff382..78fc139894d1 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 @@ package com.android.settingslib.bluetooth; 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 com.android.settingslib.Utils; 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mDevice = device; fillData(); mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; + mGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; } /** @@ -317,6 +324,24 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> 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 cca9cfac2d22..02566155f1c9 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 @@ package com.android.settingslib.bluetooth; 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 com.android.internal.annotations.VisibleForTesting; 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 @@ public class CachedBluetoothDeviceManager { 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 @@ public class CachedBluetoothDeviceManager { 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 @@ public class CachedBluetoothDeviceManager { 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 @@ public class CachedBluetoothDeviceManager { } /** - * 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 @@ public class CachedBluetoothDeviceManager { * 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 @@ public class CachedBluetoothDeviceManager { } /** + * 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 @@ public class CachedBluetoothDeviceManager { 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 @@ public class CachedBluetoothDeviceManager { 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 @@ public class CachedBluetoothDeviceManager { 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 class CachedBluetoothDeviceManager { } 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 @@ public class CachedBluetoothDeviceManager { } } + /** + * 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 000000000000..347e14b91656 --- /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 754914ff794f..6da249c2980e 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.BluetoothCsipSetCoordinator; 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 com.android.settingslib.R; 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 @@ public class CsipSetCoordinatorProfile implements LocalBluetoothProfile { device.refresh(); } + mDeviceManager.updateCsipDevices(); mProfileManager.callServiceConnectedListeners(); mIsProfileReady = true; } @@ -212,6 +215,18 @@ public class CsipSetCoordinatorProfile implements LocalBluetoothProfile { } /** + * 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 25fd430372f2..24113c5a0261 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 @@ public class LocalBluetoothProfileManager { } } } + + 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 fd5b05311247..4f8fa2fdb96e 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 @@ public class LocalBluetoothProfileManagerTest { 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 a9f3a1b63b40..462ed5c70d36 100644 --- a/services/core/java/com/android/server/NsdService.java +++ b/services/core/java/com/android/server/NsdService.java @@ -82,6 +82,8 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { /* 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 c24973d4f2d4..8926e20ae5ea 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 @@ public class HdmiCecMessageValidator { 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 @@ public class HdmiCecMessageValidator { 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 cca8be8a1bea..b049d012f0ae 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 @@ public class HdmiControlService extends SystemService { // 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 @@ public class HdmiControlService extends SystemService { @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 @@ public class HdmiControlService extends SystemService { @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 @@ public class HdmiControlService extends SystemService { 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 62f2c35f339b..c6b15c17bd3c 100644 --- a/services/net/java/android/net/ConnectivityModuleConnector.java +++ b/services/net/java/android/net/ConnectivityModuleConnector.java @@ -278,7 +278,10 @@ public class ConnectivityModuleConnector { // 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 3e5cbea6a2a4..c45d084ac7c9 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 @@ public class HdmiCecLocalDeviceTest { 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 ae7f422817e5..ae99dab6ed4e 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 @@ public class HdmiCecMessageValidatorTest { } private IntegerSubject assertMessageValidity(String message) { - return assertThat(mHdmiCecMessageValidator.isValid(buildMessage(message))); + return assertThat(mHdmiCecMessageValidator.isValid(buildMessage(message), false)); } /** diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/.project b/tests/Camera2Tests/SmartCamera/SimpleCamera/.project deleted file mode 100644 index 2517e2d4f93d..000000000000 --- a/tests/Camera2Tests/SmartCamera/SimpleCamera/.project +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<projectDescription> - <name>CameraShoot</name> - <comment></comment> - <projects> - </projects> - <buildSpec> - <buildCommand> - <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>org.eclipse.jdt.core.javabuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>com.android.ide.eclipse.adt.ApkBuilder</name> - <arguments> - </arguments> - </buildCommand> - </buildSpec> - <natures> - <nature>com.android.ide.eclipse.adt.AndroidNature</nature> - <nature>org.eclipse.jdt.core.javanature</nature> - </natures> -</projectDescription> diff --git a/tests/HwAccelerationTest/.project b/tests/HwAccelerationTest/.project deleted file mode 100644 index 7c04d3cb6426..000000000000 --- a/tests/HwAccelerationTest/.project +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<projectDescription> - <name>HwAccelerationTest</name> - <comment></comment> - <projects> - </projects> - <buildSpec> - <buildCommand> - <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>org.eclipse.jdt.core.javabuilder</name> - <arguments> - </arguments> - </buildCommand> - <buildCommand> - <name>com.android.ide.eclipse.adt.ApkBuilder</name> - <arguments> - </arguments> - </buildCommand> - </buildSpec> - <natures> - <nature>com.android.ide.eclipse.adt.AndroidNature</nature> - <nature>org.eclipse.jdt.core.javanature</nature> - </natures> -</projectDescription> diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp index 840a588bfe88..086ef95877e6 100644 --- a/tests/StagedInstallTest/Android.bp +++ b/tests/StagedInstallTest/Android.bp @@ -50,7 +50,7 @@ java_test_host { "cts-install-lib-host", ], data: [ - ":com.android.apex.cts.shim.v2_prebuilt", + ":StagedInstallTestApexV2", ":TestAppAv1", ], test_suites: ["general-tests"], |