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