Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2dd4d6af authored by chelseahao's avatar chelseahao
Browse files

Creates a helper class to handle audio stream scanning.

This is to get ready for adding target scanning functionality.

Flag: com.android.settingslib.flags.enable_le_audio_sharing
Test: atest
Bug: 395978182
Change-Id: Iec42588c9016684b2e14f3ca91e748db8027f98c
parent 5372c42f
Loading
Loading
Loading
Loading
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.connecteddevice.audiosharing.audiostreams;

import static android.bluetooth.BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE;

import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_OFF;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_ON;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_TURNING_OFF;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamScanHelper.State.STATE_TURNING_ON;

import static java.util.Collections.emptyList;

import android.annotation.NonNull;
import android.util.Log;

import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;

import java.util.concurrent.Executor;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * Helper class for managing the scanning. It utilizes the
 * {@link LocalBluetoothLeBroadcastAssistant} to initiate and stop scanning and provides callbacks
 * to inform listeners about the scan state.
 */
public class AudioStreamScanHelper implements
        AudioStreamsProgressCategoryCallback.ScanStateListener {
    enum State {
        STATE_OFF,
        STATE_TURNING_ON,
        STATE_ON,
        STATE_TURNING_OFF
    }

    private static final String TAG = "AudioStreamScanHelper";
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
    private final Consumer<Boolean> mScanStateChangedListener;
    private final @NonNull Executor mExecutor;
    private State mState = STATE_OFF;

    AudioStreamScanHelper(@NonNull Executor executor,
            @Nullable LocalBluetoothLeBroadcastAssistant leBroadcastAssistant,
            @Nonnull Consumer<Boolean> scanStateChangedListener) {
        mExecutor = executor;
        mLeBroadcastAssistant = leBroadcastAssistant;
        mScanStateChangedListener = scanStateChangedListener;
    }

    /**
     * Starts the scanning process for available audio stream sources.
     * This method will do nothing if scanning is already active or in the process of starting.
     */
    public void startScanning() {
        mExecutor.execute(() -> {
            if (mState == STATE_ON || mState == STATE_TURNING_ON) {
                Log.d(TAG, "startScanning() : do nothing, state = " + mState);
                return;
            }
            if (mLeBroadcastAssistant != null) {
                Log.d(TAG, "startScanning()");
                mLeBroadcastAssistant.startSearchingForSources(emptyList());
                setState(STATE_TURNING_ON);
            }
        });
    }

    /**
     * Stops the ongoing scanning process for audio stream sources.
     * This method will do nothing if scanning is already off or in the process of stopping.
     */
    public void stopScanning() {
        mExecutor.execute(() -> {
            if (mState == STATE_OFF || mState == STATE_TURNING_OFF) {
                Log.d(TAG, "stopScanning() : do nothing, state = " + mState);
                return;
            }
            if (mLeBroadcastAssistant != null) {
                Log.d(TAG, "stopScanning()");
                mLeBroadcastAssistant.stopSearchingForSources();
                setState(STATE_TURNING_OFF);
            }
        });
    }

    @Override
    public void scanningStarted() {
        mExecutor.execute(() -> {
            Log.d(TAG, "scanningStarted()");
            setState(STATE_ON);
        });
    }

    @Override
    public void scanningStartFailed(int reason) {
        mExecutor.execute(() -> {
            Log.d(TAG, "scanningStartFailed() : reason = " + reason);
            setState(reason == ERROR_ALREADY_IN_TARGET_STATE ? STATE_ON : STATE_OFF);
        });
    }

    @Override
    public void scanningStopped() {
        mExecutor.execute(() -> {
            Log.d(TAG, "scanningStopped()");
            setState(STATE_OFF);
        });
    }

    @Override
    public void scanningStopFailed(int reason) {
        mExecutor.execute(() -> {
            Log.d(TAG, "scanningStopFailed() : reason = " + reason);
            setState(reason == ERROR_ALREADY_IN_TARGET_STATE ? STATE_OFF : STATE_ON);
        });
    }

    private void setState(State newState) {
        mScanStateChangedListener.accept(newState == STATE_ON || newState == STATE_TURNING_ON);
        Log.d(TAG, "setState: from " + mState + " to " + newState);
        mState = newState;
    }
}
+63 −31
Original line number Diff line number Diff line
@@ -21,87 +21,119 @@ import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssista
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothStatusCodes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
    private final AudioStreamsProgressCategoryController mCategoryController;
    @Nullable private SourceStateListener mSourceStateListener = null;
    @Nullable private ScanStateListener mScanStateListener = null;

    public AudioStreamsProgressCategoryCallback(
            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
        mCategoryController = audioStreamsProgressCategoryController;
    void setSourceStateListener(SourceStateListener listener) {
        mSourceStateListener = listener;
    }

    void setScanStateListener(ScanStateListener listener) {
        mScanStateListener = listener;
    }

    @Override
    public void onReceiveStateChanged(
            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
        super.onReceiveStateChanged(sink, sourceId, state);
        if (mSourceStateListener != null) {
            var sourceState = getLocalSourceState(state);
            switch (sourceState) {
            case STREAMING -> mCategoryController.handleSourceStreaming(sink, state);
            case DECRYPTION_FAILED -> mCategoryController.handleSourceConnectBadCode(state);
            case PAUSED -> mCategoryController.handleSourcePaused(sink, state);
                case STREAMING -> mSourceStateListener.handleSourceStreaming(sink, state);
                case DECRYPTION_FAILED -> mSourceStateListener.handleSourceConnectBadCode(state);
                case PAUSED -> mSourceStateListener.handleSourcePaused(sink, state);
                default -> {
                    // Do nothing
                }
            }
        }
    }

    @Override
    public void onSearchStartFailed(int reason) {
        if (reason == BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE) {
            return;
        }
        super.onSearchStartFailed(reason);
        mCategoryController.showToast("Failed to start scanning. Try again.");
        mCategoryController.setScanning(false);
        if (mScanStateListener != null) {
            mScanStateListener.scanningStartFailed(reason);
        }
    }

    @Override
    public void onSearchStarted(int reason) {
        super.onSearchStarted(reason);
        mCategoryController.setScanning(true);
        if (mScanStateListener != null) {
            mScanStateListener.scanningStarted();
        }
    }

    @Override
    public void onSearchStopFailed(int reason) {
        if (reason == BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE) {
            return;
        }
        super.onSearchStopFailed(reason);
        mCategoryController.showToast("Failed to stop scanning. Try again.");
        if (mScanStateListener != null) {
            mScanStateListener.scanningStopFailed(reason);
        }
    }

    @Override
    public void onSearchStopped(int reason) {
        super.onSearchStopped(reason);
        mCategoryController.setScanning(false);
        if (mScanStateListener != null) {
            mScanStateListener.scanningStopped();
        }
    }

    @Override
    public void onSourceAddFailed(
            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
        super.onSourceAddFailed(sink, source, reason);
        mCategoryController.handleSourceFailedToConnect(source.getBroadcastId());
        if (mSourceStateListener != null) {
            mSourceStateListener.handleSourceFailedToConnect(source.getBroadcastId());
        }
    }

    @Override
    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
        super.onSourceFound(source);
        mCategoryController.handleSourceFound(source);
        if (mSourceStateListener != null) {
            mSourceStateListener.handleSourceFound(source);
        }
    }

    @Override
    public void onSourceLost(int broadcastId) {
        super.onSourceLost(broadcastId);
        mCategoryController.handleSourceLost(broadcastId);
        if (mSourceStateListener != null) {
            mSourceStateListener.handleSourceLost(broadcastId);
        }

    @Override
    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
        super.onSourceRemoveFailed(sink, sourceId, reason);
        mCategoryController.showToast("Failed to remove source.");
    }

    @Override
    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
        super.onSourceRemoved(sink, sourceId, reason);
        mCategoryController.handleSourceRemoved();
        if (mSourceStateListener != null) {
            mSourceStateListener.handleSourceRemoved();
        }
    }

    interface ScanStateListener {
        void scanningStarted();
        void scanningStartFailed(int reason);
        void scanningStopped();
        void scanningStopFailed(int reason);
    }

    interface SourceStateListener {
        void handleSourceStreaming(@NonNull BluetoothDevice device,
                @NonNull BluetoothLeBroadcastReceiveState receiveState);
        void handleSourceConnectBadCode(@NonNull BluetoothLeBroadcastReceiveState receiveState);
        void handleSourcePaused(@NonNull BluetoothDevice device,
                @NonNull BluetoothLeBroadcastReceiveState receiveState);
        void handleSourceFailedToConnect(int broadcastId);
        void handleSourceFound(@NonNull BluetoothLeBroadcastMetadata source);
        void handleSourceLost(int broadcastId);
        void handleSourceRemoved();
    }
}
+28 −28
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssista
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toMap;

import android.app.AlertDialog;
@@ -70,8 +69,8 @@ import java.util.concurrent.Executors;

import javax.annotation.Nullable;

public class AudioStreamsProgressCategoryController extends BasePreferenceController
        implements DefaultLifecycleObserver {
public class AudioStreamsProgressCategoryController extends BasePreferenceController implements
        DefaultLifecycleObserver, AudioStreamsProgressCategoryCallback.SourceStateListener {
    private static final String TAG = "AudioStreamsProgressCategoryController";
    private static final boolean DEBUG = BluetoothUtils.D;
    @VisibleForTesting static final int UNSET_BROADCAST_ID = -1;
@@ -152,6 +151,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
    private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
            new ConcurrentHashMap<>();
    private final boolean mHysteresisModeFixAvailable;
    private final AudioStreamScanHelper mScanHelper;
    private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
    private SourceOriginForLogging mSourceFromQrCodeOriginForLogging;
    @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
@@ -165,7 +165,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
        mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
        mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
        mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
        mScanHelper = new AudioStreamScanHelper(mExecutor, mLeBroadcastAssistant,
                this::setScanningIconSpinning);
        mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback();
        mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(
                mContext);
        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -224,7 +226,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
        mSourceFromQrCodeOriginForLogging = sourceOriginForLogging;
    }

    void setScanning(boolean isScanning) {
    void setScanningIconSpinning(boolean isScanning) {
        ThreadUtils.postOnMainThread(
                () -> {
                    if (mCategoryPreference != null) mCategoryPreference.setProgress(isScanning);
@@ -236,7 +238,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
    // 1) No preference existed, create new preference with state SYNCED
    // 2) WAIT_FOR_SYNC, move to ADD_SOURCE_WAIT_FOR_RESPONSE
    // 3) SOURCE_ADDED, leave as-is
    void handleSourceFound(BluetoothLeBroadcastMetadata source) {
    @Override
    public void handleSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
        if (DEBUG) {
            Log.d(TAG, "handleSourceFound()");
        }
@@ -359,7 +362,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
                });
    }

    void handleSourceLost(int broadcastId) {
    @Override
    public void handleSourceLost(int broadcastId) {
        if (DEBUG) {
            Log.d(TAG, "handleSourceLost()");
        }
@@ -381,7 +385,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
        }
    }

    void handleSourceRemoved() {
    @Override
    public void handleSourceRemoved() {
        if (DEBUG) {
            Log.d(TAG, "handleSourceRemoved()");
        }
@@ -420,8 +425,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
    // Expect one of the following:
    // 1) No preference existed, create new preference with state SOURCE_ADDED
    // 2) Any other state, move to SOURCE_ADDED
    void handleSourceStreaming(
            BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
    @Override
    public void handleSourceStreaming(@NonNull BluetoothDevice device,
            @NonNull BluetoothLeBroadcastReceiveState receiveState) {
        if (DEBUG) {
            Log.d(TAG, "handleSourceStreaming()");
        }
@@ -461,7 +467,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro

    // Find preference by receiveState and decide next state.
    // Expect one preference existed, move to ADD_SOURCE_BAD_CODE
    void handleSourceConnectBadCode(BluetoothLeBroadcastReceiveState receiveState) {
    @Override
    public void handleSourceConnectBadCode(
            @NonNull BluetoothLeBroadcastReceiveState receiveState) {
        if (DEBUG) {
            Log.d(TAG, "handleSourceConnectBadCode()");
        }
@@ -478,7 +486,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro

    // Find preference by broadcastId and decide next state.
    // Expect one preference existed, move to ADD_SOURCE_FAILED
    void handleSourceFailedToConnect(int broadcastId) {
    @Override
    public void handleSourceFailedToConnect(int broadcastId) {
        if (DEBUG) {
            Log.d(TAG, "handleSourceFailedToConnect()");
        }
@@ -492,7 +501,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro

    // Find preference by receiveState and decide next state.
    // Expect one preference existed, move to SOURCE_PRESENT
    void handleSourcePaused(
    @Override
    public void handleSourcePaused(
            BluetoothDevice device, BluetoothLeBroadcastReceiveState receiveState) {
        if (DEBUG) {
            Log.d(TAG, "handleSourcePaused()");
@@ -604,11 +614,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
            Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
            return;
        }
        if (mLeBroadcastAssistant.isSearchInProgress()) {
            Log.w(TAG, "startScanning(): scanning still in progress, stop scanning first.");
            stopScanning();
        }
        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
        mBroadcastAssistantCallback.setSourceStateListener(this);
        mBroadcastAssistantCallback.setScanStateListener(mScanHelper);
        mExecutor.execute(
                () -> {
                    // Handle QR code scan, display currently streaming or paused streams then start
@@ -628,10 +636,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
                            (device, stateList) ->
                                    stateList.forEach(
                                            state -> handleSourceStreaming(device, state)));
                    if (DEBUG) {
                        Log.d(TAG, "startScanning()");
                    }
                    mLeBroadcastAssistant.startSearchingForSources(emptyList());
                    mScanHelper.startScanning();
                    mMediaControlHelper.start();
                });
    }
@@ -686,17 +691,12 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
    }

    private void stopScanning() {
        mScanHelper.stopScanning();
        if (mLeBroadcastAssistant == null) {
            Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
            return;
        }
        if (mLeBroadcastAssistant.isSearchInProgress()) {
            if (DEBUG) {
                Log.d(TAG, "stopScanning()");
            }
            mLeBroadcastAssistant.stopSearchingForSources();
        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
        }
        mMediaControlHelper.stop();
        mSourceFromQrCode = null;
    }
+198 −0

File added.

Preview size limit exceeded, changes collapsed.

+17 −41
Original line number Diff line number Diff line
@@ -20,9 +20,7 @@ import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_
import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@@ -61,7 +59,8 @@ public class AudioStreamsProgressCategoryCallbackTest {
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    private final Context mContext = ApplicationProvider.getApplicationContext();
    @Mock private AudioStreamsProgressCategoryController mController;
    @Mock private AudioStreamsProgressCategoryCallback.SourceStateListener mSourceStateListener;
    @Mock private AudioStreamsProgressCategoryCallback.ScanStateListener mScanStateListener;
    @Mock private BluetoothDevice mDevice;
    @Mock private BluetoothLeBroadcastReceiveState mState;
    @Mock private BluetoothLeBroadcastMetadata mMetadata;
@@ -78,7 +77,9 @@ public class AudioStreamsProgressCategoryCallbackTest {
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mCallback = new AudioStreamsProgressCategoryCallback(mController);
        mCallback = new AudioStreamsProgressCategoryCallback();
        mCallback.setSourceStateListener(mSourceStateListener);
        mCallback.setScanStateListener(mScanStateListener);
    }

    @Test
@@ -88,7 +89,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
        when(mState.getBisSyncState()).thenReturn(bisSyncState);
        mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);

        verify(mController).handleSourceStreaming(any(), any());
        verify(mSourceStateListener).handleSourceStreaming(any(), any());
    }

    @Test
@@ -103,7 +104,7 @@ public class AudioStreamsProgressCategoryCallbackTest {
        when(mSourceDevice.getAddress()).thenReturn(address);
        mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);

        verify(mController).handleSourcePaused(any(), any());
        verify(mSourceStateListener).handleSourcePaused(any(), any());
    }

    @Test
@@ -114,53 +115,35 @@ public class AudioStreamsProgressCategoryCallbackTest {
                .thenReturn(BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE);
        mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);

        verify(mController).handleSourceConnectBadCode(any());
        verify(mSourceStateListener).handleSourceConnectBadCode(any());
    }

    @Test
    public void testOnSearchStartFailed() {
        mCallback.onSearchStartFailed(/* reason= */ 0);

        verify(mController).showToast(anyString());
        verify(mController).setScanning(anyBoolean());
    }

    @Test
    public void testOnSearchStartFailed_ignoreAlreadyInTargetState() {
        mCallback.onSearchStartFailed(/* reason= */
                BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE);

        verify(mController, never()).showToast(anyString());
        verify(mController, never()).setScanning(anyBoolean());
        verify(mScanStateListener).scanningStartFailed(eq(0));
    }

    @Test
    public void testOnSearchStarted() {
        mCallback.onSearchStarted(/* reason= */ 0);

        verify(mController).setScanning(anyBoolean());
        verify(mScanStateListener).scanningStarted();
    }

    @Test
    public void testOnSearchStopFailed() {
        mCallback.onSearchStopFailed(/* reason= */ 0);

        verify(mController).showToast(anyString());
    }

    @Test
    public void testOnSearchStopFailed_ignoreAlreadyInTargetState() {
        mCallback.onSearchStopFailed(/* reason= */
                BluetoothStatusCodes.ERROR_ALREADY_IN_TARGET_STATE);

        verify(mController, never()).showToast(anyString());
        verify(mScanStateListener).scanningStopFailed(eq(0));
    }

    @Test
    public void testOnSearchStopped() {
        mCallback.onSearchStopped(/* reason= */ 0);

        verify(mController).setScanning(anyBoolean());
        verify(mScanStateListener).scanningStopped();
    }

    @Test
@@ -168,34 +151,27 @@ public class AudioStreamsProgressCategoryCallbackTest {
        when(mMetadata.getBroadcastId()).thenReturn(1);
        mCallback.onSourceAddFailed(mDevice, mMetadata, /* reason= */ 0);

        verify(mController).handleSourceFailedToConnect(1);
        verify(mSourceStateListener).handleSourceFailedToConnect(1);
    }

    @Test
    public void testOnSourceFound() {
        mCallback.onSourceFound(mMetadata);

        verify(mController).handleSourceFound(mMetadata);
        verify(mSourceStateListener).handleSourceFound(mMetadata);
    }

    @Test
    public void testOnSourceLost() {
        mCallback.onSourceLost(/* broadcastId= */ 1);

        verify(mController).handleSourceLost(1);
    }

    @Test
    public void testOnSourceRemoveFailed() {
        mCallback.onSourceRemoveFailed(mDevice, /* sourceId= */ 0, /* reason= */ 0);

        verify(mController).showToast(anyString());
        verify(mSourceStateListener).handleSourceLost(1);
    }

    @Test
    public void testOnSourceRemoved() {
        mCallback.onSourceRemoved(mDevice, /* sourceId= */ 0, /* reason= */ 0);

        verify(mController).handleSourceRemoved();
        verify(mSourceStateListener).handleSourceRemoved();
    }
}
Loading