Loading packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerCallbackExt.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.bluetooth import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch /** [Flow] for [LocalBluetoothProfileManager.ServiceListener] service state changes */ val LocalBluetoothProfileManager.onServiceStateChanged: Flow<Unit> get() = callbackFlow { val listener = object : LocalBluetoothProfileManager.ServiceListener { override fun onServiceConnected() { launch { trySend(Unit) } } override fun onServiceDisconnected() { launch { trySend(Unit) } } } addServiceListener(listener) awaitClose { removeServiceListener(listener) } } .buffer(capacity = Channel.CONFLATED) No newline at end of file packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +99 −56 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.bluetooth.onProfileConnectionStateChanged import com.android.settingslib.bluetooth.onServiceStateChanged import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN Loading Loading @@ -90,13 +91,24 @@ class AudioSharingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : AudioSharingRepository { private val isAudioSharingProfilesReady: StateFlow<Boolean> = btManager.profileManager.onServiceStateChanged .map { isAudioSharingProfilesReady() } .onStart { emit(isAudioSharingProfilesReady()) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) override val inAudioSharing: Flow<Boolean> = btManager.profileManager.leAudioBroadcastProfile?.let { broadcast -> broadcast.onBroadcastStartedOrStopped isAudioSharingProfilesReady.flatMapLatest { ready -> if (ready) { btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped .map { isBroadcasting() } .onStart { emit(isBroadcasting()) } .flowOn(backgroundCoroutineContext) } ?: flowOf(false) } else { flowOf(false) } } private val primaryChange: Flow<Unit> = callbackFlow { val callback = Loading @@ -108,7 +120,8 @@ class AudioSharingRepositoryImpl( contentResolver.registerContentObserver( Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), false, callback) callback ) awaitClose { contentResolver.unregisterContentObserver(callback) } } Loading @@ -120,13 +133,20 @@ class AudioSharingRepositoryImpl( .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver)) BluetoothCsipSetCoordinator.GROUP_ID_INVALID ) override val secondaryGroupId: StateFlow<Int> = merge( isAudioSharingProfilesReady.flatMapLatest { ready -> if (ready) { btManager.profileManager.leAudioBroadcastAssistantProfile ?.onSourceConnectedOrRemoved ?.map { getSecondaryGroupId() } ?: emptyFlow(), .onSourceConnectedOrRemoved .map { getSecondaryGroupId() } } else { emptyFlow() } }, btManager.eventManager.onProfileConnectionStateChanged .filter { profileConnection -> profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED && Loading @@ -137,10 +157,13 @@ class AudioSharingRepositoryImpl( primaryGroupId.map { getSecondaryGroupId() }) .onStart { emit(getSecondaryGroupId()) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId()) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), BluetoothCsipSetCoordinator.GROUP_ID_INVALID ) override val volumeMap: StateFlow<GroupIdToVolumes> = (btManager.profileManager.volumeControlProfile?.let { volumeControl -> inAudioSharing.flatMapLatest { isSharing -> if (isSharing) { callbackFlow { Loading @@ -150,7 +173,8 @@ class AudioSharingRepositoryImpl( device: BluetoothDevice, @IntRange( from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) to = AUDIO_SHARING_VOLUME_MAX.toLong() ) volume: Int ) { launch { send(Pair(device, volume)) } Loading @@ -158,14 +182,20 @@ class AudioSharingRepositoryImpl( } // Once registered, we will receive the initial volume of all // connected BT devices on VolumeControlProfile via callbacks volumeControl.registerCallback( ConcurrentUtils.DIRECT_EXECUTOR, callback) awaitClose { volumeControl.unregisterCallback(callback) } btManager.profileManager.volumeControlProfile.registerCallback( ConcurrentUtils.DIRECT_EXECUTOR, callback ) awaitClose { btManager.profileManager.volumeControlProfile.unregisterCallback( callback ) } } .runningFold(emptyMap<Int, Int>()) { acc, value -> val groupId = BluetoothUtils.getGroupId( btManager.cachedDeviceManager.findDevice(value.first)) btManager.cachedDeviceManager.findDevice(value.first) ) if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { acc + Pair(groupId, value.second) } else { Loading @@ -177,7 +207,6 @@ class AudioSharingRepositoryImpl( emptyFlow() } } } ?: emptyFlow()) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap()) override suspend fun setSecondaryVolume( Loading @@ -196,12 +225,25 @@ class AudioSharingRepositoryImpl( } } private fun isBroadcastProfileReady(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isProfileReady ?: false private fun isAssistantProfileReady(): Boolean = btManager.profileManager.leAudioBroadcastAssistantProfile?.isProfileReady ?: false private fun isVolumeControlProfileReady(): Boolean = btManager.profileManager.volumeControlProfile?.isProfileReady ?: false private fun isAudioSharingProfilesReady(): Boolean = isBroadcastProfileReady() && isAssistantProfileReady() && isVolumeControlProfileReady() private fun isBroadcasting(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isEnabled(null) ?: false private fun getSecondaryGroupId(): Int = BluetoothUtils.getGroupId( BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager)) BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) ) } class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { Loading @@ -215,5 +257,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) volume: Int ) {} ) { } } packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt +47 −4 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.Spy Loading Loading @@ -145,8 +146,11 @@ class AudioSharingRepositoryTest { } @Test fun audioSharingStateChange_emitValues() { fun audioSharingStateChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() Loading @@ -155,7 +159,19 @@ class AudioSharingRepositoryTest { triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted) runCurrent() Truth.assertThat(states).containsExactly(true, false, true) Truth.assertThat(states).containsExactly(false, true, false, true) } } @Test fun audioSharingStateChange_profileNotReady_broadcastCallbackNotRegistered() { testScope.runTest { val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() verify(broadcast, never()).registerServiceCallBack(any(), any()) Truth.assertThat(states).containsExactly(false) } } Loading @@ -176,8 +192,21 @@ class AudioSharingRepositoryTest { } @Test fun secondaryGroupIdChange_emitValues() { fun secondaryGroupIdChange_profileNotReady_assistantCallbackNotRegistered() { testScope.runTest { val groupIds = mutableListOf<Int?>() underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) runCurrent() verify(assistant, never()).registerServiceCallBack(any(), any()) } } @Test fun secondaryGroupIdChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val groupIds = mutableListOf<Int?>() underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) runCurrent() Loading Loading @@ -211,8 +240,11 @@ class AudioSharingRepositoryTest { } @Test fun volumeMapChange_emitValues() { fun volumeMapChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val volumeMaps = mutableListOf<GroupIdToVolumes?>() underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) runCurrent() Loading @@ -233,6 +265,16 @@ class AudioSharingRepositoryTest { } } @Test fun volumeMapChange_profileNotReady_volumeControlCallbackNotRegistered() { testScope.runTest { val volumeMaps = mutableListOf<GroupIdToVolumes?>() underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) runCurrent() verify(volumeControl, never()).registerCallback(any(), any()) } } @Test fun setSecondaryVolume_setValue() { testScope.runTest { Loading @@ -258,6 +300,7 @@ class AudioSharingRepositoryTest { `when`(broadcast.isEnabled(null)).thenReturn(true) broadcastCallbackCaptor.value.broadcastAction() } TriggerType.BROADCAST_STOP -> { `when`(broadcast.isEnabled(null)).thenReturn(false) broadcastCallbackCaptor.value.broadcastAction() Loading Loading
packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerCallbackExt.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.bluetooth import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch /** [Flow] for [LocalBluetoothProfileManager.ServiceListener] service state changes */ val LocalBluetoothProfileManager.onServiceStateChanged: Flow<Unit> get() = callbackFlow { val listener = object : LocalBluetoothProfileManager.ServiceListener { override fun onServiceConnected() { launch { trySend(Unit) } } override fun onServiceDisconnected() { launch { trySend(Unit) } } } addServiceListener(listener) awaitClose { removeServiceListener(listener) } } .buffer(capacity = Channel.CONFLATED) No newline at end of file
packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +99 −56 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.bluetooth.onProfileConnectionStateChanged import com.android.settingslib.bluetooth.onServiceStateChanged import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN Loading Loading @@ -90,13 +91,24 @@ class AudioSharingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : AudioSharingRepository { private val isAudioSharingProfilesReady: StateFlow<Boolean> = btManager.profileManager.onServiceStateChanged .map { isAudioSharingProfilesReady() } .onStart { emit(isAudioSharingProfilesReady()) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) override val inAudioSharing: Flow<Boolean> = btManager.profileManager.leAudioBroadcastProfile?.let { broadcast -> broadcast.onBroadcastStartedOrStopped isAudioSharingProfilesReady.flatMapLatest { ready -> if (ready) { btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped .map { isBroadcasting() } .onStart { emit(isBroadcasting()) } .flowOn(backgroundCoroutineContext) } ?: flowOf(false) } else { flowOf(false) } } private val primaryChange: Flow<Unit> = callbackFlow { val callback = Loading @@ -108,7 +120,8 @@ class AudioSharingRepositoryImpl( contentResolver.registerContentObserver( Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), false, callback) callback ) awaitClose { contentResolver.unregisterContentObserver(callback) } } Loading @@ -120,13 +133,20 @@ class AudioSharingRepositoryImpl( .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver)) BluetoothCsipSetCoordinator.GROUP_ID_INVALID ) override val secondaryGroupId: StateFlow<Int> = merge( isAudioSharingProfilesReady.flatMapLatest { ready -> if (ready) { btManager.profileManager.leAudioBroadcastAssistantProfile ?.onSourceConnectedOrRemoved ?.map { getSecondaryGroupId() } ?: emptyFlow(), .onSourceConnectedOrRemoved .map { getSecondaryGroupId() } } else { emptyFlow() } }, btManager.eventManager.onProfileConnectionStateChanged .filter { profileConnection -> profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED && Loading @@ -137,10 +157,13 @@ class AudioSharingRepositoryImpl( primaryGroupId.map { getSecondaryGroupId() }) .onStart { emit(getSecondaryGroupId()) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId()) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), BluetoothCsipSetCoordinator.GROUP_ID_INVALID ) override val volumeMap: StateFlow<GroupIdToVolumes> = (btManager.profileManager.volumeControlProfile?.let { volumeControl -> inAudioSharing.flatMapLatest { isSharing -> if (isSharing) { callbackFlow { Loading @@ -150,7 +173,8 @@ class AudioSharingRepositoryImpl( device: BluetoothDevice, @IntRange( from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) to = AUDIO_SHARING_VOLUME_MAX.toLong() ) volume: Int ) { launch { send(Pair(device, volume)) } Loading @@ -158,14 +182,20 @@ class AudioSharingRepositoryImpl( } // Once registered, we will receive the initial volume of all // connected BT devices on VolumeControlProfile via callbacks volumeControl.registerCallback( ConcurrentUtils.DIRECT_EXECUTOR, callback) awaitClose { volumeControl.unregisterCallback(callback) } btManager.profileManager.volumeControlProfile.registerCallback( ConcurrentUtils.DIRECT_EXECUTOR, callback ) awaitClose { btManager.profileManager.volumeControlProfile.unregisterCallback( callback ) } } .runningFold(emptyMap<Int, Int>()) { acc, value -> val groupId = BluetoothUtils.getGroupId( btManager.cachedDeviceManager.findDevice(value.first)) btManager.cachedDeviceManager.findDevice(value.first) ) if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { acc + Pair(groupId, value.second) } else { Loading @@ -177,7 +207,6 @@ class AudioSharingRepositoryImpl( emptyFlow() } } } ?: emptyFlow()) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap()) override suspend fun setSecondaryVolume( Loading @@ -196,12 +225,25 @@ class AudioSharingRepositoryImpl( } } private fun isBroadcastProfileReady(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isProfileReady ?: false private fun isAssistantProfileReady(): Boolean = btManager.profileManager.leAudioBroadcastAssistantProfile?.isProfileReady ?: false private fun isVolumeControlProfileReady(): Boolean = btManager.profileManager.volumeControlProfile?.isProfileReady ?: false private fun isAudioSharingProfilesReady(): Boolean = isBroadcastProfileReady() && isAssistantProfileReady() && isVolumeControlProfileReady() private fun isBroadcasting(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isEnabled(null) ?: false private fun getSecondaryGroupId(): Int = BluetoothUtils.getGroupId( BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager)) BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) ) } class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { Loading @@ -215,5 +257,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) volume: Int ) {} ) { } }
packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt +47 −4 Original line number Diff line number Diff line Loading @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.Spy Loading Loading @@ -145,8 +146,11 @@ class AudioSharingRepositoryTest { } @Test fun audioSharingStateChange_emitValues() { fun audioSharingStateChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() Loading @@ -155,7 +159,19 @@ class AudioSharingRepositoryTest { triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted) runCurrent() Truth.assertThat(states).containsExactly(true, false, true) Truth.assertThat(states).containsExactly(false, true, false, true) } } @Test fun audioSharingStateChange_profileNotReady_broadcastCallbackNotRegistered() { testScope.runTest { val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() verify(broadcast, never()).registerServiceCallBack(any(), any()) Truth.assertThat(states).containsExactly(false) } } Loading @@ -176,8 +192,21 @@ class AudioSharingRepositoryTest { } @Test fun secondaryGroupIdChange_emitValues() { fun secondaryGroupIdChange_profileNotReady_assistantCallbackNotRegistered() { testScope.runTest { val groupIds = mutableListOf<Int?>() underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) runCurrent() verify(assistant, never()).registerServiceCallBack(any(), any()) } } @Test fun secondaryGroupIdChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val groupIds = mutableListOf<Int?>() underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) runCurrent() Loading Loading @@ -211,8 +240,11 @@ class AudioSharingRepositoryTest { } @Test fun volumeMapChange_emitValues() { fun volumeMapChange_profileReady_emitValues() { testScope.runTest { `when`(broadcast.isProfileReady).thenReturn(true) `when`(assistant.isProfileReady).thenReturn(true) `when`(volumeControl.isProfileReady).thenReturn(true) val volumeMaps = mutableListOf<GroupIdToVolumes?>() underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) runCurrent() Loading @@ -233,6 +265,16 @@ class AudioSharingRepositoryTest { } } @Test fun volumeMapChange_profileNotReady_volumeControlCallbackNotRegistered() { testScope.runTest { val volumeMaps = mutableListOf<GroupIdToVolumes?>() underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) runCurrent() verify(volumeControl, never()).registerCallback(any(), any()) } } @Test fun setSecondaryVolume_setValue() { testScope.runTest { Loading @@ -258,6 +300,7 @@ class AudioSharingRepositoryTest { `when`(broadcast.isEnabled(null)).thenReturn(true) broadcastCallbackCaptor.value.broadcastAction() } TriggerType.BROADCAST_STOP -> { `when`(broadcast.isEnabled(null)).thenReturn(false) broadcastCallbackCaptor.value.broadcastAction() Loading