• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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