/* * Copyright 2018 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.bluetooth.audio_util; import android.content.Context; import android.content.pm.ResolveInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import com.android.bluetooth.Utils; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class provides a way to connect to multiple browsable players at a time. * It will attempt to simultaneously connect to a list of services that support * the MediaBrowserService. After a timeout, the list of connected players will * be returned via callback. * * The main use of this class is to check whether a player can be browsed despite * using the MediaBrowserService. This way we do not have to do the same checks * when constructing BrowsedPlayerWrappers by hand. */ public class BrowsablePlayerConnector { private static final String TAG = "AvrcpBrowsablePlayerConnector"; private static final boolean DEBUG = true; private static final long CONNECT_TIMEOUT_MS = 10000; // Time in ms to wait for a connection private static final int MSG_GET_FOLDER_ITEMS_CB = 0; private static final int MSG_CONNECT_CB = 1; private static final int MSG_TIMEOUT = 2; private static BrowsablePlayerConnector sInjectConnector; private Handler mHandler; private Context mContext; private PlayerListCallback mCallback; private List mResults = new ArrayList(); private Set mPendingPlayers = new HashSet(); interface PlayerListCallback { void run(List result); } private static void setInstanceForTesting(BrowsablePlayerConnector connector) { Utils.enforceInstrumentationTestMode(); sInjectConnector = connector; } static BrowsablePlayerConnector connectToPlayers( Context context, Looper looper, List players, PlayerListCallback cb) { if (sInjectConnector != null) { return sInjectConnector; } if (cb == null) { Log.wtf(TAG, "Null callback passed"); return null; } BrowsablePlayerConnector newWrapper = new BrowsablePlayerConnector(context, looper, cb); // Try to start connecting all the browsed player wrappers for (ResolveInfo info : players) { BrowsedPlayerWrapper player = BrowsedPlayerWrapper.wrap( context, looper, info.serviceInfo.packageName, info.serviceInfo.name); newWrapper.mPendingPlayers.add(player); player.connect((int status, BrowsedPlayerWrapper wrapper) -> { // Use the handler to avoid concurrency issues if (DEBUG) { Log.d(TAG, "Browse player callback called: package=" + info.serviceInfo.packageName + " : status=" + status); } Message msg = newWrapper.mHandler.obtainMessage(MSG_CONNECT_CB); msg.arg1 = status; msg.obj = wrapper; newWrapper.mHandler.sendMessage(msg); }); } Message msg = newWrapper.mHandler.obtainMessage(MSG_TIMEOUT); newWrapper.mHandler.sendMessageDelayed(msg, CONNECT_TIMEOUT_MS); return newWrapper; } private BrowsablePlayerConnector(Context context, Looper looper, PlayerListCallback cb) { mContext = context; mCallback = cb; mHandler = new Handler(looper) { public void handleMessage(Message msg) { if (DEBUG) Log.d(TAG, "Received a message: msg.what=" + msg.what); switch(msg.what) { case MSG_GET_FOLDER_ITEMS_CB: { BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; // If we failed to remove the wrapper from the pending set, that // means a timeout occurred and the callback was triggered afterwards if (!mPendingPlayers.remove(wrapper)) { return; } Log.i(TAG, "Successfully added package to results: " + wrapper.getPackageName()); mResults.add(wrapper); } break; case MSG_CONNECT_CB: { BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; if (msg.arg1 != BrowsedPlayerWrapper.STATUS_SUCCESS) { Log.i(TAG, wrapper.getPackageName() + " is not browsable"); mPendingPlayers.remove(wrapper); return; } // Check to see if the root folder has any items if (DEBUG) { Log.i(TAG, "Checking root contents for " + wrapper.getPackageName()); } wrapper.getFolderItems(wrapper.getRootId(), (int status, String mediaId, List results) -> { if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { mPendingPlayers.remove(wrapper); return; } if (results.size() == 0) { mPendingPlayers.remove(wrapper); return; } // Send the response as a message so that it is properly // synchronized Message success = mHandler.obtainMessage(MSG_GET_FOLDER_ITEMS_CB); success.obj = wrapper; mHandler.sendMessage(success); }); } break; case MSG_TIMEOUT: { Log.v(TAG, "Timed out waiting for players"); removePendingPlayers(); } break; } if (mPendingPlayers.size() == 0) { Log.i(TAG, "Successfully connected to " + mResults.size() + " browsable players."); removeMessages(MSG_TIMEOUT); mCallback.run(mResults); } } }; } private void removePendingPlayers() { for (BrowsedPlayerWrapper wrapper : mPendingPlayers) { if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName()); wrapper.disconnect(); } mPendingPlayers.clear(); } void cleanup() { if (mPendingPlayers.size() != 0) { Log.i(TAG, "Bluetooth turn off with " + mPendingPlayers.size() + " pending player(s)"); mHandler.removeMessages(MSG_TIMEOUT); removePendingPlayers(); mHandler = null; } } }