blob: 08f6230fd7d00a6333765a90662f14b21b43611e [file] [log] [blame]
/*
* 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<BrowsedPlayerWrapper> mResults = new ArrayList<BrowsedPlayerWrapper>();
private Set<BrowsedPlayerWrapper> mPendingPlayers = new HashSet<BrowsedPlayerWrapper>();
interface PlayerListCallback {
void run(List<BrowsedPlayerWrapper> result);
}
private static void setInstanceForTesting(BrowsablePlayerConnector connector) {
Utils.enforceInstrumentationTestMode();
sInjectConnector = connector;
}
static BrowsablePlayerConnector connectToPlayers(
Context context,
Looper looper,
List<ResolveInfo> 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<ListItem> 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;
}
}
}