Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java +10 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,16 @@ public interface BluetoothCallback { default void onAclConnectionStateChanged( @NonNull CachedBluetoothDevice cachedDevice, int state) {} /** * Called when the Auto-on state is changed for any user. Listens to intent * {@link android.bluetooth.BluetoothAdapter#ACTION_AUTO_ON_STATE_CHANGED } * * @param state the Auto-on state, the possible values are: * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_ENABLED}, * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_DISABLED} */ default void onAutoOnStateChanged(int state) {} @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "STATE_" }, value = { STATE_DISCONNECTED, Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +19 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,8 @@ public class BluetoothEventManager { addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler()); addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler()); addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); registerAdapterIntentReceiver(); } Loading Loading @@ -552,4 +554,21 @@ public class BluetoothEventManager { dispatchAudioModeChanged(); } } private class AutoOnStateChangedHandler implements Handler { @Override public void onReceive(Context context, Intent intent, BluetoothDevice device) { String action = intent.getAction(); if (action == null) { Log.w(TAG, "AutoOnStateChangedHandler() action is null"); return; } int state = intent.getIntExtra(BluetoothAdapter.EXTRA_AUTO_ON_STATE, BluetoothAdapter.ERROR); for (BluetoothCallback callback : mCallbacks) { callback.onAutoOnStateChanged(state); } } } } packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; Loading Loading @@ -489,4 +490,17 @@ public class BluetoothEventManagerTest { verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME), eq(R.string.bluetooth_pairing_pin_error_message)); } /** * Intent ACTION_AUTO_ON_STATE_CHANGED should dispatch to callback. */ @Test public void intentWithExtraState_autoOnStateChangedShouldDispatchToRegisterCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); mIntent = new Intent(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED); mContext.sendBroadcast(mIntent); verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); } } packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt +5 −14 Original line number Diff line number Diff line Loading @@ -19,8 +19,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.util.Log import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */ @SysUISingleton Loading @@ -30,14 +28,10 @@ constructor( private val bluetoothAutoOnRepository: BluetoothAutoOnRepository, ) { val isEnabled = bluetoothAutoOnRepository.isAutoOn.map { it == ENABLED }.distinctUntilChanged() val isEnabled = bluetoothAutoOnRepository.isAutoOn /** * Checks if the auto on value is present in the repository. * * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server). */ suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent() /** Checks if the auto on feature is supported. */ suspend fun isAutoOnSupported(): Boolean = bluetoothAutoOnRepository.isAutoOnSupported() /** * Sets enabled or disabled based on the provided value. Loading @@ -45,17 +39,14 @@ constructor( * @param value `true` to enable the feature, `false` to disable it. */ suspend fun setEnabled(value: Boolean) { if (!isValuePresent()) { if (!isAutoOnSupported()) { Log.e(TAG, "Trying to set toggle value while feature not available.") } else { val newValue = if (value) ENABLED else DISABLED bluetoothAutoOnRepository.setAutoOn(newValue) bluetoothAutoOnRepository.setAutoOn(value) } } companion object { private const val TAG = "BluetoothAutoOnInteractor" const val DISABLED = 0 const val ENABLED = 1 } } packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt +76 −49 Original line number Diff line number Diff line Loading @@ -16,22 +16,23 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.bluetooth.BluetoothAdapter import android.util.Log import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext Loading @@ -44,61 +45,87 @@ import kotlinx.coroutines.withContext class BluetoothAutoOnRepository @Inject constructor( private val secureSettings: SecureSettings, private val userRepository: UserRepository, localBluetoothManager: LocalBluetoothManager?, private val bluetoothAdapter: BluetoothAdapter?, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { // Flow representing the auto on setting value for the current user @OptIn(ExperimentalCoroutinesApi::class) internal val isAutoOn: StateFlow<Int> = userRepository.selectedUserInfo .flatMapLatest { userInfo -> secureSettings .observerFlow(userInfo.id, SETTING_NAME) .onStart { emit(Unit) } .map { secureSettings.getIntForUser(SETTING_NAME, UNSET, userInfo.id) } // Flow representing the auto on state for the current user internal val isAutoOn: Flow<Boolean> = localBluetoothManager?.eventManager?.let { eventManager -> conflatedCallbackFlow { val listener = object : BluetoothCallback { override fun onAutoOnStateChanged(autoOnState: Int) { super.onAutoOnStateChanged(autoOnState) if ( autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED || autoOnState == BluetoothAdapter.AUTO_ON_STATE_DISABLED ) { trySendWithFailureLogging( autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED, TAG, "onAutoOnStateChanged" ) } } } .distinctUntilChanged() eventManager.registerCallback(listener) awaitClose { eventManager.unregisterCallback(listener) } } .onStart { emit(isAutoOnEnabled()) } .flowOn(backgroundDispatcher) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), UNSET initialValue = false ) } ?: flowOf(false) /** * Checks if the auto on setting value is ever set for the current user. * Checks if the auto on feature is supported for the current user. * * @return `true` if the setting value is not UNSET, `false` otherwise. * @throws Exception if an error occurs while checking auto-on support. */ suspend fun isValuePresent(): Boolean = suspend fun isAutoOnSupported(): Boolean = withContext(backgroundDispatcher) { secureSettings.getIntForUser( SETTING_NAME, UNSET, userRepository.getSelectedUserInfo().id ) != UNSET try { bluetoothAdapter?.isAutoOnSupported ?: false } catch (e: Exception) { // Server could throw TimeoutException, InterruptedException or ExecutionException Log.e(TAG, "Error calling isAutoOnSupported", e) false } } /** * Sets the Bluetooth Auto-On setting value for the current user. * * @param value The new setting value to be applied. */ suspend fun setAutoOn(value: Int) { /** Sets the Bluetooth Auto-On for the current user. */ suspend fun setAutoOn(value: Boolean) { withContext(backgroundDispatcher) { secureSettings.putIntForUser( SETTING_NAME, value, userRepository.getSelectedUserInfo().id ) try { bluetoothAdapter?.setAutoOnEnabled(value) } catch (e: Exception) { // Server could throw IllegalStateException, TimeoutException, InterruptedException // or ExecutionException Log.e(TAG, "Error calling setAutoOnEnabled", e) } } } private suspend fun isAutoOnEnabled() = withContext(backgroundDispatcher) { try { bluetoothAdapter?.isAutoOnEnabled ?: false } catch (e: Exception) { // Server could throw IllegalStateException, TimeoutException, InterruptedException // or ExecutionException Log.e(TAG, "Error calling isAutoOnEnabled", e) false } } companion object { const val SETTING_NAME = "bluetooth_automatic_turn_on" const val UNSET = -1 private companion object { const val TAG = "BluetoothAutoOnRepository" } } Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java +10 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,16 @@ public interface BluetoothCallback { default void onAclConnectionStateChanged( @NonNull CachedBluetoothDevice cachedDevice, int state) {} /** * Called when the Auto-on state is changed for any user. Listens to intent * {@link android.bluetooth.BluetoothAdapter#ACTION_AUTO_ON_STATE_CHANGED } * * @param state the Auto-on state, the possible values are: * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_ENABLED}, * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_DISABLED} */ default void onAutoOnStateChanged(int state) {} @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "STATE_" }, value = { STATE_DISCONNECTED, Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +19 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,8 @@ public class BluetoothEventManager { addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler()); addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler()); addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); registerAdapterIntentReceiver(); } Loading Loading @@ -552,4 +554,21 @@ public class BluetoothEventManager { dispatchAudioModeChanged(); } } private class AutoOnStateChangedHandler implements Handler { @Override public void onReceive(Context context, Intent intent, BluetoothDevice device) { String action = intent.getAction(); if (action == null) { Log.w(TAG, "AutoOnStateChangedHandler() action is null"); return; } int state = intent.getIntExtra(BluetoothAdapter.EXTRA_AUTO_ON_STATE, BluetoothAdapter.ERROR); for (BluetoothCallback callback : mCallbacks) { callback.onAutoOnStateChanged(state); } } } }
packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; Loading Loading @@ -489,4 +490,17 @@ public class BluetoothEventManagerTest { verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME), eq(R.string.bluetooth_pairing_pin_error_message)); } /** * Intent ACTION_AUTO_ON_STATE_CHANGED should dispatch to callback. */ @Test public void intentWithExtraState_autoOnStateChangedShouldDispatchToRegisterCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); mIntent = new Intent(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED); mContext.sendBroadcast(mIntent); verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); } }
packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt +5 −14 Original line number Diff line number Diff line Loading @@ -19,8 +19,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.util.Log import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */ @SysUISingleton Loading @@ -30,14 +28,10 @@ constructor( private val bluetoothAutoOnRepository: BluetoothAutoOnRepository, ) { val isEnabled = bluetoothAutoOnRepository.isAutoOn.map { it == ENABLED }.distinctUntilChanged() val isEnabled = bluetoothAutoOnRepository.isAutoOn /** * Checks if the auto on value is present in the repository. * * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server). */ suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent() /** Checks if the auto on feature is supported. */ suspend fun isAutoOnSupported(): Boolean = bluetoothAutoOnRepository.isAutoOnSupported() /** * Sets enabled or disabled based on the provided value. Loading @@ -45,17 +39,14 @@ constructor( * @param value `true` to enable the feature, `false` to disable it. */ suspend fun setEnabled(value: Boolean) { if (!isValuePresent()) { if (!isAutoOnSupported()) { Log.e(TAG, "Trying to set toggle value while feature not available.") } else { val newValue = if (value) ENABLED else DISABLED bluetoothAutoOnRepository.setAutoOn(newValue) bluetoothAutoOnRepository.setAutoOn(value) } } companion object { private const val TAG = "BluetoothAutoOnInteractor" const val DISABLED = 0 const val ENABLED = 1 } }
packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt +76 −49 Original line number Diff line number Diff line Loading @@ -16,22 +16,23 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.bluetooth.BluetoothAdapter import android.util.Log import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext Loading @@ -44,61 +45,87 @@ import kotlinx.coroutines.withContext class BluetoothAutoOnRepository @Inject constructor( private val secureSettings: SecureSettings, private val userRepository: UserRepository, localBluetoothManager: LocalBluetoothManager?, private val bluetoothAdapter: BluetoothAdapter?, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { // Flow representing the auto on setting value for the current user @OptIn(ExperimentalCoroutinesApi::class) internal val isAutoOn: StateFlow<Int> = userRepository.selectedUserInfo .flatMapLatest { userInfo -> secureSettings .observerFlow(userInfo.id, SETTING_NAME) .onStart { emit(Unit) } .map { secureSettings.getIntForUser(SETTING_NAME, UNSET, userInfo.id) } // Flow representing the auto on state for the current user internal val isAutoOn: Flow<Boolean> = localBluetoothManager?.eventManager?.let { eventManager -> conflatedCallbackFlow { val listener = object : BluetoothCallback { override fun onAutoOnStateChanged(autoOnState: Int) { super.onAutoOnStateChanged(autoOnState) if ( autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED || autoOnState == BluetoothAdapter.AUTO_ON_STATE_DISABLED ) { trySendWithFailureLogging( autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED, TAG, "onAutoOnStateChanged" ) } } } .distinctUntilChanged() eventManager.registerCallback(listener) awaitClose { eventManager.unregisterCallback(listener) } } .onStart { emit(isAutoOnEnabled()) } .flowOn(backgroundDispatcher) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), UNSET initialValue = false ) } ?: flowOf(false) /** * Checks if the auto on setting value is ever set for the current user. * Checks if the auto on feature is supported for the current user. * * @return `true` if the setting value is not UNSET, `false` otherwise. * @throws Exception if an error occurs while checking auto-on support. */ suspend fun isValuePresent(): Boolean = suspend fun isAutoOnSupported(): Boolean = withContext(backgroundDispatcher) { secureSettings.getIntForUser( SETTING_NAME, UNSET, userRepository.getSelectedUserInfo().id ) != UNSET try { bluetoothAdapter?.isAutoOnSupported ?: false } catch (e: Exception) { // Server could throw TimeoutException, InterruptedException or ExecutionException Log.e(TAG, "Error calling isAutoOnSupported", e) false } } /** * Sets the Bluetooth Auto-On setting value for the current user. * * @param value The new setting value to be applied. */ suspend fun setAutoOn(value: Int) { /** Sets the Bluetooth Auto-On for the current user. */ suspend fun setAutoOn(value: Boolean) { withContext(backgroundDispatcher) { secureSettings.putIntForUser( SETTING_NAME, value, userRepository.getSelectedUserInfo().id ) try { bluetoothAdapter?.setAutoOnEnabled(value) } catch (e: Exception) { // Server could throw IllegalStateException, TimeoutException, InterruptedException // or ExecutionException Log.e(TAG, "Error calling setAutoOnEnabled", e) } } } private suspend fun isAutoOnEnabled() = withContext(backgroundDispatcher) { try { bluetoothAdapter?.isAutoOnEnabled ?: false } catch (e: Exception) { // Server could throw IllegalStateException, TimeoutException, InterruptedException // or ExecutionException Log.e(TAG, "Error calling isAutoOnEnabled", e) false } } companion object { const val SETTING_NAME = "bluetooth_automatic_turn_on" const val UNSET = -1 private companion object { const val TAG = "BluetoothAutoOnRepository" } }