1 /* 2 * Copyright 2019 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 /* 18 * Defines utility inteface that is used by state machine/service to either send vendor specific AT 19 * command or receive vendor specific response from the native stack. 20 */ 21 package com.android.bluetooth.hfpclient; 22 23 import static android.Manifest.permission.BLUETOOTH_CONNECT; 24 25 import android.bluetooth.BluetoothAssignedNumbers; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothHeadsetClient; 28 import android.content.Intent; 29 import android.util.Log; 30 31 import com.android.bluetooth.Utils; 32 33 import java.util.HashMap; 34 import java.util.Map; 35 import java.util.Objects; 36 37 class VendorCommandResponseProcessor { 38 39 private static final String TAG = "VendorCommandResponseProcessor"; 40 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 41 42 private final HeadsetClientService mService; 43 private final NativeInterface mNativeInterface; 44 45 // Keys are AT commands (without payload), and values are the company IDs. 46 private static final Map<String, Integer> SUPPORTED_VENDOR_AT_COMMANDS; 47 static { 48 SUPPORTED_VENDOR_AT_COMMANDS = new HashMap<>(); 49 SUPPORTED_VENDOR_AT_COMMANDS.put( 50 "+XAPL=", 51 BluetoothAssignedNumbers.APPLE); 52 SUPPORTED_VENDOR_AT_COMMANDS.put( 53 "+IPHONEACCEV=", 54 BluetoothAssignedNumbers.APPLE); 55 SUPPORTED_VENDOR_AT_COMMANDS.put( 56 "+APLSIRI?", 57 BluetoothAssignedNumbers.APPLE); 58 SUPPORTED_VENDOR_AT_COMMANDS.put( 59 "+APLEFM", 60 BluetoothAssignedNumbers.APPLE); 61 } 62 63 // Keys are AT events (without payload), and values are the company IDs. 64 private static final Map<String, Integer> SUPPORTED_VENDOR_EVENTS; 65 static { 66 SUPPORTED_VENDOR_EVENTS = new HashMap<>(); 67 SUPPORTED_VENDOR_EVENTS.put( 68 "+APLSIRI:", 69 BluetoothAssignedNumbers.APPLE); 70 SUPPORTED_VENDOR_EVENTS.put( 71 "+XAPL=", 72 BluetoothAssignedNumbers.APPLE); 73 SUPPORTED_VENDOR_EVENTS.put( 74 "+ANDROID:", 75 BluetoothAssignedNumbers.GOOGLE); 76 } 77 VendorCommandResponseProcessor(HeadsetClientService context, NativeInterface nativeInterface)78 VendorCommandResponseProcessor(HeadsetClientService context, NativeInterface nativeInterface) { 79 mService = context; 80 mNativeInterface = nativeInterface; 81 } 82 sendCommand(int vendorId, String atCommand, BluetoothDevice device)83 public boolean sendCommand(int vendorId, String atCommand, BluetoothDevice device) { 84 if (device == null) { 85 Log.w(TAG, "processVendorCommand device is null"); 86 return false; 87 } 88 89 // Do not support more than one command at one line. 90 // We simplify and say no ; allowed as well. 91 int indexOfSemicolon = atCommand.indexOf(';'); 92 if (indexOfSemicolon > 0) { 93 Log.e(TAG, "Do not support ; and more than one command:" 94 + atCommand); 95 return false; 96 } 97 98 // Get command word 99 int indexOfEqual = atCommand.indexOf('='); 100 int indexOfQuestionMark = atCommand.indexOf('?'); 101 String commandWord; 102 if (indexOfEqual > 0) { 103 commandWord = atCommand.substring(0, indexOfEqual + 1); 104 } else if (indexOfQuestionMark > 0) { 105 commandWord = atCommand.substring(0, indexOfQuestionMark + 1); 106 } else { 107 commandWord = atCommand; 108 } 109 110 // replace all white spaces 111 commandWord = commandWord.replaceAll("\\s+", ""); 112 113 if (!Objects.equals(SUPPORTED_VENDOR_AT_COMMANDS.get(commandWord), vendorId)) { 114 Log.e(TAG, "Invalid command " + atCommand + ", " + vendorId + ". Cand=" 115 + commandWord); 116 return false; 117 } 118 119 if (!mNativeInterface.sendATCmd(device, 120 HeadsetClientHalConstants 121 .HANDSFREECLIENT_AT_CMD_VENDOR_SPECIFIC_CMD, 122 0, 0, atCommand)) { 123 Log.e(TAG, "Failed to send vendor specific at command"); 124 return false; 125 } 126 logD("Send vendor command: " + atCommand); 127 return true; 128 } 129 getVendorIdFromAtCommand(String atString)130 private String getVendorIdFromAtCommand(String atString) { 131 // Get event code 132 int indexOfEqual = atString.indexOf('='); 133 int indexOfColon = atString.indexOf(':'); 134 String eventCode; 135 if (indexOfEqual > 0) { 136 eventCode = atString.substring(0, indexOfEqual + 1); 137 } else if (indexOfColon > 0) { 138 eventCode = atString.substring(0, indexOfColon + 1); 139 } else { 140 eventCode = atString; 141 } 142 143 // replace all white spaces 144 eventCode = eventCode.replaceAll("\\s+", ""); 145 146 return eventCode; 147 } 148 isAndroidAtCommand(String atString)149 public boolean isAndroidAtCommand(String atString) { 150 String eventCode = getVendorIdFromAtCommand(atString); 151 Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); 152 if (vendorId == null) { 153 return false; 154 } 155 return vendorId == BluetoothAssignedNumbers.GOOGLE; 156 } 157 processEvent(String atString, BluetoothDevice device)158 public boolean processEvent(String atString, BluetoothDevice device) { 159 if (device == null) { 160 Log.w(TAG, "processVendorEvent device is null"); 161 return false; 162 } 163 164 String eventCode = getVendorIdFromAtCommand(atString); 165 Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode); 166 if (vendorId == null) { 167 Log.e(TAG, "Invalid response: " + atString + ". " + eventCode); 168 return false; 169 } else { 170 broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device); 171 logD("process vendor event " + vendorId + ", " + eventCode + ", " 172 + atString + " for device" + device); 173 } 174 return true; 175 } 176 broadcastVendorSpecificEventIntent(int vendorId, String vendorEventCode, String vendorResponse, BluetoothDevice device)177 private void broadcastVendorSpecificEventIntent(int vendorId, String vendorEventCode, 178 String vendorResponse, BluetoothDevice device) { 179 logD("broadcastVendorSpecificEventIntent(" + vendorResponse + ")"); 180 Intent intent = new Intent(BluetoothHeadsetClient 181 .ACTION_VENDOR_SPECIFIC_HEADSETCLIENT_EVENT); 182 intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_ID, vendorId); 183 intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_CODE, vendorEventCode); 184 intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_FULL_ARGS, vendorResponse); 185 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 186 Utils.sendBroadcast(mService, intent, BLUETOOTH_CONNECT, 187 Utils.getTempAllowlistBroadcastOptions()); 188 } 189 logD(String msg)190 private void logD(String msg) { 191 if (DBG) { 192 Log.d(TAG, msg); 193 } 194 } 195 } 196