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

Commit db07bfcb authored by Alex Shabalin's avatar Alex Shabalin
Browse files

Move suggestion device chip state management into a separate class

The suggestion device chip is only used by the Media Controls and the
state management is specific to that piece of UI. Moving that logic out
of generic InfoMediaManager to SuggestedDeviceManager for the ease of
maintenance.

Bug: 428663871
Test: atest SuggestedDeviceManagerTest
Test: atest LocalMediaManagerTest InfoMediaManagerTest
Test: atest MediaDeviceManagerTest
Test: On a physical device
Flag: com.android.media.flags.enable_suggested_device_api
Change-Id: I1d32370dd0c7274a37828584f9de6b657d70c70d
parent 69e91bb1
Loading
Loading
Loading
Loading
+21 −129
Original line number Original line Diff line number Diff line
@@ -18,10 +18,7 @@ package com.android.settingslib.media;
import static android.media.MediaRoute2Info.CONNECTION_STATE_CONNECTING;
import static android.media.MediaRoute2Info.CONNECTION_STATE_CONNECTING;
import static android.media.session.MediaController.PlaybackInfo;
import static android.media.session.MediaController.PlaybackInfo;


import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
import static com.android.settingslib.media.MediaDeviceUtilKt.isBluetoothMediaDevice;
import static com.android.settingslib.media.MediaDeviceUtilKt.isBluetoothMediaDevice;
import static com.android.settingslib.media.MediaDeviceUtilKt.isComplexMediaDevice;
import static com.android.settingslib.media.MediaDeviceUtilKt.isComplexMediaDevice;
@@ -64,7 +61,6 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -113,9 +109,14 @@ public abstract class InfoMediaManager {
         */
         */
        void onRequestFailed(int reason);
        void onRequestFailed(int reason);


        /** Callback for notifying that the suggested device has been updated. */
        /**
        default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState suggestedDevice) {}
         * Callback for changes to the suggested device list.
        ;
         *
         * @param deviceSuggestions the list of suggested devices.
         */
        default void onDeviceSuggestionsUpdated(
                @NonNull List<SuggestedDeviceInfo> deviceSuggestions) {
        }
    }
    }




@@ -144,8 +145,6 @@ public abstract class InfoMediaManager {
            new ConcurrentHashMap<>();
            new ConcurrentHashMap<>();
    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new HashMap<>();
    private final Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceMap = new HashMap<>();
    @GuardedBy("mLock")
    @Nullable private SuggestedDeviceState mSuggestedDeviceState;


    private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
    private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();


@@ -298,7 +297,7 @@ public abstract class InfoMediaManager {


    protected final void rebuildDeviceList() {
    protected final void rebuildDeviceList() {
        buildAvailableRoutes();
        buildAvailableRoutes();
        updateDeviceSuggestion();
        updateMediaDevicesSuggestionState();
    }
    }


    protected final void notifyCurrentConnectedDeviceChanged() {
    protected final void notifyCurrentConnectedDeviceChanged() {
@@ -610,13 +609,6 @@ public abstract class InfoMediaManager {
        return getActiveRoutingSession().getName();
        return getActiveRoutingSession().getName();
    }
    }


    @Nullable
    public SuggestedDeviceState getSuggestedDevice() {
        synchronized (mLock) {
            return mSuggestedDeviceState;
        }
    }

    /** Requests a suggestion from other routers. */
    /** Requests a suggestion from other routers. */
    public abstract void requestDeviceSuggestion();
    public abstract void requestDeviceSuggestion();


@@ -628,6 +620,9 @@ public abstract class InfoMediaManager {


    protected void notifyDeviceSuggestionUpdated(
    protected void notifyDeviceSuggestionUpdated(
            String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) {
            String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestions) {
        if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) {
            return;
        }
        synchronized (mLock) {
        synchronized (mLock) {
            if (suggestions == null) {
            if (suggestions == null) {
                mSuggestedDeviceMap.remove(suggestingPackageName);
                mSuggestedDeviceMap.remove(suggestingPackageName);
@@ -635,116 +630,21 @@ public abstract class InfoMediaManager {
                mSuggestedDeviceMap.put(suggestingPackageName, suggestions);
                mSuggestedDeviceMap.put(suggestingPackageName, suggestions);
            }
            }
        }
        }
        updateDeviceSuggestion();
    }

    private void updateDeviceSuggestion() {
        if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) {
            return;
        }
        if (updateSuggestedDeviceState()) {
            dispatchOnSuggestedDeviceUpdated();
        }
        if (updateMediaDevicesSuggestionState()) {
        if (updateMediaDevicesSuggestionState()) {
            dispatchDeviceListAdded(getMediaDevices());
            dispatchDeviceListAdded(getMediaDevices());
        }
        }
        dispatchOnDeviceSuggestionsUpdated();
    }
    }


    private boolean updateSuggestedDeviceState() {
    private void dispatchOnDeviceSuggestionsUpdated() {
        if (!com.android.media.flags.Flags.enableSuggestedDeviceApi()) {
        Log.i(TAG, "dispatchDeviceSuggestionsUpdated()");
            return false;
        }
        SuggestedDeviceInfo topSuggestion = null;
        SuggestedDeviceState newSuggestedDeviceState = null;
        SuggestedDeviceState previousState = getSuggestedDevice();
        List<SuggestedDeviceInfo> suggestions = getSuggestions();
        if (suggestions != null && !suggestions.isEmpty()) {
            topSuggestion = suggestions.get(0);
        }
        if (topSuggestion != null) {
            synchronized (mLock) {
                for (MediaDevice device : mMediaDevices) {
                    if (Objects.equals(device.getId(), topSuggestion.getRouteId())) {
                        newSuggestedDeviceState =
                                new SuggestedDeviceState(topSuggestion, device.getState());
                        break;
                    }
                }
            }
            if (newSuggestedDeviceState == null) {
                if (previousState != null
                        && topSuggestion
                                .getRouteId()
                                .equals(previousState.getSuggestedDeviceInfo().getRouteId())) {
                    return false;
                }
                newSuggestedDeviceState = new SuggestedDeviceState(topSuggestion);
            }
        }
        if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) {
            newSuggestedDeviceState = null;
        }
        if (!Objects.equals(previousState, newSuggestedDeviceState)) {
            synchronized (mLock) {
                mSuggestedDeviceState = newSuggestedDeviceState;
            }
            return true;
        }
        return false;
    }

    private boolean isSuggestedDeviceSelected(
            @NonNull SuggestedDeviceState newSuggestedDeviceState) {
        synchronized (mLock) {
            return mMediaDevices.stream().anyMatch(device ->
                    device.isSelected()
                            && Objects.equals(
                            device.getId(),
                            newSuggestedDeviceState
                                    .getSuggestedDeviceInfo()
                                    .getRouteId()));
        }
    }

    final void onConnectionAttemptedForSuggestion(@NonNull SuggestedDeviceState suggestion) {
        synchronized (mLock) {
            if (!Objects.equals(suggestion, mSuggestedDeviceState)) {
                return;
            }
            if (mSuggestedDeviceState.getConnectionState() != STATE_DISCONNECTED
                    && mSuggestedDeviceState.getConnectionState() != STATE_CONNECTING_FAILED) {
                return;
            }
            mSuggestedDeviceState =
                    new SuggestedDeviceState(
                            mSuggestedDeviceState.getSuggestedDeviceInfo(), STATE_CONNECTING);
        }
        dispatchOnSuggestedDeviceUpdated();
    }

    final void onConnectionAttemptCompletedForSuggestion(
            @NonNull SuggestedDeviceState suggestion, boolean success) {
        synchronized (mLock) {
            if (!Objects.equals(suggestion, mSuggestedDeviceState)) {
                return;
            }
            int state = success ? STATE_CONNECTED : STATE_CONNECTING_FAILED;
            mSuggestedDeviceState =
                    new SuggestedDeviceState(mSuggestedDeviceState.getSuggestedDeviceInfo(), state);
        }
        dispatchOnSuggestedDeviceUpdated();
    }

    private void dispatchOnSuggestedDeviceUpdated() {
        SuggestedDeviceState state = getSuggestedDevice();
        Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: " + state);
        for (MediaDeviceCallback callback : mCallbacks) {
        for (MediaDeviceCallback callback : mCallbacks) {
            callback.onSuggestedDeviceUpdated(state);
            callback.onDeviceSuggestionsUpdated(getSuggestions());
        }
        }
    }
    }


    @Nullable
    @NonNull
    private List<SuggestedDeviceInfo> getSuggestions() {
    List<SuggestedDeviceInfo> getSuggestions() {
        // Give suggestions in the following order
        // Give suggestions in the following order
        // 1. Suggestions from the local router
        // 1. Suggestions from the local router
        // 2. Suggestions from the proxy router if only one proxy router is providing suggestions
        // 2. Suggestions from the proxy router if only one proxy router is providing suggestions
@@ -762,7 +662,7 @@ public abstract class InfoMediaManager {
                }
                }
            }
            }
        }
        }
        return null;
        return List.of();
    }
    }


    // Go through all current MediaDevices, and update the ones that are suggested.
    // Go through all current MediaDevices, and update the ones that are suggested.
@@ -772,12 +672,9 @@ public abstract class InfoMediaManager {
        }
        }
        Set<String> suggestedDevices = new HashSet<>();
        Set<String> suggestedDevices = new HashSet<>();
        // Prioritize suggestions from the package, otherwise pick any.
        // Prioritize suggestions from the package, otherwise pick any.
        List<SuggestedDeviceInfo> suggestions = getSuggestions();
        for (SuggestedDeviceInfo suggestion : getSuggestions()) {
        if (suggestions != null) {
            for (SuggestedDeviceInfo suggestion : suggestions) {
            suggestedDevices.add(suggestion.getRouteId());
            suggestedDevices.add(suggestion.getRouteId());
        }
        }
        }
        boolean didUpdate = false;
        boolean didUpdate = false;
        synchronized (mLock) {
        synchronized (mLock) {
            for (MediaDevice device : mMediaDevices) {
            for (MediaDevice device : mMediaDevices) {
@@ -948,11 +845,6 @@ public abstract class InfoMediaManager {
            return;
            return;
        }
        }
        device.setState(state);
        device.setState(state);
        if (device.isSuggestedDevice()) {
            if (updateSuggestedDeviceState()) {
                dispatchOnSuggestedDeviceUpdated();
            }
        }
    }
    }


    @RequiresApi(34)
    @RequiresApi(34)
+50 −19
Original line number Original line Diff line number Diff line
@@ -29,6 +29,7 @@ import android.media.AudioManager;
import android.media.RoutingChangeInfo;
import android.media.RoutingChangeInfo;
import android.media.RoutingChangeInfo.EntryPoint;
import android.media.RoutingChangeInfo.EntryPoint;
import android.media.RoutingSessionInfo;
import android.media.RoutingSessionInfo;
import android.media.SuggestedDeviceInfo;
import android.os.Build;
import android.os.Build;
import android.os.Handler;
import android.os.Handler;
import android.text.TextUtils;
import android.text.TextUtils;
@@ -55,7 +56,6 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collection;
import java.util.List;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArrayList;


/**
/**
@@ -255,11 +255,6 @@ public class LocalMediaManager implements BluetoothCallback {
            if (mConnectingSuggestedDeviceState != null) {
            if (mConnectingSuggestedDeviceState != null) {
                return;
                return;
            }
            }
            SuggestedDeviceState currentSuggestion = mInfoMediaManager.getSuggestedDevice();
            if (!Objects.equals(suggestion, currentSuggestion)) {
                Log.w(TAG, "Suggestion got changed, aborting connection.");
                return;
            }
            for (MediaDevice device : mMediaDevices) {
            for (MediaDevice device : mMediaDevices) {
                if (suggestion.getSuggestedDeviceInfo().getRouteId().equals(device.getId())) {
                if (suggestion.getSuggestedDeviceInfo().getRouteId().equals(device.getId())) {
                    Log.i(TAG, "Suggestion: device is available, connecting. deviceId = "
                    Log.i(TAG, "Suggestion: device is available, connecting. deviceId = "
@@ -270,7 +265,7 @@ public class LocalMediaManager implements BluetoothCallback {
            }
            }
            mConnectingSuggestedDeviceState =
            mConnectingSuggestedDeviceState =
                    new ConnectingSuggestedDeviceState(
                    new ConnectingSuggestedDeviceState(
                            currentSuggestion, routingChangeInfo.getEntryPoint());
                            suggestion, routingChangeInfo.getEntryPoint());
            mConnectingSuggestedDeviceState.tryConnect();
            mConnectingSuggestedDeviceState.tryConnect();
        }
        }
    }
    }
@@ -292,9 +287,9 @@ public class LocalMediaManager implements BluetoothCallback {
        }
        }
    }
    }


    @Nullable
    @NonNull
    public SuggestedDeviceState getSuggestedDevice() {
    public List<SuggestedDeviceInfo> getSuggestions() {
        return mInfoMediaManager.getSuggestedDevice();
        return mInfoMediaManager.getSuggestions();
    }
    }


    void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
    void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
@@ -356,9 +351,21 @@ public class LocalMediaManager implements BluetoothCallback {
        }
        }
    }
    }


    void dispatchOnSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {
    void dispatchDeviceSuggestionsUpdated(List<SuggestedDeviceInfo> deviceSuggestions) {
        for (DeviceCallback callback : getCallbacks()) {
            callback.onDeviceSuggestionsUpdated(deviceSuggestions);
        }
    }

    void dispatchConnectSuggestedDeviceFinished(SuggestedDeviceState state, boolean success) {
        for (DeviceCallback callback : getCallbacks()) {
            callback.onConnectSuggestedDeviceFinished(state, success);
        }
    }

    void dispatchConnectionAttemptedForSuggestion(SuggestedDeviceState state) {
        for (DeviceCallback callback : getCallbacks()) {
        for (DeviceCallback callback : getCallbacks()) {
            callback.onSuggestedDeviceUpdated(device);
            callback.onConnectionAttemptedForSuggestion(state);
        }
        }
    }
    }


@@ -410,6 +417,17 @@ public class LocalMediaManager implements BluetoothCallback {
        return null;
        return null;
    }
    }


    /**
     * Returns a list of MediaDevice objects.
     *
     * @return a list of media devices
     */
    public List<MediaDevice> getMediaDevices() {
        synchronized (mMediaDevicesLock) {
            return new ArrayList<>(mMediaDevices);
        }
    }

    /**
    /**
     * Find the current connected MediaDevice.
     * Find the current connected MediaDevice.
     *
     *
@@ -769,8 +787,9 @@ public class LocalMediaManager implements BluetoothCallback {
        }
        }


        @Override
        @Override
        public void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {
        public void onDeviceSuggestionsUpdated(
            dispatchOnSuggestedDeviceUpdated(device);
                @NonNull List<SuggestedDeviceInfo> deviceSuggestions) {
            dispatchDeviceSuggestionsUpdated(deviceSuggestions);
        }
        }
    }
    }


@@ -850,8 +869,20 @@ public class LocalMediaManager implements BluetoothCallback {
         */
         */
        default void onAboutToConnectDeviceRemoved() {}
        default void onAboutToConnectDeviceRemoved() {}


        /** Callback for notifying that the suggested device has been updated. */
        /** Callback for notifying that the suggested device list has been updated. */
        default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {}
        default void onDeviceSuggestionsUpdated(
                @NonNull List<SuggestedDeviceInfo> deviceSuggestions) {
        }

        /** Callback for notifying that connection to suggested device is finished. */
        default void onConnectSuggestedDeviceFinished(
                @NonNull SuggestedDeviceState suggestedDeviceState, boolean success) {
        }

        /** Callback for notifying that connection to suggested device is started. */
        default void onConnectionAttemptedForSuggestion(
                @NonNull SuggestedDeviceState suggestedDeviceState) {
        }
    }
    }


    /**
    /**
@@ -951,8 +982,8 @@ public class LocalMediaManager implements BluetoothCallback {
                        stopScan();
                        stopScan();
                        Log.i(TAG, "Suggestion: scan stopped. success = "
                        Log.i(TAG, "Suggestion: scan stopped. success = "
                                + mDidAttemptCompleteSuccessfully);
                                + mDidAttemptCompleteSuccessfully);
                        mInfoMediaManager.onConnectionAttemptCompletedForSuggestion(
                        dispatchConnectSuggestedDeviceFinished(mSuggestedDeviceState,
                                mSuggestedDeviceState, mDidAttemptCompleteSuccessfully);
                                mDidAttemptCompleteSuccessfully);
                    };
                    };
        }
        }


@@ -968,7 +999,7 @@ public class LocalMediaManager implements BluetoothCallback {
            startScan();
            startScan();
            mConnectSuggestedDeviceHandler.postDelayed(
            mConnectSuggestedDeviceHandler.postDelayed(
                    mConnectionAttemptFinishedRunnable, SCAN_DURATION_MS);
                    mConnectionAttemptFinishedRunnable, SCAN_DURATION_MS);
            mInfoMediaManager.onConnectionAttemptedForSuggestion(mSuggestedDeviceState);
            dispatchConnectionAttemptedForSuggestion(mSuggestedDeviceState);
        }
        }
    }
    }
}
}
+213 −0
Original line number Original line 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.settingslib.media

import android.media.RoutingChangeInfo
import android.media.SuggestedDeviceInfo
import android.util.Log
import androidx.annotation.GuardedBy
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED
import java.util.concurrent.CopyOnWriteArraySet

private const val TAG = "SuggestedDeviceManager"

/**
 * Provides data to render and handles user interactions for the suggested device chip within the
 * Android Media Controls.
 *
 * This class exposes the [SuggestedDeviceState] which is calculated based on:
 * - Lists of device suggestions and media routes (media devices) provided by the Media Router.
 * - User interactions with the suggested device chip.
 * - The results of user-initiated connection attempts to these devices.
 */
class SuggestedDeviceManager(private val localMediaManager: LocalMediaManager) {
  private val lock: Any = Object()
  private val listeners = CopyOnWriteArraySet<Listener>()
  @GuardedBy("lock") private var mediaDevices: List<MediaDevice> = listOf()
  @GuardedBy("lock") private var suggestions: List<SuggestedDeviceInfo> = listOf()
  @GuardedBy("lock") private var suggestedDeviceState: SuggestedDeviceState? = null

  private val localMediaManagerDeviceCallback =
    object : LocalMediaManager.DeviceCallback {
      override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) {
        val stateChanged = synchronized(lock) {
          mediaDevices = newDevices?.toList() ?: listOf()
          updateSuggestedDeviceStateLocked()
        }
        if (stateChanged) {
          dispatchOnSuggestedDeviceUpdated()
        }
      }

      override fun onDeviceSuggestionsUpdated(newSuggestions: List<SuggestedDeviceInfo>) {
        val stateChanged = synchronized(lock) {
          suggestions = newSuggestions
          updateSuggestedDeviceStateLocked()
        }
        if (stateChanged) {
          dispatchOnSuggestedDeviceUpdated()
        }
      }

      override fun onConnectionAttemptedForSuggestion(
        newSuggestedDeviceState: SuggestedDeviceState
      ) {
        synchronized(lock) {
          if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) {
            Log.w(TAG, "onConnectionAttemptedForSuggestion. Suggestion got changed.")
            return
          }
          if (
            suggestedDeviceState?.connectionState != STATE_DISCONNECTED &&
              suggestedDeviceState?.connectionState != STATE_CONNECTING_FAILED
          ) {
            return
          }
          suggestedDeviceState = suggestedDeviceState?.copy(connectionState = STATE_CONNECTING)
        }
        dispatchOnSuggestedDeviceUpdated()
      }

      override fun onConnectSuggestedDeviceFinished(
        newSuggestedDeviceState: SuggestedDeviceState,
        success: Boolean,
      ) {
        if (!isCurrentSuggestion(newSuggestedDeviceState.suggestedDeviceInfo)) {
          Log.w(TAG, "onConnectSuggestedDeviceFinished. Suggestion got changed.")
          return
        }
        synchronized(lock) {
          val connectionState = if (success) STATE_CONNECTED else STATE_CONNECTING_FAILED
          suggestedDeviceState = suggestedDeviceState?.copy(connectionState = connectionState)
        }
        dispatchOnSuggestedDeviceUpdated()
      }
    }

  fun addListener(listener: Listener) {
    val shouldRegisterCallback = synchronized(lock) {
      val wasSetEmpty = listeners.isEmpty()
      listeners.add(listener)
      wasSetEmpty
    }

    if (shouldRegisterCallback) {
      eagerlyUpdateState()
      localMediaManager.registerCallback(localMediaManagerDeviceCallback)
    }
  }

  fun removeListener(listener: Listener) {
    val shouldUnregisterCallback = synchronized(lock) {
      listeners.remove(listener)
      listeners.isEmpty()
    }

    if (shouldUnregisterCallback) {
      localMediaManager.unregisterCallback(localMediaManagerDeviceCallback)
    }
  }

  fun requestDeviceSuggestion() {
    localMediaManager.requestDeviceSuggestion()
  }

  fun getSuggestedDevice(): SuggestedDeviceState? {
    if (listeners.isEmpty()) {
      // If there were no callbacks set, recalculate the state before returning the result.
      eagerlyUpdateState()
    }
    return suggestedDeviceState
  }

  fun connectSuggestedDevice(
    suggestedDeviceState: SuggestedDeviceState,
    routingChangeInfo: RoutingChangeInfo,
  ) {
    if (!isCurrentSuggestion(suggestedDeviceState.suggestedDeviceInfo)) {
      Log.w(TAG, "Suggestion got changed, aborting connection.")
      return
    }
    localMediaManager.connectSuggestedDevice(suggestedDeviceState, routingChangeInfo)
  }

  private fun eagerlyUpdateState() {
    synchronized(lock) {
      mediaDevices = localMediaManager.mediaDevices
      suggestions = localMediaManager.suggestions
      updateSuggestedDeviceStateLocked()
    }
  }

  @GuardedBy("lock")
  private fun updateSuggestedDeviceStateLocked(): Boolean {
    var newSuggestedDeviceState: SuggestedDeviceState? = null
    val previousState = suggestedDeviceState
    val topSuggestion = suggestions.firstOrNull()
    if (topSuggestion != null) {
      val matchedDevice = getDeviceById(mediaDevices, topSuggestion.routeId)
      if (matchedDevice != null) {
        newSuggestedDeviceState = SuggestedDeviceState(topSuggestion, matchedDevice.state)
      }
      if (newSuggestedDeviceState == null) {
        if (previousState != null
          && (topSuggestion.routeId == previousState.suggestedDeviceInfo.routeId)) {
          return false
        }
        newSuggestedDeviceState = SuggestedDeviceState(topSuggestion)
      }
    }

    if (newSuggestedDeviceState != null && isSuggestedDeviceSelected(newSuggestedDeviceState)) {
      newSuggestedDeviceState = null
    }
    if (previousState != newSuggestedDeviceState) {
      synchronized(lock) { suggestedDeviceState = newSuggestedDeviceState }
      return true
    }
    return false
  }

  private fun isSuggestedDeviceSelected(newSuggestedDeviceState: SuggestedDeviceState): Boolean {
    synchronized(lock) {
      return mediaDevices.any { device ->
        device.isSelected() && device.getId() == newSuggestedDeviceState.suggestedDeviceInfo.routeId
      }
    }
  }

  private fun getDeviceById(mediaDevices: List<MediaDevice>, routeId: String): MediaDevice? =
    mediaDevices.find { it.id == routeId }

  private fun isCurrentSuggestion(suggestedDeviceInfo: SuggestedDeviceInfo) =
    synchronized(lock) {
      suggestedDeviceState?.suggestedDeviceInfo?.routeId == suggestedDeviceInfo.routeId
    }

  private fun dispatchOnSuggestedDeviceUpdated() {
    val state = synchronized(lock) { suggestedDeviceState }
    Log.i(TAG, "dispatchOnSuggestedDeviceUpdated(), state: $state")
    listeners.forEach { it.onSuggestedDeviceStateUpdated(state) }
  }

  interface Listener {
    fun onSuggestedDeviceStateUpdated(state: SuggestedDeviceState?)
  }
}
+7 −263

File changed.

Preview size limit exceeded, changes collapsed.

+2 −9
Original line number Original line Diff line number Diff line
@@ -643,7 +643,6 @@ public class LocalMediaManagerTest {


    @Test
    @Test
    public void connectSuggestedDevice_deviceIsDiscovered_immediatelyConnects() {
    public void connectSuggestedDevice_deviceIsDiscovered_immediatelyConnects() {
        when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1);


@@ -657,7 +656,6 @@ public class LocalMediaManagerTest {


    @Test
    @Test
    public void connectSuggestedDevice_deviceIsNotDiscovered_scanStarted() {
    public void connectSuggestedDevice_deviceIsNotDiscovered_scanStarted() {
        when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_2);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_2);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice1);


@@ -671,7 +669,6 @@ public class LocalMediaManagerTest {


    @Test
    @Test
    public void connectSuggestedDevice_deviceDiscoveredAfter_connects() {
    public void connectSuggestedDevice_deviceDiscoveredAfter_connects() {
        when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);


@@ -687,7 +684,6 @@ public class LocalMediaManagerTest {


    @Test
    @Test
    public void connectSuggestedDevice_handlerTimesOut_completesConnectionAttempt() {
    public void connectSuggestedDevice_handlerTimesOut_completesConnectionAttempt() {
        when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);


@@ -701,13 +697,11 @@ public class LocalMediaManagerTest {


        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();


        verify(mInfoMediaManager)
        verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, false);
                .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, false);
    }
    }


    @Test
    @Test
    public void connectSuggestedDevice_connectionSuccess_completesConnectionAttempt() {
    public void connectSuggestedDevice_connectionSuccess_completesConnectionAttempt() {
        when(mInfoMediaManager.getSuggestedDevice()).thenReturn(mSuggestedDeviceState);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        when(mSuggestedDeviceInfo.getRouteId()).thenReturn(TEST_DEVICE_ID_1);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);
        mLocalMediaManager.mMediaDevices.add(mInfoMediaDevice2);


@@ -721,8 +715,7 @@ public class LocalMediaManagerTest {


        mLocalMediaManager.dispatchSelectedDeviceStateChanged(mInfoMediaDevice1,
        mLocalMediaManager.dispatchSelectedDeviceStateChanged(mInfoMediaDevice1,
            LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
            LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
        verify(mInfoMediaManager)
        verify(mCallback).onConnectSuggestedDeviceFinished(mSuggestedDeviceState, true);
                .onConnectionAttemptCompletedForSuggestion(mSuggestedDeviceState, true);
    }
    }


    @Test
    @Test
Loading