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.avrcp; 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 java.util.ArrayList; 27 import java.util.HashSet; 28 import java.util.List; 29 import java.util.Set; 30 31 /** 32 * This class provides a way to connect to multiple browsable players at a time. 33 * It will attempt to simultaneously connect to a list of services that support 34 * the MediaBrowserService. After a timeout, the list of connected players will 35 * be returned via callback. 36 * 37 * The main use of this class is to check whether a player can be browsed despite 38 * using the MediaBrowserService. This way we do not have to do the same checks 39 * when constructing BrowsedPlayerWrappers by hand. 40 */ 41 public class BrowsablePlayerConnector { 42 private static final String TAG = "AvrcpBrowsablePlayerConnector"; 43 private static final boolean DEBUG = true; 44 private static final long CONNECT_TIMEOUT_MS = 10000; // Time in ms to wait for a connection 45 46 private static final int MSG_GET_FOLDER_ITEMS_CB = 0; 47 private static final int MSG_CONNECT_CB = 1; 48 private static final int MSG_TIMEOUT = 2; 49 50 private Handler mHandler; 51 private Context mContext; 52 private PlayerListCallback mCallback; 53 54 private List<BrowsedPlayerWrapper> mResults = new ArrayList<BrowsedPlayerWrapper>(); 55 private Set<BrowsedPlayerWrapper> mPendingPlayers = new HashSet<BrowsedPlayerWrapper>(); 56 57 interface PlayerListCallback { run(List<BrowsedPlayerWrapper> result)58 void run(List<BrowsedPlayerWrapper> result); 59 } 60 connectToPlayers( Context context, Looper looper, List<ResolveInfo> players, PlayerListCallback cb)61 static BrowsablePlayerConnector connectToPlayers( 62 Context context, 63 Looper looper, 64 List<ResolveInfo> players, 65 PlayerListCallback cb) { 66 if (cb == null) { 67 Log.wtfStack(TAG, "Null callback passed"); 68 return null; 69 } 70 71 BrowsablePlayerConnector newWrapper = new BrowsablePlayerConnector(context, looper, cb); 72 73 // Try to start connecting all the browsed player wrappers 74 for (ResolveInfo info : players) { 75 BrowsedPlayerWrapper player = BrowsedPlayerWrapper.wrap( 76 context, 77 info.serviceInfo.packageName, 78 info.serviceInfo.name); 79 newWrapper.mPendingPlayers.add(player); 80 player.connect((int status, BrowsedPlayerWrapper wrapper) -> { 81 // Use the handler to avoid concurrency issues 82 if (DEBUG) { 83 Log.d(TAG, "Browse player callback called: package=" 84 + info.serviceInfo.packageName 85 + " : status=" + status); 86 } 87 Message msg = newWrapper.mHandler.obtainMessage(MSG_CONNECT_CB); 88 msg.arg1 = status; 89 msg.obj = wrapper; 90 newWrapper.mHandler.sendMessage(msg); 91 }); 92 } 93 94 Message msg = newWrapper.mHandler.obtainMessage(MSG_TIMEOUT); 95 newWrapper.mHandler.sendMessageDelayed(msg, CONNECT_TIMEOUT_MS); 96 return newWrapper; 97 } 98 BrowsablePlayerConnector(Context context, Looper looper, PlayerListCallback cb)99 private BrowsablePlayerConnector(Context context, Looper looper, PlayerListCallback cb) { 100 mContext = context; 101 mCallback = cb; 102 mHandler = new Handler(looper) { 103 public void handleMessage(Message msg) { 104 if (DEBUG) Log.d(TAG, "Received a message: msg.what=" + msg.what); 105 switch(msg.what) { 106 case MSG_GET_FOLDER_ITEMS_CB: { 107 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 108 // If we failed to remove the wrapper from the pending set, that 109 // means a timeout occurred and the callback was triggered afterwards 110 if (!mPendingPlayers.remove(wrapper)) { 111 return; 112 } 113 114 Log.i(TAG, "Successfully added package to results: " 115 + wrapper.getPackageName()); 116 mResults.add(wrapper); 117 } break; 118 119 case MSG_CONNECT_CB: { 120 BrowsedPlayerWrapper wrapper = (BrowsedPlayerWrapper) msg.obj; 121 122 if (msg.arg1 != BrowsedPlayerWrapper.STATUS_SUCCESS) { 123 Log.i(TAG, wrapper.getPackageName() + " is not browsable"); 124 mPendingPlayers.remove(wrapper); 125 return; 126 } 127 128 // Check to see if the root folder has any items 129 if (DEBUG) { 130 Log.i(TAG, "Checking root contents for " + wrapper.getPackageName()); 131 } 132 wrapper.getFolderItems(wrapper.getRootId(), 133 (int status, String mediaId, List<ListItem> results) -> { 134 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 135 mPendingPlayers.remove(wrapper); 136 return; 137 } 138 139 if (results.size() == 0) { 140 mPendingPlayers.remove(wrapper); 141 return; 142 } 143 144 // Send the response as a message so that it is properly 145 // synchronized 146 Message success = 147 mHandler.obtainMessage(MSG_GET_FOLDER_ITEMS_CB); 148 success.obj = wrapper; 149 mHandler.sendMessage(success); 150 }); 151 } break; 152 153 case MSG_TIMEOUT: { 154 Log.v(TAG, "Timed out waiting for players"); 155 removePendingPlayers(); 156 } break; 157 } 158 159 if (mPendingPlayers.size() == 0) { 160 Log.i(TAG, "Successfully connected to " 161 + mResults.size() + " browsable players."); 162 removeMessages(MSG_TIMEOUT); 163 mCallback.run(mResults); 164 } 165 } 166 }; 167 } 168 removePendingPlayers()169 private void removePendingPlayers() { 170 for (BrowsedPlayerWrapper wrapper : mPendingPlayers) { 171 if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName()); 172 wrapper.disconnect(); 173 } 174 mPendingPlayers.clear(); 175 } 176 cleanup()177 void cleanup() { 178 if (mPendingPlayers.size() != 0) { 179 Log.i(TAG, "Bluetooth turn off with " + mPendingPlayers.size() + " pending player(s)"); 180 mHandler.removeMessages(MSG_TIMEOUT); 181 removePendingPlayers(); 182 mHandler = null; 183 } 184 } 185 } 186