Merge "Clear transform when updating animation frame" into main
diff --git a/cmds/gpu_counter_producer/main.cpp b/cmds/gpu_counter_producer/main.cpp
index 1054cba..4616638 100644
--- a/cmds/gpu_counter_producer/main.cpp
+++ b/cmds/gpu_counter_producer/main.cpp
@@ -133,6 +133,12 @@
daemon(0, 0);
}
+ if (getenv("LD_LIBRARY_PATH") == nullptr) {
+ setenv("LD_LIBRARY_PATH", "/vendor/lib64:/vendor/lib", 0 /*override*/);
+ LOG_INFO("execv with: LD_LIBRARY_PATH=%s", getenv("LD_LIBRARY_PATH"));
+ execvpe(pname, argv, environ);
+ }
+
if (!writeToPidFile()) {
LOG_ERR("Could not open %s", kPidFileName);
return 1;
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index 6001155..2b5fcd8 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -24,8 +24,8 @@
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.LinearLayout;
-import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.ColorInt;
@@ -41,9 +41,9 @@
* This component is used as the main switch of the page
* to enable or disable the prefereces on the page.
*/
-public class MainSwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener {
+public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListener {
- private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+ private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
@ColorInt
private int mBackgroundColor;
@@ -51,8 +51,8 @@
private int mBackgroundActivatedColor;
protected TextView mTextView;
- protected Switch mSwitch;
- private View mFrameView;
+ protected CompoundButton mSwitch;
+ private final View mFrameView;
public MainSwitchBar(Context context) {
this(context, null);
@@ -84,8 +84,8 @@
setClickable(true);
mFrameView = findViewById(R.id.frame);
- mTextView = (TextView) findViewById(R.id.switch_text);
- mSwitch = (Switch) findViewById(android.R.id.switch_widget);
+ mTextView = findViewById(R.id.switch_text);
+ mSwitch = findViewById(android.R.id.switch_widget);
addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
if (mSwitch.getVisibility() == VISIBLE) {
@@ -136,13 +136,6 @@
}
/**
- * Return the Switch
- */
- public final Switch getSwitch() {
- return mSwitch;
- }
-
- /**
* Set the title text
*/
public void setTitle(CharSequence text) {
@@ -192,7 +185,7 @@
/**
* Adds a listener for switch changes
*/
- public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+ public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
if (!mSwitchChangeListeners.contains(listener)) {
mSwitchChangeListeners.add(listener);
}
@@ -201,7 +194,7 @@
/**
* Remove a listener for switch changes
*/
- public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+ public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
mSwitchChangeListeners.remove(listener);
}
@@ -223,9 +216,8 @@
private void propagateChecked(boolean isChecked) {
setBackground(isChecked);
- final int count = mSwitchChangeListeners.size();
- for (int n = 0; n < count; n++) {
- mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked);
+ for (OnCheckedChangeListener changeListener : mSwitchChangeListeners) {
+ changeListener.onCheckedChanged(mSwitch, isChecked);
}
}
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index 11a6804..b294d4e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -19,24 +19,25 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
-import android.widget.Switch;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.TwoStatePreference;
+import com.android.settingslib.widget.mainswitch.R;
+
import java.util.ArrayList;
import java.util.List;
-import com.android.settingslib.widget.mainswitch.R;
-
/**
* MainSwitchPreference is a Preference with a customized Switch.
* This component is used as the main switch of the page
* to enable or disable the prefereces on the page.
*/
-public class MainSwitchPreference extends TwoStatePreference implements OnMainSwitchChangeListener {
+public class MainSwitchPreference extends TwoStatePreference implements OnCheckedChangeListener {
- private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+ private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
private MainSwitchBar mMainSwitchBar;
@@ -120,7 +121,7 @@
}
@Override
- public void onSwitchChanged(Switch switchView, boolean isChecked) {
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
super.setChecked(isChecked);
}
@@ -138,7 +139,7 @@
/**
* Adds a listener for switch changes
*/
- public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+ public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
if (!mSwitchChangeListeners.contains(listener)) {
mSwitchChangeListeners.add(listener);
}
@@ -151,7 +152,7 @@
/**
* Remove a listener for switch changes
*/
- public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+ public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
mSwitchChangeListeners.remove(listener);
if (mMainSwitchBar != null) {
mMainSwitchBar.removeOnSwitchChangeListener(listener);
@@ -159,7 +160,7 @@
}
private void registerListenerToSwitchBar() {
- for (OnMainSwitchChangeListener listener : mSwitchChangeListeners) {
+ for (OnCheckedChangeListener listener : mSwitchChangeListeners) {
mMainSwitchBar.addOnSwitchChangeListener(listener);
}
}
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
deleted file mode 100644
index 03868f9..0000000
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 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.widget;
-
-import android.widget.Switch;
-
-import com.android.settingslib.widget.mainswitch.R;
-
-/**
- * Called when the checked state of the Switch has changed.
- */
-public interface OnMainSwitchChangeListener {
- /**
- * @param switchView The Switch view whose state has changed.
- * @param isChecked The new checked state of switchView.
- */
- void onSwitchChanged(Switch switchView, boolean isChecked);
-}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt
new file mode 100644
index 0000000..2c60db4
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * A [BroadcastReceiver] flow for the given [intentFilter].
+ */
+fun Context.broadcastReceiverAsUserFlow(
+ intentFilter: IntentFilter,
+ userHandle: UserHandle,
+): Flow<Intent> = callbackFlow {
+ val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ trySend(intent)
+ }
+ }
+ registerReceiverAsUser(
+ broadcastReceiver,
+ userHandle,
+ intentFilter,
+ null,
+ null,
+ Context.RECEIVER_NOT_EXPORTED,
+ )
+
+ awaitClose { unregisterReceiver(broadcastReceiver) }
+}.conflate().flowOn(Dispatchers.Default)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index ad907cf..7d6ee19 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -17,14 +17,14 @@
package com.android.settingslib.spaprivileged.framework.compose
import android.content.BroadcastReceiver
-import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
-import com.android.settingslib.spa.framework.compose.LifecycleEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
+import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow
/**
* A [BroadcastReceiver] which registered when on start and unregistered when on stop.
@@ -35,27 +35,6 @@
userHandle: UserHandle,
onReceive: (Intent) -> Unit,
) {
- val context = LocalContext.current
- val broadcastReceiver = remember {
- object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- onReceive(intent)
- }
- }
- }
- LifecycleEffect(
- onStart = {
- context.registerReceiverAsUser(
- broadcastReceiver,
- userHandle,
- intentFilter,
- null,
- null,
- Context.RECEIVER_NOT_EXPORTED,
- )
- },
- onStop = {
- context.unregisterReceiver(broadcastReceiver)
- },
- )
+ LocalContext.current.broadcastReceiverAsUserFlow(intentFilter, userHandle)
+ .collectLatestWithLifecycle(LocalLifecycleOwner.current, action = onReceive)
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt
new file mode 100644
index 0000000..dfb8e22
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+class BroadcastReceiverAsUserFlowTest {
+
+ private var registeredBroadcastReceiver: BroadcastReceiver? = null
+
+ private val context = mock<Context> {
+ on {
+ registerReceiverAsUser(
+ any(),
+ eq(USER_HANDLE),
+ eq(INTENT_FILTER),
+ isNull(),
+ isNull(),
+ eq(Context.RECEIVER_NOT_EXPORTED),
+ )
+ } doAnswer {
+ registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
+ null
+ }
+ }
+
+ @Test
+ fun broadcastReceiverAsUserFlow_registered() = runBlocking {
+ val flow = context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE)
+
+ flow.firstWithTimeoutOrNull()
+
+ assertThat(registeredBroadcastReceiver).isNotNull()
+ }
+
+ @Test
+ fun broadcastReceiverAsUserFlow_isCalledOnReceive() = runBlocking {
+ var onReceiveIsCalled = false
+ launch {
+ context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE).first {
+ onReceiveIsCalled = true
+ true
+ }
+ }
+
+ delay(100)
+ registeredBroadcastReceiver!!.onReceive(context, Intent())
+ delay(100)
+
+ assertThat(onReceiveIsCalled).isTrue()
+ }
+
+ private companion object {
+ val USER_HANDLE: UserHandle = UserHandle.of(0)
+
+ val INTENT_FILTER = IntentFilter()
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
index 2c8fb66..f812f95 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -23,38 +23,32 @@
import android.os.UserHandle
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import org.junit.Before
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
-import org.mockito.kotlin.whenever
+import org.mockito.kotlin.mock
@RunWith(AndroidJUnit4::class)
class DisposableBroadcastReceiverAsUserTest {
@get:Rule
val composeTestRule = createComposeRule()
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
-
- @Mock
- private lateinit var context: Context
-
private var registeredBroadcastReceiver: BroadcastReceiver? = null
- @Before
- fun setUp() {
- whenever(
- context.registerReceiverAsUser(
+ private val context = mock<Context> {
+ on {
+ registerReceiverAsUser(
any(),
eq(USER_HANDLE),
eq(INTENT_FILTER),
@@ -62,7 +56,7 @@
isNull(),
eq(Context.RECEIVER_NOT_EXPORTED),
)
- ).then {
+ } doAnswer {
registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver
null
}
@@ -71,7 +65,10 @@
@Test
fun broadcastReceiver_registered() {
composeTestRule.setContent {
- CompositionLocalProvider(LocalContext provides context) {
+ CompositionLocalProvider(
+ LocalContext provides context,
+ LocalLifecycleOwner provides TestLifecycleOwner(),
+ ) {
DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {}
}
}
@@ -80,10 +77,13 @@
}
@Test
- fun broadcastReceiver_isCalledOnReceive() {
+ fun broadcastReceiver_isCalledOnReceive() = runBlocking {
var onReceiveIsCalled = false
composeTestRule.setContent {
- CompositionLocalProvider(LocalContext provides context) {
+ CompositionLocalProvider(
+ LocalContext provides context,
+ LocalLifecycleOwner provides TestLifecycleOwner(),
+ ) {
DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {
onReceiveIsCalled = true
}
@@ -91,6 +91,7 @@
}
registeredBroadcastReceiver!!.onReceive(context, Intent())
+ delay(100)
assertThat(onReceiveIsCalled).isTrue()
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 840bca8..44973a7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
+import android.content.pm.FakeFeatureFlagsImpl
+import android.content.pm.Flags
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.ResolveInfoFlags
@@ -29,76 +31,62 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.R
-import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Spy
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-import android.content.pm.FakeFeatureFlagsImpl
-import android.content.pm.Flags
@RunWith(AndroidJUnit4::class)
class AppListRepositoryTest {
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
+ private val resources = mock<Resources> {
+ on { getStringArray(R.array.config_hideWhenDisabled_packageNames) } doReturn emptyArray()
+ }
- @Spy
- private val context: Context = ApplicationProvider.getApplicationContext()
-
- @Mock
- private lateinit var resources: Resources
-
- @Mock
- private lateinit var packageManager: PackageManager
-
- @Mock
- private lateinit var userManager: UserManager
-
- private lateinit var repository: AppListRepository
-
- @Before
- fun setUp() {
- whenever(context.resources).thenReturn(resources)
- whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames))
- .thenReturn(emptyArray())
- whenever(context.packageManager).thenReturn(packageManager)
- whenever(context.userManager).thenReturn(userManager)
- whenever(packageManager.getInstalledModules(any())).thenReturn(emptyList())
- whenever(packageManager.getHomeActivities(any())).thenAnswer {
+ private val packageManager = mock<PackageManager> {
+ on { getInstalledModules(any()) } doReturn emptyList()
+ on { getHomeActivities(any()) } doAnswer {
@Suppress("UNCHECKED_CAST")
val resolveInfos = it.arguments[0] as MutableList<ResolveInfo>
resolveInfos += resolveInfoOf(packageName = HOME_APP.packageName)
null
}
- whenever(
- packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>())
- ).thenReturn(listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName)))
- whenever(userManager.getUserInfo(ADMIN_USER_ID)).thenReturn(UserInfo().apply {
- flags = UserInfo.FLAG_ADMIN
- })
- whenever(userManager.getProfileIdsWithDisabled(ADMIN_USER_ID))
- .thenReturn(intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID))
-
- repository = AppListRepositoryImpl(context)
+ on { queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>()) } doReturn
+ listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName))
}
+ private val mockUserManager = mock<UserManager> {
+ on { getUserInfo(ADMIN_USER_ID) } doReturn UserInfo().apply {
+ flags = UserInfo.FLAG_ADMIN
+ }
+ on { getProfileIdsWithDisabled(ADMIN_USER_ID) } doReturn
+ intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID)
+ }
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ on { resources } doReturn resources
+ on { packageManager } doReturn packageManager
+ on { getSystemService(UserManager::class.java) } doReturn mockUserManager
+ }
+
+ private val repository = AppListRepositoryImpl(context)
+
private fun mockInstalledApplications(apps: List<ApplicationInfo>, userId: Int) {
- whenever(
- packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId))
- ).thenReturn(apps)
+ packageManager.stub {
+ on { getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId)) } doReturn
+ apps
+ }
}
@Test
@@ -135,13 +123,13 @@
)
assertThat(appList).containsExactly(NORMAL_APP)
- argumentCaptor<ApplicationInfoFlags> {
+ val flags = argumentCaptor<ApplicationInfoFlags> {
verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
- assertThat(firstValue.value).isEqualTo(
- PackageManager.MATCH_DISABLED_COMPONENTS or
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
- )
- }
+ }.firstValue
+ assertThat(flags.value).isEqualTo(
+ PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ )
}
@Test
@@ -154,11 +142,10 @@
)
assertThat(appList).containsExactly(NORMAL_APP)
- argumentCaptor<ApplicationInfoFlags> {
+ val flags = argumentCaptor<ApplicationInfoFlags> {
verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
- assertThat(firstValue.value and PackageManager.MATCH_ANY_USER.toLong())
- .isGreaterThan(0L)
- }
+ }.firstValue
+ assertThat(flags.value and PackageManager.MATCH_ANY_USER.toLong()).isGreaterThan(0L)
}
@Test
@@ -278,14 +265,14 @@
val appList = repository.loadApps(userId = ADMIN_USER_ID)
assertThat(appList).containsExactly(NORMAL_APP, ARCHIVED_APP)
- argumentCaptor<ApplicationInfoFlags> {
+ val flags = argumentCaptor<ApplicationInfoFlags> {
verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
- assertThat(firstValue.value).isEqualTo(
- (PackageManager.MATCH_DISABLED_COMPONENTS or
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or
- PackageManager.MATCH_ARCHIVED_PACKAGES
- )
- }
+ }.firstValue
+ assertThat(flags.value).isEqualTo(
+ (PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or
+ PackageManager.MATCH_ARCHIVED_PACKAGES
+ )
}
@Test
@@ -294,13 +281,13 @@
val appList = repository.loadApps(userId = ADMIN_USER_ID)
assertThat(appList).containsExactly(NORMAL_APP)
- argumentCaptor<ApplicationInfoFlags> {
+ val flags = argumentCaptor<ApplicationInfoFlags> {
verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID))
- assertThat(firstValue.value).isEqualTo(
- PackageManager.MATCH_DISABLED_COMPONENTS or
- PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
- )
- }
+ }.firstValue
+ assertThat(flags.value).isEqualTo(
+ PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ )
}
@Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
index 942e915..74a282f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
@@ -21,30 +21,25 @@
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
-import android.text.TextUtils;
import android.view.View;
-import android.widget.Switch;
+import android.widget.CompoundButton;
import android.widget.TextView;
+import androidx.test.core.app.ApplicationProvider;
+
import com.android.settingslib.widget.mainswitch.R;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public class MainSwitchBarTest {
- private Context mContext;
- private MainSwitchBar mBar;
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final MainSwitchBar mBar = new MainSwitchBar(mContext);
- @Before
- public void setUp() {
- mContext = RuntimeEnvironment.application;
- mBar = new MainSwitchBar(mContext);
- }
+ private final CompoundButton mSwitch = mBar.findViewById(android.R.id.switch_widget);
@Test
public void setChecked_true_shouldChecked() {
@@ -60,7 +55,7 @@
mBar.setTitle(title);
final TextView textView = ((TextView) mBar.findViewById(R.id.switch_text));
- assertThat(textView.getText()).isEqualTo(title);
+ assertThat(textView.getText().toString()).isEqualTo(title);
}
@Test
@@ -69,23 +64,18 @@
mBar.setTitle(title);
- final Switch switchObj = mBar.getSwitch();
- assertThat(TextUtils.isEmpty(switchObj.getContentDescription())).isTrue();
+ assertThat(mSwitch.getContentDescription()).isNull();
}
@Test
public void getSwitch_shouldNotNull() {
- final Switch switchObj = mBar.getSwitch();
-
- assertThat(switchObj).isNotNull();
+ assertThat(mSwitch).isNotNull();
}
@Test
public void getSwitch_shouldNotFocusableAndClickable() {
- final Switch switchObj = mBar.getSwitch();
-
- assertThat(switchObj.isFocusable()).isFalse();
- assertThat(switchObj.isClickable()).isFalse();
+ assertThat(mSwitch.isFocusable()).isFalse();
+ assertThat(mSwitch.isClickable()).isFalse();
}
@Test
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8780f58..9a3c6d5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3266,4 +3266,9 @@
<string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
<!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+
+ <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
+ <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
+ <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
+ <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index e16bb0b..1e9be09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -30,6 +30,7 @@
import android.view.ViewGroup.MarginLayoutParams
import android.view.Window
import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
@@ -78,23 +79,29 @@
private lateinit var stepProperties: StepViewProperties
@ColorInt
- var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
- @ColorInt
- var emptyRectangleColor =
- getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
- @ColorInt
- var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
- @ColorInt
- var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
- @ColorInt
- var defaultIconBackgroundColor =
+ private val filledRectangleColor =
getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
@ColorInt
- var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+ private val emptyRectangleColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
@ColorInt
- var dimmedIconBackgroundColor =
+ private val backgroundColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
+ @ColorInt
+ private val defaultIconColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+ @ColorInt
+ private val defaultIconBackgroundColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+ @ColorInt
+ private val dimmedIconColor =
+ getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+ @ColorInt
+ private val dimmedIconBackgroundColor =
getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
+ private val levelContentDescription = context.getString(R.string.keyboard_backlight_value)
+
init {
currentLevel = initialCurrentLevel
maxLevel = initialMaxLevel
@@ -103,6 +110,8 @@
override fun onCreate(savedInstanceState: Bundle?) {
setUpWindowProperties(this)
setWindowPosition()
+ // title is used for a11y announcement
+ window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title))
updateResources()
rootView = buildRootView()
setContentView(rootView)
@@ -159,6 +168,12 @@
currentLevel = current
updateIconTile()
updateStepColors()
+ updateAccessibilityInfo()
+ }
+
+ private fun updateAccessibilityInfo() {
+ rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel)
+ rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
}
private fun updateIconTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 093d098..d9a8080 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -325,7 +325,13 @@
} else {
// TODO(b/278729185): Replace fire and forget service with a bounded service.
val intent = NoteTaskControllerUpdateService.createIntent(context)
- context.startServiceAsUser(intent, user)
+ try {
+ // If the user is stopped before 'startServiceAsUser' kicks-in, a
+ // 'SecurityException' will be thrown.
+ context.startServiceAsUser(intent, user)
+ } catch (e: SecurityException) {
+ debugLog(error = e) { "Unable to start 'NoteTaskControllerUpdateService'." }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 966ff35..ec90a8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -90,10 +90,10 @@
} else {
setWillBeGone(true);
}
- setContentVisible(visible, true /* animate */, null /* runAfter */);
+ setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
} else {
setVisibility(visible ? VISIBLE : GONE);
- setContentVisible(visible, false /* animate */, null /* runAfter */);
+ setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
setWillBeGone(false);
notifyHeightChanged(false /* needsAnimation */);
}
@@ -108,7 +108,7 @@
* Change content visibility to {@code visible}, animated.
*/
public void setContentVisibleAnimated(boolean visible) {
- setContentVisible(visible, true /* animate */, null /* runAfter */);
+ setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 5c1149b..580431a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -53,7 +53,7 @@
mContents = requireViewById(R.id.content);
bindContents();
super.onFinishInflate();
- setVisible(true /* nowVisible */, false /* animate */);
+ setVisible(true /* visible */, false /* animate */);
}
private void bindContents() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
new file mode 100644
index 0000000..8b572eb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyboard.backlight.ui.view
+
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWithLooper
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyboardBacklightDialogTest : SysuiTestCase() {
+
+ private lateinit var dialog: KeyboardBacklightDialog
+ private lateinit var rootView: View
+ private val descriptionString = context.getString(R.string.keyboard_backlight_value)
+
+ @Before
+ fun setUp() {
+ dialog =
+ KeyboardBacklightDialog(context, initialCurrentLevel = 0, initialMaxLevel = MAX_LEVEL)
+ dialog.show()
+ rootView = dialog.requireViewById(R.id.keyboard_backlight_dialog_container)
+ }
+
+ @Test
+ fun rootViewContentDescription_containsInitialLevel() {
+ assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(INITIAL_LEVEL))
+ }
+
+ @Test
+ fun contentDescriptionUpdated_afterEveryLevelUpdate() {
+ val events = startCollectingAccessibilityEvents(rootView)
+
+ dialog.updateState(current = 1, max = MAX_LEVEL)
+
+ assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(1))
+ assertThat(events).contains(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
+ }
+
+ private fun contentDescriptionForLevel(level: Int): String {
+ return String.format(descriptionString, level, MAX_LEVEL)
+ }
+
+ private fun startCollectingAccessibilityEvents(rootView: View): MutableList<Int> {
+ val events = mutableListOf<Int>()
+ rootView.accessibilityDelegate =
+ object : View.AccessibilityDelegate() {
+ override fun sendAccessibilityEvent(host: View, eventType: Int) {
+ super.sendAccessibilityEvent(host, eventType)
+ events.add(eventType)
+ }
+ }
+ return events
+ }
+
+ companion object {
+ private const val MAX_LEVEL = 5
+ private const val INITIAL_LEVEL = 0
+ }
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 2897075..5d4f711 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -70,7 +70,6 @@
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -172,7 +171,7 @@
@NonNull AudioSystemAdapter audioSystem) {
mContext = context;
mAudioService = service;
- mBtHelper = new BtHelper(this);
+ mBtHelper = new BtHelper(this, context);
mDeviceInventory = new AudioDeviceInventory(this);
mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
mAudioSystem = audioSystem;
@@ -188,7 +187,7 @@
@NonNull AudioSystemAdapter audioSystem) {
mContext = context;
mAudioService = service;
- mBtHelper = new BtHelper(this);
+ mBtHelper = new BtHelper(this, context);
mDeviceInventory = mockDeviceInventory;
mSystemServer = mockSystemServer;
mAudioSystem = audioSystem;
@@ -1392,6 +1391,10 @@
return mAudioService.hasAudioFocusUsers();
}
+ /*package*/ void postInitSpatializerHeadTrackingSensors() {
+ mAudioService.postInitSpatializerHeadTrackingSensors();
+ }
+
//---------------------------------------------------------------------
// Message handling on behalf of helper classes.
// Each of these methods posts a message to mBrokerHandler message queue.
@@ -1475,6 +1478,15 @@
sendLMsgNoDelay(MSG_L_RECEIVED_BT_EVENT, SENDMSG_QUEUE, intent);
}
+ /*package*/ void postUpdateLeAudioGroupAddresses(int groupId) {
+ sendIMsgNoDelay(
+ MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES, SENDMSG_QUEUE, groupId);
+ }
+
+ /*package*/ void postSynchronizeLeDevicesInInventory(AdiDeviceState deviceState) {
+ sendLMsgNoDelay(MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState);
+ }
+
/*package*/ static final class CommunicationDeviceInfo {
final @NonNull IBinder mCb; // Identifies the requesting client for death handler
final int mUid; // Requester UID
@@ -1604,6 +1616,14 @@
}
}
+ /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+ return mBtHelper.getLeAudioDeviceGroupId(device);
+ }
+
+ /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+ return mBtHelper.getLeAudioGroupAddresses(groupId);
+ }
+
/*package*/ void broadcastStickyIntentToCurrentProfileGroup(Intent intent) {
mSystemServer.broadcastStickyIntentToCurrentProfileGroup(intent);
}
@@ -1976,6 +1996,22 @@
onCheckCommunicationRouteClientState(msg.arg1, msg.arg2 == 1);
}
} break;
+
+ case MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES:
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onUpdateLeAudioGroupAddresses(msg.arg1);
+ }
+ } break;
+
+ case MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY:
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onSynchronizeLeDevicesInInventory(
+ (AdiDeviceState) msg.obj);
+ }
+ } break;
+
default:
Log.wtf(TAG, "Invalid message " + msg.what);
}
@@ -2058,6 +2094,10 @@
private static final int MSG_L_RECEIVED_BT_EVENT = 55;
private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56;
+ private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
+ private static final int MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY = 58;
+
+
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
@@ -2582,9 +2622,9 @@
}
}
- @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+ List<String> getDeviceAddresses(AudioDeviceAttributes device) {
synchronized (mDeviceStateLock) {
- return mDeviceInventory.getDeviceSensorUuid(device);
+ return mDeviceInventory.getDeviceAddresses(device);
}
}
@@ -2605,15 +2645,19 @@
* in order to be mocked by a test a the same package
* (see https://code.google.com/archive/p/mockito/issues/127)
*/
- public void persistAudioDeviceSettings() {
+ public void postPersistAudioDeviceSettings() {
sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000);
}
void onPersistAudioDeviceSettings() {
final String deviceSettings = mDeviceInventory.getDeviceSettings();
- Log.v(TAG, "saving AdiDeviceState: " + deviceSettings);
- final SettingsAdapter settings = mAudioService.getSettings();
- boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(),
+ Log.v(TAG, "onPersistAudioDeviceSettings AdiDeviceState: " + deviceSettings);
+ String currentSettings = readDeviceSettings();
+ if (deviceSettings.equals(currentSettings)) {
+ return;
+ }
+ final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+ boolean res = settingsAdapter.putSecureStringForUser(mAudioService.getContentResolver(),
Settings.Secure.AUDIO_DEVICE_INVENTORY,
deviceSettings, UserHandle.USER_CURRENT);
if (!res) {
@@ -2621,11 +2665,17 @@
}
}
+ private String readDeviceSettings() {
+ final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+ final ContentResolver contentResolver = mAudioService.getContentResolver();
+ return settingsAdapter.getSecureStringForUser(contentResolver,
+ Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+ }
+
void onReadAudioDeviceSettings() {
final SettingsAdapter settingsAdapter = mAudioService.getSettings();
final ContentResolver contentResolver = mAudioService.getContentResolver();
- String settings = settingsAdapter.getSecureStringForUser(contentResolver,
- Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+ String settings = readDeviceSettings();
if (settings == null) {
Log.i(TAG, "reading AdiDeviceState from legacy key"
+ Settings.Secure.SPATIAL_AUDIO_ENABLED);
@@ -2688,8 +2738,8 @@
}
@Nullable
- AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
- return mDeviceInventory.findBtDeviceStateForAddress(address, isBle);
+ AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+ return mDeviceInventory.findBtDeviceStateForAddress(address, deviceType);
}
//------------------------------------------------
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e59fd77..7ba0827 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -15,14 +15,23 @@
*/
package com.android.server.audio;
+import static android.media.AudioSystem.DEVICE_IN_ALL_SCO_SET;
import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP_SET;
import static android.media.AudioSystem.DEVICE_OUT_ALL_BLE_SET;
+import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO_SET;
+import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
+import static android.media.AudioSystem.isBluetoothA2dpOutDevice;
import static android.media.AudioSystem.isBluetoothDevice;
+import static android.media.AudioSystem.isBluetoothLeOutDevice;
+import static android.media.AudioSystem.isBluetoothOutDevice;
+import static android.media.AudioSystem.isBluetoothScoOutDevice;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.media.AudioDeviceAttributes;
@@ -72,7 +81,6 @@
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
-import java.util.UUID;
import java.util.stream.Stream;
/**
@@ -118,6 +126,7 @@
return oldState;
});
}
+ mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
}
/**
@@ -125,23 +134,28 @@
* Bluetooth device and no corresponding entry already exists.
* @param ada the device to add if needed
*/
- void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) {
- if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) {
+ void addAudioDeviceInInventoryIfNeeded(int deviceType, String address, String peerAddres) {
+ if (!isBluetoothOutDevice(deviceType)) {
return;
}
synchronized (mDeviceInventoryLock) {
- if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) {
+ AdiDeviceState ads = findBtDeviceStateForAddress(address, deviceType);
+ if (ads == null) {
+ ads = findBtDeviceStateForAddress(peerAddres, deviceType);
+ }
+ if (ads != null) {
+ mDeviceBroker.postSynchronizeLeDevicesInInventory(ads);
return;
}
- AdiDeviceState ads = new AdiDeviceState(
- ada.getType(), ada.getInternalType(), ada.getAddress());
+ ads = new AdiDeviceState(AudioDeviceInfo.convertInternalDeviceToDeviceType(deviceType),
+ deviceType, address);
mDeviceInventory.put(ads.getDeviceId(), ads);
+ mDeviceBroker.postPersistAudioDeviceSettings();
}
- mDeviceBroker.persistAudioDeviceSettings();
}
/**
- * Adds a new AdiDeviceState or updates the audio device cateogory of the matching
+ * Adds a new AdiDeviceState or updates the audio device category of the matching
* AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
* @param deviceState the device to update
*/
@@ -152,6 +166,63 @@
return oldState;
});
}
+ mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
+ }
+
+ /**
+ * synchronize AdiDeviceState for LE devices in the same group
+ */
+ void onSynchronizeLeDevicesInInventory(AdiDeviceState updatedDevice) {
+ synchronized (mDevicesLock) {
+ synchronized (mDeviceInventoryLock) {
+ boolean found = false;
+ for (DeviceInfo di : mConnectedDevices.values()) {
+ if (di.mDeviceType != updatedDevice.getInternalDeviceType()) {
+ continue;
+ }
+ if (di.mDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+ for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+ if (!(di.mDeviceType == ads2.getInternalDeviceType()
+ && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) {
+ continue;
+ }
+ ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+ ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+ ads2.setSAEnabled(updatedDevice.isSAEnabled());
+ ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+ found = true;
+ AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+ "onSynchronizeLeDevicesInInventory synced device pair ads1="
+ + updatedDevice + " ads2=" + ads2).printLog(TAG));
+ break;
+ }
+ }
+ if (di.mPeerDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+ for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+ if (!(di.mDeviceType == ads2.getInternalDeviceType()
+ && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) {
+ continue;
+ }
+ ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+ ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+ ads2.setSAEnabled(updatedDevice.isSAEnabled());
+ ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+ found = true;
+ AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+ "onSynchronizeLeDevicesInInventory synced device pair ads1="
+ + updatedDevice + " peer ads2=" + ads2).printLog(TAG));
+ break;
+ }
+ }
+ if (found) {
+ break;
+ }
+ }
+ if (found) {
+ mDeviceBroker.postPersistAudioDeviceSettings();
+ }
+ }
+ }
}
/**
@@ -163,9 +234,21 @@
* @return the found {@link AdiDeviceState} or {@code null} otherwise.
*/
@Nullable
- AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
+ AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+ Set<Integer> deviceSet;
+ if (isBluetoothA2dpOutDevice(deviceType)) {
+ deviceSet = DEVICE_OUT_ALL_A2DP_SET;
+ } else if (isBluetoothLeOutDevice(deviceType)) {
+ deviceSet = DEVICE_OUT_ALL_BLE_SET;
+ } else if (isBluetoothScoOutDevice(deviceType)) {
+ deviceSet = DEVICE_OUT_ALL_SCO_SET;
+ } else if (deviceType == DEVICE_OUT_HEARING_AID) {
+ deviceSet = new HashSet<>();
+ deviceSet.add(DEVICE_OUT_HEARING_AID);
+ } else {
+ return null;
+ }
synchronized (mDeviceInventoryLock) {
- final Set<Integer> deviceSet = isBle ? DEVICE_OUT_ALL_BLE_SET : DEVICE_OUT_ALL_A2DP_SET;
for (Integer internalType : deviceSet) {
AdiDeviceState deviceState = mDeviceInventory.get(
new Pair<>(internalType, address));
@@ -345,7 +428,8 @@
final @NonNull String mDeviceName;
final @NonNull String mDeviceAddress;
int mDeviceCodecFormat;
- final UUID mSensorUuid;
+ @NonNull String mPeerDeviceAddress;
+ final int mGroupId;
/** Disabled operating modes for this device. Use a negative logic so that by default
* an empty list means all modes are allowed.
@@ -353,12 +437,13 @@
@NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
DeviceInfo(int deviceType, String deviceName, String deviceAddress,
- int deviceCodecFormat, @Nullable UUID sensorUuid) {
+ int deviceCodecFormat, String peerDeviceAddress, int groupId) {
mDeviceType = deviceType;
mDeviceName = deviceName == null ? "" : deviceName;
mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
mDeviceCodecFormat = deviceCodecFormat;
- mSensorUuid = sensorUuid;
+ mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress;
+ mGroupId = groupId;
}
void setModeDisabled(String mode) {
@@ -379,7 +464,8 @@
DeviceInfo(int deviceType, String deviceName, String deviceAddress,
int deviceCodecFormat) {
- this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null);
+ this(deviceType, deviceName, deviceAddress, deviceCodecFormat,
+ null, BluetoothLeAudio.GROUP_ID_INVALID);
}
DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
@@ -393,7 +479,8 @@
+ ") name:" + mDeviceName
+ " addr:" + mDeviceAddress
+ " codec: " + Integer.toHexString(mDeviceCodecFormat)
- + " sensorUuid: " + Objects.toString(mSensorUuid)
+ + " peer addr:" + mPeerDeviceAddress
+ + " group:" + mGroupId
+ " disabled modes: " + mDisabledModes + "]";
}
@@ -714,6 +801,27 @@
}
}
+
+ /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) {
+ synchronized (mDevicesLock) {
+ for (DeviceInfo di : mConnectedDevices.values()) {
+ if (di.mGroupId == groupId) {
+ List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+ if (di.mPeerDeviceAddress.equals("")) {
+ for (String addr : addresses) {
+ if (!addr.equals(di.mDeviceAddress)) {
+ di.mPeerDeviceAddress = addr;
+ break;
+ }
+ }
+ } else if (!addresses.contains(di.mPeerDeviceAddress)) {
+ di.mPeerDeviceAddress = "";
+ }
+ }
+ }
+ }
+ }
+
/*package*/ void onReportNewRoutes() {
int n = mRoutesObservers.beginBroadcast();
if (n > 0) {
@@ -1419,7 +1527,7 @@
if (!connect) {
purgeDevicesRoles_l();
} else {
- addAudioDeviceInInventoryIfNeeded(attributes);
+ addAudioDeviceInInventoryIfNeeded(device, address, "");
}
}
mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
@@ -1477,7 +1585,7 @@
final ArraySet<String> toRemove = new ArraySet<>();
// Disconnect ALL DEVICE_OUT_HEARING_AID devices
mConnectedDevices.values().forEach(deviceInfo -> {
- if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+ if (deviceInfo.mDeviceType == DEVICE_OUT_HEARING_AID) {
toRemove.add(deviceInfo.mDeviceAddress);
}
});
@@ -1485,8 +1593,8 @@
.set(MediaMetrics.Property.EVENT, "disconnectHearingAid")
.record();
if (toRemove.size() > 0) {
- final int delay = checkSendBecomingNoisyIntentInt(
- AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
+ final int delay = checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID,
+ AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE);
toRemove.stream().forEach(deviceAddress ->
// TODO delay not used?
makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/)
@@ -1687,12 +1795,8 @@
// Reset A2DP suspend state each time a new sink is connected
mDeviceBroker.clearA2dpSuspended(true /* internalOnly */);
- // The convention for head tracking sensors associated with A2DP devices is to
- // use a UUID derived from the MAC address as follows:
- // time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address
- UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
- address, codec, sensorUuid);
+ address, codec);
final String diKey = di.getKey();
mConnectedDevices.put(diKey, di);
// on a connection always overwrite the device seen by AudioPolicy, see comment above when
@@ -1703,7 +1807,7 @@
setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/);
- addAudioDeviceInInventoryIfNeeded(ada);
+ addAudioDeviceInInventoryIfNeeded(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, "");
}
static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER,
@@ -1723,15 +1827,15 @@
return;
}
DeviceInfo leOutDevice =
- getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+ getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_BLE_SET);
DeviceInfo leInDevice =
getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET);
DeviceInfo a2dpDevice =
- getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+ getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET);
DeviceInfo scoOutDevice =
- getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET);
+ getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET);
DeviceInfo scoInDevice =
- getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET);
+ getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET);
boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled());
boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled())
|| (leInDevice != null && leInDevice.isDuplexModeEnabled());
@@ -1765,7 +1869,7 @@
continue;
}
- if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) {
+ if (isBluetoothOutDevice(di.mDeviceType)) {
for (AudioProductStrategy strategy : mStrategies) {
boolean disable = false;
if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) {
@@ -1832,23 +1936,20 @@
int checkProfileIsConnected(int profile) {
switch (profile) {
case BluetoothProfile.HEADSET:
- if (getFirstConnectedDeviceOfTypes(
- AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null
- || getFirstConnectedDeviceOfTypes(
- AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) {
+ if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET) != null
+ || getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET) != null) {
return profile;
}
break;
case BluetoothProfile.A2DP:
- if (getFirstConnectedDeviceOfTypes(
- AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) {
+ if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET) != null) {
return profile;
}
break;
case BluetoothProfile.LE_AUDIO:
case BluetoothProfile.LE_AUDIO_BROADCAST:
if (getFirstConnectedDeviceOfTypes(
- AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null
+ DEVICE_OUT_ALL_BLE_SET) != null
|| getFirstConnectedDeviceOfTypes(
AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) {
return profile;
@@ -2006,28 +2107,28 @@
private void makeHearingAidDeviceAvailable(
String address, String name, int streamType, String eventSource) {
final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
- AudioSystem.DEVICE_OUT_HEARING_AID);
+ DEVICE_OUT_HEARING_AID);
mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource);
AudioDeviceAttributes ada = new AudioDeviceAttributes(
- AudioSystem.DEVICE_OUT_HEARING_AID, address, name);
+ DEVICE_OUT_HEARING_AID, address, name);
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_AVAILABLE,
AudioSystem.AUDIO_FORMAT_DEFAULT);
mConnectedDevices.put(
- DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
- new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address));
- mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
+ DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
+ new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
+ mDeviceBroker.postAccessoryPlugMediaUnmute(DEVICE_OUT_HEARING_AID);
mDeviceBroker.postApplyVolumeOnDevice(streamType,
- AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
+ DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/);
- addAudioDeviceInInventoryIfNeeded(ada);
+ addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_HEARING_AID, address, "");
new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
.set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
.set(MediaMetrics.Property.DEVICE,
- AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+ AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
.set(MediaMetrics.Property.NAME, name)
.set(MediaMetrics.Property.STREAM_TYPE,
AudioSystem.streamToString(streamType))
@@ -2037,18 +2138,18 @@
@GuardedBy("mDevicesLock")
private void makeHearingAidDeviceUnavailable(String address) {
AudioDeviceAttributes ada = new AudioDeviceAttributes(
- AudioSystem.DEVICE_OUT_HEARING_AID, address);
+ DEVICE_OUT_HEARING_AID, address);
mAudioSystem.setDeviceConnectionState(ada,
AudioSystem.DEVICE_STATE_UNAVAILABLE,
AudioSystem.AUDIO_FORMAT_DEFAULT);
mConnectedDevices.remove(
- DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
+ DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
// Remove Hearing Aid routes as well
setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable")
.set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
.set(MediaMetrics.Property.DEVICE,
- AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+ AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
.record();
mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
}
@@ -2060,7 +2161,7 @@
*/
boolean isHearingAidConnected() {
return getFirstConnectedDeviceOfTypes(
- Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null;
+ Sets.newHashSet(DEVICE_OUT_HEARING_AID)) != null;
}
/**
@@ -2102,6 +2203,20 @@
final String address = btInfo.mDevice.getAddress();
String name = BtHelper.getName(btInfo.mDevice);
+ // Find LE Group ID and peer headset address if available
+ final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice);
+ String peerAddress = "";
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+ if (addresses.size() > 1) {
+ for (String addr : addresses) {
+ if (!addr.equals(address)) {
+ peerAddress = addr;
+ break;
+ }
+ }
+ }
+ }
// The BT Stack does not provide a name for LE Broadcast devices
if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) {
name = "Broadcast";
@@ -2127,14 +2242,12 @@
}
// Reset LEA suspend state each time a new sink is connected
mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
-
- UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT,
- sensorUuid));
+ peerAddress, groupId));
mDeviceBroker.postAccessoryPlugMediaUnmute(device);
setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
- addAudioDeviceInInventoryIfNeeded(ada);
+ addAudioDeviceInInventoryIfNeeded(device, address, peerAddress);
}
if (streamType == AudioSystem.STREAM_DEFAULT) {
@@ -2226,12 +2339,12 @@
BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI);
BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET);
BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE);
- BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID);
+ BECOMING_NOISY_INTENT_DEVICES_SET.add(DEVICE_OUT_HEARING_AID);
BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_HEADSET);
BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_BROADCAST);
- BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+ BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_A2DP_SET);
BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
- BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+ BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_BLE_SET);
}
// must be called before removing the device from mConnectedDevices
@@ -2512,16 +2625,22 @@
mDevRoleCapturePresetDispatchers.finishBroadcast();
}
- @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+ List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+ List<String> addresses = new ArrayList<String>();
final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
device.getAddress());
synchronized (mDevicesLock) {
DeviceInfo di = mConnectedDevices.get(key);
- if (di == null) {
- return null;
+ if (di != null) {
+ if (!di.mDeviceAddress.isEmpty()) {
+ addresses.add(di.mDeviceAddress);
+ }
+ if (!di.mPeerDeviceAddress.isEmpty()) {
+ addresses.add(di.mPeerDeviceAddress);
+ }
}
- return di.mSensorUuid;
}
+ return addresses;
}
/*package*/ String getDeviceSettings() {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d0ae0f2..99321c4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -237,7 +237,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
-import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -11054,7 +11053,9 @@
final String addr = Objects.requireNonNull(address);
- AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr, isBle);
+ AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr,
+ (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+ : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
int internalType = !isBle ? DEVICE_OUT_BLUETOOTH_A2DP
: ((btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES)
@@ -11070,7 +11071,7 @@
deviceState.setAudioDeviceCategory(btAudioDeviceCategory);
mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
mSoundDoseHelper.setAudioDeviceCategory(addr, internalType,
@@ -11084,7 +11085,8 @@
super.getBluetoothAudioDeviceCategory_enforcePermission();
final AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(
- Objects.requireNonNull(address), isBle);
+ Objects.requireNonNull(address), (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+ : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
if (deviceState == null) {
return AUDIO_DEVICE_CATEGORY_UNKNOWN;
}
@@ -13596,8 +13598,8 @@
return activeAssistantUids;
}
- UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
- return mDeviceBroker.getDeviceSensorUuid(device);
+ List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+ return mDeviceBroker.getDeviceAddresses(device);
}
//======================
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index cce6bd2..7b96215 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -26,7 +26,9 @@
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothLeAudioCodecStatus;
import android.bluetooth.BluetoothProfile;
+import android.content.Context;
import android.content.Intent;
import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
@@ -43,6 +45,7 @@
import com.android.server.utils.EventLogger;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -57,9 +60,11 @@
private static final String TAG = "AS.BtHelper";
private final @NonNull AudioDeviceBroker mDeviceBroker;
+ private final @NonNull Context mContext;
- BtHelper(@NonNull AudioDeviceBroker broker) {
+ BtHelper(@NonNull AudioDeviceBroker broker, Context context) {
mDeviceBroker = broker;
+ mContext = context;
}
// BluetoothHeadset API to control SCO connection
@@ -498,6 +503,32 @@
}
}
+ // BluetoothLeAudio callback used to update the list of addresses in the same group as a
+ // connected LE Audio device
+ MyLeAudioCallback mLeAudioCallback = null;
+
+ class MyLeAudioCallback implements BluetoothLeAudio.Callback {
+ @Override
+ public void onCodecConfigChanged(int groupId,
+ @NonNull BluetoothLeAudioCodecStatus status) {
+ // Do nothing
+ }
+
+ @Override
+ public void onGroupNodeAdded(@NonNull BluetoothDevice device, int groupId) {
+ mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+ }
+
+ @Override
+ public void onGroupNodeRemoved(@NonNull BluetoothDevice device, int groupId) {
+ mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+ }
+ @Override
+ public void onGroupStatusChanged(int groupId, int groupStatus) {
+ mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+ }
+ }
+
// @GuardedBy("mDeviceBroker.mSetModeLock")
@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
/*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
@@ -519,6 +550,11 @@
mHearingAid = (BluetoothHearingAid) proxy;
break;
case BluetoothProfile.LE_AUDIO:
+ if (mLeAudio == null) {
+ mLeAudioCallback = new MyLeAudioCallback();
+ ((BluetoothLeAudio) proxy).registerCallback(
+ mContext.getMainExecutor(), mLeAudioCallback);
+ }
mLeAudio = (BluetoothLeAudio) proxy;
break;
case BluetoothProfile.A2DP_SINK:
@@ -977,6 +1013,28 @@
return result;
}
+ /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+ if (mLeAudio == null || device == null) {
+ return BluetoothLeAudio.GROUP_ID_INVALID;
+ }
+ return mLeAudio.getGroupId(device);
+ }
+
+ /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+ List<String> addresses = new ArrayList<String>();
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null || mLeAudio == null) {
+ return addresses;
+ }
+ List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
+ for (BluetoothDevice device : activeDevices) {
+ if (device != null && mLeAudio.getGroupId(device) == groupId) {
+ addresses.add(device.getAddress());
+ }
+ }
+ return addresses;
+ }
+
/**
* Returns the String equivalent of the btCodecType.
*
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 7abd9c7..ea92154 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -564,7 +564,7 @@
}
if (updatedDevice != null) {
onRoutingUpdated();
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
logDeviceState(updatedDevice, "addCompatibleAudioDevice");
}
}
@@ -614,7 +614,7 @@
if (deviceState != null && deviceState.isSAEnabled()) {
deviceState.setSAEnabled(false);
onRoutingUpdated();
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
logDeviceState(deviceState, "removeCompatibleAudioDevice");
}
}
@@ -716,7 +716,7 @@
ada.getAddress());
initSAState(deviceState);
mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState);
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
}
}
@@ -1206,7 +1206,7 @@
}
Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
deviceState.setHeadTrackerEnabled(enabled);
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
logDeviceState(deviceState, "setHeadTrackerEnabled");
// check current routing to see if it affects the headtracking mode
@@ -1248,7 +1248,7 @@
if (deviceState != null) {
if (!deviceState.hasHeadTracker()) {
deviceState.setHasHeadTracker(true);
- mDeviceBroker.persistAudioDeviceSettings();
+ mDeviceBroker.postPersistAudioDeviceSettings();
logDeviceState(deviceState, "setHasHeadTracker");
}
return deviceState.isHeadTrackerEnabled();
@@ -1631,25 +1631,33 @@
return headHandle;
}
final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
- UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice);
+ List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
+
// We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
// with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
// and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
// SensorPoseProvider).
// Note: this is a dynamic sensor list right now.
List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER);
- for (Sensor sensor : sensors) {
- final UUID uuid = sensor.getUuid();
- if (uuid.equals(routingDeviceUuid)) {
- headHandle = sensor.getHandle();
- if (!setHasHeadTracker(currentDevice)) {
- headHandle = -1;
+ for (String address : deviceAddresses) {
+ UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes(
+ new AudioDeviceAttributes(currentDevice.getInternalType(), address));
+ for (Sensor sensor : sensors) {
+ final UUID uuid = sensor.getUuid();
+ if (uuid.equals(routingDeviceUuid)) {
+ headHandle = sensor.getHandle();
+ if (!setHasHeadTracker(currentDevice)) {
+ headHandle = -1;
+ }
+ break;
}
- break;
+ if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+ headHandle = sensor.getHandle();
+ // we do not break, perhaps we find a head tracker on device.
+ }
}
- if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
- headHandle = sensor.getHandle();
- // we do not break, perhaps we find a head tracker on device.
+ if (headHandle != -1) {
+ break;
}
}
return headHandle;
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
index ad09ef0..061b8ff 100644
--- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -79,7 +79,7 @@
final AudioDeviceAttributes dev3 =
new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop");
- doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings();
+ doNothing().when(mSpyDeviceBroker).postPersistAudioDeviceSettings();
mSpatHelper.initForTest(true /*binaural*/, true /*transaural*/);
// test with single device