• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.bluetooth.mcp;
19 
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.IBluetoothMcpServiceManager;
23 import android.content.AttributionSource;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.sysprop.BluetoothProperties;
27 import android.util.Log;
28 
29 import com.android.bluetooth.Utils;
30 import com.android.bluetooth.btservice.ProfileService;
31 import com.android.bluetooth.le_audio.LeAudioService;
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.util.HashMap;
36 import java.util.Map;
37 
38 /**
39  * Provides Media Control Profile, as a service in the Bluetooth application.
40  * @hide
41  */
42 public class McpService extends ProfileService {
43     private static final boolean DBG = true;
44     private static final boolean VDBG = false;
45     private static final String TAG = "BluetoothMcpService";
46 
47     private static McpService sMcpService;
48     private static MediaControlProfile sGmcsForTesting;
49 
50     private Object mLock = new Object();
51     @GuardedBy("mLock")
52     private MediaControlProfile mGmcs;
53     private Map<BluetoothDevice, Integer> mDeviceAuthorizations = new HashMap<>();
54     private Handler mHandler = new Handler(Looper.getMainLooper());
55 
isEnabled()56     public static boolean isEnabled() {
57         return BluetoothProperties.isProfileMcpServerEnabled().orElse(false);
58     }
59 
setMcpService(McpService instance)60     private static synchronized void setMcpService(McpService instance) {
61         if (VDBG) {
62             Log.d(TAG, "setMcpService(): set to: " + instance);
63         }
64         sMcpService = instance;
65     }
66 
getMcpService()67     public static synchronized McpService getMcpService() {
68         if (sMcpService == null) {
69             Log.w(TAG, "getMcpService(): service is NULL");
70             return null;
71         }
72 
73         if (!sMcpService.isAvailable()) {
74             Log.w(TAG, "getMcpService(): service is not available");
75             return null;
76         }
77         return sMcpService;
78     }
79 
80     @VisibleForTesting
getMediaControlProfile()81     public static MediaControlProfile getMediaControlProfile() {
82         return sGmcsForTesting;
83     }
84 
85     @VisibleForTesting
setMediaControlProfileForTesting(MediaControlProfile mediaControlProfile)86     public static void setMediaControlProfileForTesting(MediaControlProfile mediaControlProfile) {
87         sGmcsForTesting = mediaControlProfile;
88     }
89 
90     @Override
initBinder()91     protected IProfileServiceBinder initBinder() {
92         return new BluetoothMcpServiceBinder(this);
93     }
94 
95     @Override
create()96     protected void create() {
97         if (DBG) {
98             Log.d(TAG, "create()");
99         }
100     }
101 
102     @Override
start()103     protected boolean start() {
104         if (DBG) {
105             Log.d(TAG, "start()");
106         }
107 
108         if (sMcpService != null) {
109             throw new IllegalStateException("start() called twice");
110         }
111 
112         // Mark service as started
113         setMcpService(this);
114 
115         synchronized (mLock) {
116             if (getGmcsLocked() == null) {
117                 // Initialize the Media Control Service Server
118                 mGmcs = new MediaControlProfile(this);
119                 // Requires this service to be already started thus we have to make it an async call
120                 mHandler.post(() -> {
121                     synchronized (mLock) {
122                         if (mGmcs != null) {
123                             mGmcs.init();
124                         }
125                     }
126                 });
127             }
128         }
129 
130         return true;
131     }
132 
133     @Override
stop()134     protected boolean stop() {
135         if (DBG) {
136             Log.d(TAG, "stop()");
137         }
138 
139         if (sMcpService == null) {
140             Log.w(TAG, "stop() called before start()");
141             return true;
142         }
143 
144         synchronized (mLock) {
145             // A runnable for calling mGmcs.init() could be pending on mHandler
146             mHandler.removeCallbacksAndMessages(null);
147             if (mGmcs != null) {
148                 mGmcs.cleanup();
149                 mGmcs = null;
150             }
151             if (sGmcsForTesting != null) {
152                 sGmcsForTesting.cleanup();
153                 sGmcsForTesting = null;
154             }
155         }
156 
157         // Mark service as stopped
158         setMcpService(null);
159         return true;
160     }
161 
162     @Override
cleanup()163     protected void cleanup() {
164         if (DBG) {
165             Log.d(TAG, "cleanup()");
166         }
167     }
168 
169     @Override
dump(StringBuilder sb)170     public void dump(StringBuilder sb) {
171         super.dump(sb);
172         synchronized (mLock) {
173             MediaControlProfile gmcs = getGmcsLocked();
174             if (gmcs != null) {
175                 gmcs.dump(sb);
176             }
177         }
178     }
179 
onDeviceUnauthorized(BluetoothDevice device)180     public void onDeviceUnauthorized(BluetoothDevice device) {
181         if (Utils.isPtsTestMode()) {
182             Log.d(TAG, "PTS test: setDeviceAuthorized");
183             setDeviceAuthorized(device, true);
184             return;
185         }
186         Log.w(TAG, "onDeviceUnauthorized - authorization notification not implemented yet ");
187         setDeviceAuthorized(device, false);
188     }
189 
setDeviceAuthorized(BluetoothDevice device, boolean isAuthorized)190     public void setDeviceAuthorized(BluetoothDevice device, boolean isAuthorized) {
191         Log.i(TAG, "setDeviceAuthorized(): device: " + device + ", isAuthorized: " + isAuthorized);
192         int authorization = isAuthorized ? BluetoothDevice.ACCESS_ALLOWED
193                 : BluetoothDevice.ACCESS_REJECTED;
194         mDeviceAuthorizations.put(device, authorization);
195 
196         synchronized (mLock) {
197             MediaControlProfile gmcs = getGmcsLocked();
198             if (gmcs != null) {
199                 gmcs.onDeviceAuthorizationSet(device);
200             }
201         }
202     }
203 
getDeviceAuthorization(BluetoothDevice device)204     public int getDeviceAuthorization(BluetoothDevice device) {
205         /* Media control is allowed for
206          * 1. in PTS mode
207          * 2. authorized devices
208          * 3. Any LeAudio devices which are allowed to connect
209          */
210         int authorization = mDeviceAuthorizations.getOrDefault(device, Utils.isPtsTestMode()
211                 ? BluetoothDevice.ACCESS_ALLOWED : BluetoothDevice.ACCESS_UNKNOWN);
212         if (authorization != BluetoothDevice.ACCESS_UNKNOWN) {
213             return authorization;
214         }
215 
216         LeAudioService leAudioService = LeAudioService.getLeAudioService();
217         if (leAudioService == null) {
218             Log.e(TAG, "MCS access not permited. LeAudioService not available");
219             return BluetoothDevice.ACCESS_UNKNOWN;
220         }
221 
222         if (leAudioService.getConnectionPolicy(device)
223                 > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
224             if (DBG) {
225                 Log.d(TAG, "MCS authorization allowed based on supported LeAudio service");
226             }
227             setDeviceAuthorized(device, true);
228             return BluetoothDevice.ACCESS_ALLOWED;
229         }
230 
231         Log.e(TAG, "MCS access not permited");
232         return BluetoothDevice.ACCESS_UNKNOWN;
233     }
234 
235     @GuardedBy("mLock")
getGmcsLocked()236     private MediaControlProfile getGmcsLocked() {
237         if (sGmcsForTesting != null) {
238             return sGmcsForTesting;
239         } else {
240             return mGmcs;
241         }
242     }
243 
244     /**
245      * Binder object: must be a static class or memory leak may occur
246      */
247     static class BluetoothMcpServiceBinder
248             extends IBluetoothMcpServiceManager.Stub implements IProfileServiceBinder {
249         private McpService mService;
250 
BluetoothMcpServiceBinder(McpService svc)251         BluetoothMcpServiceBinder(McpService svc) {
252             mService = svc;
253         }
254 
getService(AttributionSource source)255         private McpService getService(AttributionSource source) {
256             if (mService != null && mService.isAvailable()) {
257                 return mService;
258             }
259             Log.e(TAG, "getService() - Service requested, but not available!");
260             return null;
261         }
262 
263         @Override
setDeviceAuthorized(BluetoothDevice device, boolean isAuthorized, AttributionSource source)264         public void setDeviceAuthorized(BluetoothDevice device, boolean isAuthorized,
265                 AttributionSource source) {
266             McpService service = getService(source);
267             if (service == null) {
268                 return;
269             }
270             Utils.enforceBluetoothPrivilegedPermission(service);
271             service.setDeviceAuthorized(device, isAuthorized);
272         }
273 
274         @Override
cleanup()275         public void cleanup() {
276             if (mService != null) {
277                 mService.cleanup();
278             }
279             mService = null;
280         }
281     }
282 }
283