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