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