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 PlayerListCallback mCallback; 56 57 private List<BrowsedPlayerWrapper> mResults = new ArrayList<BrowsedPlayerWrapper>(); 58 private Set<BrowsedPlayerWrapper> mPendingPlayers = new HashSet<BrowsedPlayerWrapper>(); 59 60 interface PlayerListCallback { run(List<BrowsedPlayerWrapper> result)61 void run(List<BrowsedPlayerWrapper> result); 62 } 63 64 /** 65 * @hide 66 */ 67 @VisibleForTesting setInstanceForTesting(BrowsablePlayerConnector connector)68 static void setInstanceForTesting(BrowsablePlayerConnector connector) { 69 Utils.enforceInstrumentationTestMode(); 70 sInjectConnector = connector; 71 } 72 connectToPlayers( Context context, Looper looper, List<ResolveInfo> players, PlayerListCallback cb)73 static BrowsablePlayerConnector connectToPlayers( 74 Context context, 75 Looper looper, 76 List<ResolveInfo> players, 77 PlayerListCallback cb) { 78 if (sInjectConnector != null) { 79 return sInjectConnector; 80 } 81 if (cb == null) { 82 Log.wtf(TAG, "Null callback passed"); 83 return null; 84 } 85 86 BrowsablePlayerConnector newWrapper = new BrowsablePlayerConnector(looper, cb); 87 88 // Try to start connecting all the browsed player wrappers 89 for (ResolveInfo info : players) { 90 BrowsedPlayerWrapper player = BrowsedPlayerWrapper.wrap( 91 context, 92 looper, 93 info.serviceInfo.packageName, 94 info.serviceInfo.name); 95 newWrapper.mPendingPlayers.add(player); 96 player.connect((int status, BrowsedPlayerWrapper wrapper) -> { 97 // Use the handler to avoid concurrency issues 98 if (DEBUG) { 99 Log.d(TAG, "Browse player callback called: package=" 100 + info.serviceInfo.packageName 101 + " : status=" + status); 102 } 103 Message msg = newWrapper.mHandler.obtainMessage(MSG_CONNECT_CB); 104 msg.arg1 = status; 105 msg.obj = wrapper; 106 newWrapper.mHandler.sendMessage(msg); 107 }); 108 } 109 110 Message msg = newWrapper.mHandler.obtainMessage(MSG_TIMEOUT); 111 newWrapper.mHandler.sendMessageDelayed(msg, CONNECT_TIMEOUT_MS); 112 return newWrapper; 113 } 114 BrowsablePlayerConnector(Looper looper, PlayerListCallback cb)115 private BrowsablePlayerConnector(Looper looper, PlayerListCallback cb) { 116 mCallback = cb; 117 mHandler = new Handler(looper) { 118 public void handleMessage(Message msg) { 119 if (DEBUG) Log.d(TAG, "Received a message: msg.what=" + msg.what); 120 switch(msg.what) { 121 case MSG_GET_FOLDER_ITEMS_CB: { 122 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 123 // If we failed to remove the wrapper from the pending set, that 124 // means a timeout occurred and the callback was triggered afterwards 125 if (!mPendingPlayers.remove(wrapper)) { 126 return; 127 } 128 129 Log.i(TAG, "Successfully added package to results: " 130 + wrapper.getPackageName()); 131 mResults.add(wrapper); 132 } break; 133 134 case MSG_CONNECT_CB: { 135 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 136 137 if (msg.arg1 != BrowsedPlayerWrapper.STATUS_SUCCESS) { 138 Log.i(TAG, wrapper.getPackageName() + " is not browsable"); 139 mPendingPlayers.remove(wrapper); 140 return; 141 } 142 143 // Check to see if the root folder has any items 144 if (DEBUG) { 145 Log.i(TAG, "Checking root contents for " + wrapper.getPackageName()); 146 } 147 wrapper.getFolderItems(wrapper.getRootId(), 148 (int status, String mediaId, List<ListItem> results) -> { 149 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 150 mPendingPlayers.remove(wrapper); 151 return; 152 } 153 154 if (results.size() == 0) { 155 mPendingPlayers.remove(wrapper); 156 return; 157 } 158 159 // Send the response as a message so that it is properly 160 // synchronized 161 Message success = 162 mHandler.obtainMessage(MSG_GET_FOLDER_ITEMS_CB); 163 success.obj = wrapper; 164 mHandler.sendMessage(success); 165 }); 166 } break; 167 168 case MSG_TIMEOUT: { 169 Log.v(TAG, "Timed out waiting for players"); 170 removePendingPlayers(); 171 } break; 172 } 173 174 if (mPendingPlayers.size() == 0) { 175 Log.i(TAG, "Successfully connected to " 176 + mResults.size() + " browsable players."); 177 removeMessages(MSG_TIMEOUT); 178 mCallback.run(mResults); 179 } 180 } 181 }; 182 } 183 removePendingPlayers()184 private void removePendingPlayers() { 185 for (BrowsedPlayerWrapper wrapper : mPendingPlayers) { 186 if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName()); 187 wrapper.disconnect(); 188 } 189 mPendingPlayers.clear(); 190 } 191 cleanup()192 void cleanup() { 193 if (mPendingPlayers.size() != 0) { 194 Log.i(TAG, "Bluetooth turn off with " + mPendingPlayers.size() + " pending player(s)"); 195 mHandler.removeMessages(MSG_TIMEOUT); 196 removePendingPlayers(); 197 mHandler = null; 198 } 199 } 200 } 201