1/*
2 * Copyright (C) 2007 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
17package android.bluetooth;
18
19import java.util.*;
20
21/**
22 * An AT (Hayes command) Parser based on (a subset of) the ITU-T V.250 standard.
23 * <p>
24 *
25 * Conformant with the subset of V.250 required for implementation of the
26 * Bluetooth Headset and Handsfree Profiles, as per Bluetooth SIP
27 * specifications. Also implements some V.250 features not required by
28 * Bluetooth - such as chained commands.<p>
29 *
30 * Command handlers are registered with an AtParser object. These handlers are
31 * invoked when command lines are processed by AtParser's process() method.<p>
32 *
33 * The AtParser object accepts a new command line to parse via its process()
34 * method. It breaks each command line into one or more commands. Each command
35 * is parsed for name, type, and (optional) arguments, and an appropriate
36 * external handler method is called through the AtCommandHandler interface.
37 *
38 * The command types are<ul>
39 * <li>Basic Command. For example "ATDT1234567890". Basic command names are a
40 * single character (e.g. "D"), and everything following this character is
41 * passed to the handler as a string argument (e.g. "T1234567890").
42 * <li>Action Command. For example "AT+CIMI". The command name is "CIMI", and
43 * there are no arguments for action commands.
44 * <li>Read Command. For example "AT+VGM?". The command name is "VGM", and there
45 * are no arguments for get commands.
46 * <li>Set Command. For example "AT+VGM=14". The command name is "VGM", and
47 * there is a single integer argument in this case. In the general case then
48 * can be zero or more arguments (comma delimited) each of integer or string
49 * form.
50 * <li>Test Command. For example "AT+VGM=?. No arguments.
51 * </ul>
52 *
53 * In V.250 the last four command types are known as Extended Commands, and
54 * they are used heavily in Bluetooth.<p>
55 *
56 * Basic commands cannot be chained in this implementation. For Bluetooth
57 * headset/handsfree use this is acceptable, because they only use the basic
58 * commands ATA and ATD, which are not allowed to be chained. For general V.250
59 * use we would need to improve this class to allow Basic command chaining -
60 * however it's tricky to get right because there is no delimiter for Basic
61 * command chaining.<p>
62 *
63 * Extended commands can be chained. For example:<p>
64 * AT+VGM?;+VGM=14;+CIMI<p>
65 * This is equivalent to:<p>
66 * AT+VGM?
67 * AT+VGM=14
68 * AT+CIMI
69 * Except that only one final result code is return (although several
70 * intermediate responses may be returned), and as soon as one command in the
71 * chain fails the rest are abandoned.<p>
72 *
73 * Handlers are registered by there command name via register(Char c, ...) or
74 * register(String s, ...). Handlers for Basic command should be registered by
75 * the basic command character, and handlers for Extended commands should be
76 * registered by String.<p>
77 *
78 * Refer to:<ul>
79 * <li>ITU-T Recommendation V.250
80 * <li>ETSI TS 127.007  (AT Command set for User Equipment, 3GPP TS 27.007)
81 * <li>Bluetooth Headset Profile Spec (K6)
82 * <li>Bluetooth Handsfree Profile Spec (HFP 1.5)
83 * </ul>
84 * @hide
85 */
86public class AtParser {
87
88    // Extended command type enumeration, only used internally
89    private static final int TYPE_ACTION = 0;   // AT+FOO
90    private static final int TYPE_READ = 1;     // AT+FOO?
91    private static final int TYPE_SET = 2;      // AT+FOO=
92    private static final int TYPE_TEST = 3;     // AT+FOO=?
93
94    private HashMap<String, AtCommandHandler> mExtHandlers;
95    private HashMap<Character, AtCommandHandler> mBasicHandlers;
96
97    private String mLastInput;  // for "A/" (repeat last command) support
98
99    /**
100     * Create a new AtParser.<p>
101     * No handlers are registered.
102     */
103    public AtParser() {
104        mBasicHandlers = new HashMap<Character, AtCommandHandler>();
105        mExtHandlers = new HashMap<String, AtCommandHandler>();
106        mLastInput = "";
107    }
108
109    /**
110     * Register a Basic command handler.<p>
111     * Basic command handlers are later called via their
112     * <code>handleBasicCommand(String args)</code> method.
113     * @param  command Command name - a single character
114     * @param  handler Handler to register
115     */
116    public void register(Character command, AtCommandHandler handler) {
117        mBasicHandlers.put(command, handler);
118    }
119
120    /**
121     * Register an Extended command handler.<p>
122     * Extended command handlers are later called via:<ul>
123     * <li><code>handleActionCommand()</code>
124     * <li><code>handleGetCommand()</code>
125     * <li><code>handleSetCommand()</code>
126     * <li><code>handleTestCommand()</code>
127     * </ul>
128     * Only one method will be called for each command processed.
129     * @param  command Command name - can be multiple characters
130     * @param  handler Handler to register
131     */
132    public void register(String command, AtCommandHandler handler) {
133        mExtHandlers.put(command, handler);
134    }
135
136
137    /**
138     * Strip input of whitespace and force Uppercase - except sections inside
139     * quotes. Also fixes unmatched quotes (by appending a quote). Double
140     * quotes " are the only quotes allowed by V.250
141     */
142    static private String clean(String input) {
143        StringBuilder out = new StringBuilder(input.length());
144
145        for (int i = 0; i < input.length(); i++) {
146            char c = input.charAt(i);
147            if (c == '"') {
148                int j = input.indexOf('"', i + 1 );  // search for closing "
149                if (j == -1) {  // unmatched ", insert one.
150                    out.append(input.substring(i, input.length()));
151                    out.append('"');
152                    break;
153                }
154                out.append(input.substring(i, j + 1));
155                i = j;
156            } else if (c != ' ') {
157                out.append(Character.toUpperCase(c));
158            }
159        }
160
161        return out.toString();
162    }
163
164    static private boolean isAtoZ(char c) {
165        return (c >= 'A' && c <= 'Z');
166    }
167
168    /**
169     * Find a character ch, ignoring quoted sections.
170     * Return input.length() if not found.
171     */
172    static private int findChar(char ch, String input, int fromIndex) {
173        for (int i = fromIndex; i < input.length(); i++) {
174            char c = input.charAt(i);
175            if (c == '"') {
176                i = input.indexOf('"', i + 1);
177                if (i == -1) {
178                    return input.length();
179                }
180            } else if (c == ch) {
181                return i;
182            }
183        }
184        return input.length();
185    }
186
187    /**
188     * Break an argument string into individual arguments (comma delimited).
189     * Integer arguments are turned into Integer objects. Otherwise a String
190     * object is used.
191     */
192    static private Object[] generateArgs(String input) {
193        int i = 0;
194        int j;
195        ArrayList<Object> out = new ArrayList<Object>();
196        while (i <= input.length()) {
197            j = findChar(',', input, i);
198
199            String arg = input.substring(i, j);
200            try {
201                out.add(new Integer(arg));
202            } catch (NumberFormatException e) {
203                out.add(arg);
204            }
205
206            i = j + 1; // move past comma
207        }
208        return out.toArray();
209    }
210
211    /**
212     * Return the index of the end of character after the last character in
213     * the extended command name. Uses the V.250 spec for allowed command
214     * names.
215     */
216    static private int findEndExtendedName(String input, int index) {
217        for (int i = index; i < input.length(); i++) {
218            char c = input.charAt(i);
219
220            // V.250 defines the following chars as legal extended command
221            // names
222            if (isAtoZ(c)) continue;
223            if (c >= '0' && c <= '9') continue;
224            switch (c) {
225            case '!':
226            case '%':
227            case '-':
228            case '.':
229            case '/':
230            case ':':
231            case '_':
232                continue;
233            default:
234                return i;
235            }
236        }
237        return input.length();
238    }
239
240    /**
241     * Processes an incoming AT command line.<p>
242     * This method will invoke zero or one command handler methods for each
243     * command in the command line.<p>
244     * @param raw_input The AT input, without EOL delimiter (e.g. <CR>).
245     * @return          Result object for this command line. This can be
246     *                  converted to a String[] response with toStrings().
247     */
248    public AtCommandResult process(String raw_input) {
249        String input = clean(raw_input);
250
251        // Handle "A/" (repeat previous line)
252        if (input.regionMatches(0, "A/", 0, 2)) {
253            input = new String(mLastInput);
254        } else {
255            mLastInput = new String(input);
256        }
257
258        // Handle empty line - no response necessary
259        if (input.equals("")) {
260            // Return []
261            return new AtCommandResult(AtCommandResult.UNSOLICITED);
262        }
263
264        // Anything else deserves an error
265        if (!input.regionMatches(0, "AT", 0, 2)) {
266            // Return ["ERROR"]
267            return new AtCommandResult(AtCommandResult.ERROR);
268        }
269
270        // Ok we have a command that starts with AT. Process it
271        int index = 2;
272        AtCommandResult result =
273                new AtCommandResult(AtCommandResult.UNSOLICITED);
274        while (index < input.length()) {
275            char c = input.charAt(index);
276
277            if (isAtoZ(c)) {
278                // Option 1: Basic Command
279                // Pass the rest of the line as is to the handler. Do not
280                // look for any more commands on this line.
281                String args = input.substring(index + 1);
282                if (mBasicHandlers.containsKey((Character)c)) {
283                    result.addResult(mBasicHandlers.get(
284                            (Character)c).handleBasicCommand(args));
285                    return result;
286                } else {
287                    // no handler
288                    result.addResult(
289                            new AtCommandResult(AtCommandResult.ERROR));
290                    return result;
291                }
292                // control never reaches here
293            }
294
295            if (c == '+') {
296                // Option 2: Extended Command
297                // Search for first non-name character. Short-circuit if
298                // we don't handle this command name.
299                int i = findEndExtendedName(input, index + 1);
300                String commandName = input.substring(index, i);
301                if (!mExtHandlers.containsKey(commandName)) {
302                    // no handler
303                    result.addResult(
304                            new AtCommandResult(AtCommandResult.ERROR));
305                    return result;
306                }
307                AtCommandHandler handler = mExtHandlers.get(commandName);
308
309                // Search for end of this command - this is usually the end of
310                // line
311                int endIndex = findChar(';', input, index);
312
313                // Determine what type of command this is.
314                // Default to TYPE_ACTION if we can't find anything else
315                // obvious.
316                int type;
317
318                if (i >= endIndex) {
319                    type = TYPE_ACTION;
320                } else if (input.charAt(i) == '?') {
321                    type = TYPE_READ;
322                } else if (input.charAt(i) == '=') {
323                    if (i + 1 < endIndex) {
324                        if (input.charAt(i + 1) == '?') {
325                            type = TYPE_TEST;
326                        } else {
327                            type = TYPE_SET;
328                        }
329                    } else {
330                        type = TYPE_SET;
331                    }
332                } else {
333                    type = TYPE_ACTION;
334                }
335
336                // Call this command. Short-circuit as soon as a command fails
337                switch (type) {
338                case TYPE_ACTION:
339                    result.addResult(handler.handleActionCommand());
340                    break;
341                case TYPE_READ:
342                    result.addResult(handler.handleReadCommand());
343                    break;
344                case TYPE_TEST:
345                    result.addResult(handler.handleTestCommand());
346                    break;
347                case TYPE_SET:
348                    Object[] args =
349                            generateArgs(input.substring(i + 1, endIndex));
350                    result.addResult(handler.handleSetCommand(args));
351                    break;
352                }
353                if (result.getResultCode() != AtCommandResult.OK) {
354                    return result;   // short-circuit
355                }
356
357                index = endIndex;
358            } else {
359                // Can't tell if this is a basic or extended command.
360                // Push forwards and hope we hit something.
361                index++;
362            }
363        }
364        // Finished processing (and all results were ok)
365        return result;
366    }
367}
368