1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.audio_util; 18 19 import android.content.Context; 20 import android.content.pm.ResolveInfo; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.util.Log; 25 26 import com.android.bluetooth.Utils; 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.util.ArrayList; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Set; 33 34 /** 35 * This class provides a way to connect to multiple browsable players at a time. 36 * It will attempt to simultaneously connect to a list of services that support 37 * the MediaBrowserService. After a timeout, the list of connected players will 38 * be returned via callback. 39 * 40 * The main use of this class is to check whether a player can be browsed despite 41 * using the MediaBrowserService. This way we do not have to do the same checks 42 * when constructing BrowsedPlayerWrappers by hand. 43 */ 44 public class BrowsablePlayerConnector { 45 private static final String TAG = "AvrcpBrowsablePlayerConnector"; 46 private static final boolean DEBUG = true; 47 private static final long CONNECT_TIMEOUT_MS = 10000; // Time in ms to wait for a connection 48 49 private static final int MSG_GET_FOLDER_ITEMS_CB = 0; 50 private static final int MSG_CONNECT_CB = 1; 51 private static final int MSG_TIMEOUT = 2; 52 53 private static BrowsablePlayerConnector sInjectConnector; 54 private Handler mHandler; 55 private Context mContext; 56 private PlayerListCallback mCallback; 57 58 private List<BrowsedPlayerWrapper> mResults = new ArrayList<BrowsedPlayerWrapper>(); 59 private Set<BrowsedPlayerWrapper> mPendingPlayers = new HashSet<BrowsedPlayerWrapper>(); 60 61 interface PlayerListCallback { run(List<BrowsedPlayerWrapper> result)62 void run(List<BrowsedPlayerWrapper> result); 63 } 64 setInstanceForTesting(BrowsablePlayerConnector connector)65 private static void setInstanceForTesting(BrowsablePlayerConnector connector) { 66 Utils.enforceInstrumentationTestMode(); 67 sInjectConnector = connector; 68 } 69 connectToPlayers( Context context, Looper looper, List<ResolveInfo> players, PlayerListCallback cb)70 static BrowsablePlayerConnector connectToPlayers( 71 Context context, 72 Looper looper, 73 List<ResolveInfo> players, 74 PlayerListCallback cb) { 75 if (sInjectConnector != null) { 76 return sInjectConnector; 77 } 78 if (cb == null) { 79 Log.wtf(TAG, "Null callback passed"); 80 return null; 81 } 82 83 BrowsablePlayerConnector newWrapper = new BrowsablePlayerConnector(context, looper, cb); 84 85 // Try to start connecting all the browsed player wrappers 86 for (ResolveInfo info : players) { 87 BrowsedPlayerWrapper player = BrowsedPlayerWrapper.wrap( 88 context, 89 looper, 90 info.serviceInfo.packageName, 91 info.serviceInfo.name); 92 newWrapper.mPendingPlayers.add(player); 93 player.connect((int status, BrowsedPlayerWrapper wrapper) -> { 94 // Use the handler to avoid concurrency issues 95 if (DEBUG) { 96 Log.d(TAG, "Browse player callback called: package=" 97 + info.serviceInfo.packageName 98 + " : status=" + status); 99 } 100 Message msg = newWrapper.mHandler.obtainMessage(MSG_CONNECT_CB); 101 msg.arg1 = status; 102 msg.obj = wrapper; 103 newWrapper.mHandler.sendMessage(msg); 104 }); 105 } 106 107 Message msg = newWrapper.mHandler.obtainMessage(MSG_TIMEOUT); 108 newWrapper.mHandler.sendMessageDelayed(msg, CONNECT_TIMEOUT_MS); 109 return newWrapper; 110 } 111 BrowsablePlayerConnector(Context context, Looper looper, PlayerListCallback cb)112 private BrowsablePlayerConnector(Context context, Looper looper, PlayerListCallback cb) { 113 mContext = context; 114 mCallback = cb; 115 mHandler = new Handler(looper) { 116 public void handleMessage(Message msg) { 117 if (DEBUG) Log.d(TAG, "Received a message: msg.what=" + msg.what); 118 switch(msg.what) { 119 case MSG_GET_FOLDER_ITEMS_CB: { 120 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 121 // If we failed to remove the wrapper from the pending set, that 122 // means a timeout occurred and the callback was triggered afterwards 123 if (!mPendingPlayers.remove(wrapper)) { 124 return; 125 } 126 127 Log.i(TAG, "Successfully added package to results: " 128 + wrapper.getPackageName()); 129 mResults.add(wrapper); 130 } break; 131 132 case MSG_CONNECT_CB: { 133 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 134 135 if (msg.arg1 != BrowsedPlayerWrapper.STATUS_SUCCESS) { 136 Log.i(TAG, wrapper.getPackageName() + " is not browsable"); 137 mPendingPlayers.remove(wrapper); 138 return; 139 } 140 141 // Check to see if the root folder has any items 142 if (DEBUG) { 143 Log.i(TAG, "Checking root contents for " + wrapper.getPackageName()); 144 } 145 wrapper.getFolderItems(wrapper.getRootId(), 146 (int status, String mediaId, List<ListItem> results) -> { 147 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 148 mPendingPlayers.remove(wrapper); 149 return; 150 } 151 152 if (results.size() == 0) { 153 mPendingPlayers.remove(wrapper); 154 return; 155 } 156 157 // Send the response as a message so that it is properly 158 // synchronized 159 Message success = 160 mHandler.obtainMessage(MSG_GET_FOLDER_ITEMS_CB); 161 success.obj = wrapper; 162 mHandler.sendMessage(success); 163 }); 164 } break; 165 166 case MSG_TIMEOUT: { 167 Log.v(TAG, "Timed out waiting for players"); 168 removePendingPlayers(); 169 } break; 170 } 171 172 if (mPendingPlayers.size() == 0) { 173 Log.i(TAG, "Successfully connected to " 174 + mResults.size() + " browsable players."); 175 removeMessages(MSG_TIMEOUT); 176 mCallback.run(mResults); 177 } 178 } 179 }; 180 } 181 removePendingPlayers()182 private void removePendingPlayers() { 183 for (BrowsedPlayerWrapper wrapper : mPendingPlayers) { 184 if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName()); 185 wrapper.disconnect(); 186 } 187 mPendingPlayers.clear(); 188 } 189 cleanup()190 void cleanup() { 191 if (mPendingPlayers.size() != 0) { 192 Log.i(TAG, "Bluetooth turn off with " + mPendingPlayers.size() + " pending player(s)"); 193 mHandler.removeMessages(MSG_TIMEOUT); 194 removePendingPlayers(); 195 mHandler = null; 196 } 197 } 198 } 199