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