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 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