1/*
2 * Copyright (C) 2010 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.net.sip;
18
19import java.util.ArrayList;
20import java.util.Arrays;
21import java.util.Locale;
22
23/**
24 * An object used to manipulate messages of Session Description Protocol (SDP).
25 * It is mainly designed for the uses of Session Initiation Protocol (SIP).
26 * Therefore, it only handles connection addresses ("c="), bandwidth limits,
27 * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this
28 * implementation does not support multicast sessions.
29 *
30 * <p>Here is an example code to create a session description.</p>
31 * <pre>
32 * SimpleSessionDescription description = new SimpleSessionDescription(
33 *     System.currentTimeMillis(), "1.2.3.4");
34 * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP");
35 * media.setRtpPayload(0, "PCMU/8000", null);
36 * media.setRtpPayload(8, "PCMA/8000", null);
37 * media.setRtpPayload(127, "telephone-event/8000", "0-15");
38 * media.setAttribute("sendrecv", "");
39 * </pre>
40 * <p>Invoking <code>description.encode()</code> will produce a result like the
41 * one below.</p>
42 * <pre>
43 * v=0
44 * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4
45 * s=-
46 * c=IN IP4 1.2.3.4
47 * t=0 0
48 * m=audio 56789 RTP/AVP 0 8 127
49 * a=rtpmap:0 PCMU/8000
50 * a=rtpmap:8 PCMA/8000
51 * a=rtpmap:127 telephone-event/8000
52 * a=fmtp:127 0-15
53 * a=sendrecv
54 * </pre>
55 * @hide
56 */
57public class SimpleSessionDescription {
58    private final Fields mFields = new Fields("voscbtka");
59    private final ArrayList<Media> mMedia = new ArrayList<Media>();
60
61    /**
62     * Creates a minimal session description from the given session ID and
63     * unicast address. The address is used in the origin field ("o=") and the
64     * connection field ("c="). See {@link SimpleSessionDescription} for an
65     * example of its usage.
66     */
67    public SimpleSessionDescription(long sessionId, String address) {
68        address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address;
69        mFields.parse("v=0");
70        mFields.parse(String.format(Locale.US, "o=- %d %d %s", sessionId,
71                System.currentTimeMillis(), address));
72        mFields.parse("s=-");
73        mFields.parse("t=0 0");
74        mFields.parse("c=" + address);
75    }
76
77    /**
78     * Creates a session description from the given message.
79     *
80     * @throws IllegalArgumentException if message is invalid.
81     */
82    public SimpleSessionDescription(String message) {
83        String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+");
84        Fields fields = mFields;
85
86        for (String line : lines) {
87            try {
88                if (line.charAt(1) != '=') {
89                    throw new IllegalArgumentException();
90                }
91                if (line.charAt(0) == 'm') {
92                    String[] parts = line.substring(2).split(" ", 4);
93                    String[] ports = parts[1].split("/", 2);
94                    Media media = newMedia(parts[0], Integer.parseInt(ports[0]),
95                            (ports.length < 2) ? 1 : Integer.parseInt(ports[1]),
96                            parts[2]);
97                    for (String format : parts[3].split(" ")) {
98                        media.setFormat(format, null);
99                    }
100                    fields = media;
101                } else {
102                    fields.parse(line);
103                }
104            } catch (Exception e) {
105                throw new IllegalArgumentException("Invalid SDP: " + line);
106            }
107        }
108    }
109
110    /**
111     * Creates a new media description in this session description.
112     *
113     * @param type The media type, e.g. {@code "audio"}.
114     * @param port The first transport port used by this media.
115     * @param portCount The number of contiguous ports used by this media.
116     * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
117     */
118    public Media newMedia(String type, int port, int portCount,
119            String protocol) {
120        Media media = new Media(type, port, portCount, protocol);
121        mMedia.add(media);
122        return media;
123    }
124
125    /**
126     * Returns all the media descriptions in this session description.
127     */
128    public Media[] getMedia() {
129        return mMedia.toArray(new Media[mMedia.size()]);
130    }
131
132    /**
133     * Encodes the session description and all its media descriptions in a
134     * string. Note that the result might be incomplete if a required field
135     * has never been added before.
136     */
137    public String encode() {
138        StringBuilder buffer = new StringBuilder();
139        mFields.write(buffer);
140        for (Media media : mMedia) {
141            media.write(buffer);
142        }
143        return buffer.toString();
144    }
145
146    /**
147     * Returns the connection address or {@code null} if it is not present.
148     */
149    public String getAddress() {
150        return mFields.getAddress();
151    }
152
153    /**
154     * Sets the connection address. The field will be removed if the address
155     * is {@code null}.
156     */
157    public void setAddress(String address) {
158        mFields.setAddress(address);
159    }
160
161    /**
162     * Returns the encryption method or {@code null} if it is not present.
163     */
164    public String getEncryptionMethod() {
165        return mFields.getEncryptionMethod();
166    }
167
168    /**
169     * Returns the encryption key or {@code null} if it is not present.
170     */
171    public String getEncryptionKey() {
172        return mFields.getEncryptionKey();
173    }
174
175    /**
176     * Sets the encryption method and the encryption key. The field will be
177     * removed if the method is {@code null}.
178     */
179    public void setEncryption(String method, String key) {
180        mFields.setEncryption(method, key);
181    }
182
183    /**
184     * Returns the types of the bandwidth limits.
185     */
186    public String[] getBandwidthTypes() {
187        return mFields.getBandwidthTypes();
188    }
189
190    /**
191     * Returns the bandwidth limit of the given type or {@code -1} if it is not
192     * present.
193     */
194    public int getBandwidth(String type) {
195        return mFields.getBandwidth(type);
196    }
197
198    /**
199     * Sets the bandwith limit for the given type. The field will be removed if
200     * the value is negative.
201     */
202    public void setBandwidth(String type, int value) {
203        mFields.setBandwidth(type, value);
204    }
205
206    /**
207     * Returns the names of all the attributes.
208     */
209    public String[] getAttributeNames() {
210        return mFields.getAttributeNames();
211    }
212
213    /**
214     * Returns the attribute of the given name or {@code null} if it is not
215     * present.
216     */
217    public String getAttribute(String name) {
218        return mFields.getAttribute(name);
219    }
220
221    /**
222     * Sets the attribute for the given name. The field will be removed if
223     * the value is {@code null}. To set a binary attribute, use an empty
224     * string as the value.
225     */
226    public void setAttribute(String name, String value) {
227        mFields.setAttribute(name, value);
228    }
229
230    /**
231     * This class represents a media description of a session description. It
232     * can only be created by {@link SimpleSessionDescription#newMedia}. Since
233     * the syntax is more restricted for RTP based protocols, two sets of access
234     * methods are implemented. See {@link SimpleSessionDescription} for an
235     * example of its usage.
236     */
237    public static class Media extends Fields {
238        private final String mType;
239        private final int mPort;
240        private final int mPortCount;
241        private final String mProtocol;
242        private ArrayList<String> mFormats = new ArrayList<String>();
243
244        private Media(String type, int port, int portCount, String protocol) {
245            super("icbka");
246            mType = type;
247            mPort = port;
248            mPortCount = portCount;
249            mProtocol = protocol;
250        }
251
252        /**
253         * Returns the media type.
254         */
255        public String getType() {
256            return mType;
257        }
258
259        /**
260         * Returns the first transport port used by this media.
261         */
262        public int getPort() {
263            return mPort;
264        }
265
266        /**
267         * Returns the number of contiguous ports used by this media.
268         */
269        public int getPortCount() {
270            return mPortCount;
271        }
272
273        /**
274         * Returns the transport protocol.
275         */
276        public String getProtocol() {
277            return mProtocol;
278        }
279
280        /**
281         * Returns the media formats.
282         */
283        public String[] getFormats() {
284            return mFormats.toArray(new String[mFormats.size()]);
285        }
286
287        /**
288         * Returns the {@code fmtp} attribute of the given format or
289         * {@code null} if it is not present.
290         */
291        public String getFmtp(String format) {
292            return super.get("a=fmtp:" + format, ' ');
293        }
294
295        /**
296         * Sets a format and its {@code fmtp} attribute. If the attribute is
297         * {@code null}, the corresponding field will be removed.
298         */
299        public void setFormat(String format, String fmtp) {
300            mFormats.remove(format);
301            mFormats.add(format);
302            super.set("a=rtpmap:" + format, ' ', null);
303            super.set("a=fmtp:" + format, ' ', fmtp);
304        }
305
306        /**
307         * Removes a format and its {@code fmtp} attribute.
308         */
309        public void removeFormat(String format) {
310            mFormats.remove(format);
311            super.set("a=rtpmap:" + format, ' ', null);
312            super.set("a=fmtp:" + format, ' ', null);
313        }
314
315        /**
316         * Returns the RTP payload types.
317         */
318        public int[] getRtpPayloadTypes() {
319            int[] types = new int[mFormats.size()];
320            int length = 0;
321            for (String format : mFormats) {
322                try {
323                    types[length] = Integer.parseInt(format);
324                    ++length;
325                } catch (NumberFormatException e) { }
326            }
327            return Arrays.copyOf(types, length);
328        }
329
330        /**
331         * Returns the {@code rtpmap} attribute of the given RTP payload type
332         * or {@code null} if it is not present.
333         */
334        public String getRtpmap(int type) {
335            return super.get("a=rtpmap:" + type, ' ');
336        }
337
338        /**
339         * Returns the {@code fmtp} attribute of the given RTP payload type or
340         * {@code null} if it is not present.
341         */
342        public String getFmtp(int type) {
343            return super.get("a=fmtp:" + type, ' ');
344        }
345
346        /**
347         * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp}
348         * attributes. If any of the attributes is {@code null}, the
349         * corresponding field will be removed. See
350         * {@link SimpleSessionDescription} for an example of its usage.
351         */
352        public void setRtpPayload(int type, String rtpmap, String fmtp) {
353            String format = String.valueOf(type);
354            mFormats.remove(format);
355            mFormats.add(format);
356            super.set("a=rtpmap:" + format, ' ', rtpmap);
357            super.set("a=fmtp:" + format, ' ', fmtp);
358        }
359
360        /**
361         * Removes a RTP payload and its {@code rtpmap} and {@code fmtp}
362         * attributes.
363         */
364        public void removeRtpPayload(int type) {
365            removeFormat(String.valueOf(type));
366        }
367
368        private void write(StringBuilder buffer) {
369            buffer.append("m=").append(mType).append(' ').append(mPort);
370            if (mPortCount != 1) {
371                buffer.append('/').append(mPortCount);
372            }
373            buffer.append(' ').append(mProtocol);
374            for (String format : mFormats) {
375                buffer.append(' ').append(format);
376            }
377            buffer.append("\r\n");
378            super.write(buffer);
379        }
380    }
381
382    /**
383     * This class acts as a set of fields, and the size of the set is expected
384     * to be small. Therefore, it uses a simple list instead of maps. Each field
385     * has three parts: a key, a delimiter, and a value. Delimiters are special
386     * because they are not included in binary attributes. As a result, the
387     * private methods, which are the building blocks of this class, all take
388     * the delimiter as an argument.
389     */
390    private static class Fields {
391        private final String mOrder;
392        private final ArrayList<String> mLines = new ArrayList<String>();
393
394        Fields(String order) {
395            mOrder = order;
396        }
397
398        /**
399         * Returns the connection address or {@code null} if it is not present.
400         */
401        public String getAddress() {
402            String address = get("c", '=');
403            if (address == null) {
404                return null;
405            }
406            String[] parts = address.split(" ");
407            if (parts.length != 3) {
408                return null;
409            }
410            int slash = parts[2].indexOf('/');
411            return (slash < 0) ? parts[2] : parts[2].substring(0, slash);
412        }
413
414        /**
415         * Sets the connection address. The field will be removed if the address
416         * is {@code null}.
417         */
418        public void setAddress(String address) {
419            if (address != null) {
420                address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") +
421                        address;
422            }
423            set("c", '=', address);
424        }
425
426        /**
427         * Returns the encryption method or {@code null} if it is not present.
428         */
429        public String getEncryptionMethod() {
430            String encryption = get("k", '=');
431            if (encryption == null) {
432                return null;
433            }
434            int colon = encryption.indexOf(':');
435            return (colon == -1) ? encryption : encryption.substring(0, colon);
436        }
437
438        /**
439         * Returns the encryption key or {@code null} if it is not present.
440         */
441        public String getEncryptionKey() {
442            String encryption = get("k", '=');
443            if (encryption == null) {
444                return null;
445            }
446            int colon = encryption.indexOf(':');
447            return (colon == -1) ? null : encryption.substring(0, colon + 1);
448        }
449
450        /**
451         * Sets the encryption method and the encryption key. The field will be
452         * removed if the method is {@code null}.
453         */
454        public void setEncryption(String method, String key) {
455            set("k", '=', (method == null || key == null) ?
456                    method : method + ':' + key);
457        }
458
459        /**
460         * Returns the types of the bandwidth limits.
461         */
462        public String[] getBandwidthTypes() {
463            return cut("b=", ':');
464        }
465
466        /**
467         * Returns the bandwidth limit of the given type or {@code -1} if it is
468         * not present.
469         */
470        public int getBandwidth(String type) {
471            String value = get("b=" + type, ':');
472            if (value != null) {
473                try {
474                    return Integer.parseInt(value);
475                } catch (NumberFormatException e) { }
476                setBandwidth(type, -1);
477            }
478            return -1;
479        }
480
481        /**
482         * Sets the bandwith limit for the given type. The field will be removed
483         * if the value is negative.
484         */
485        public void setBandwidth(String type, int value) {
486            set("b=" + type, ':', (value < 0) ? null : String.valueOf(value));
487        }
488
489        /**
490         * Returns the names of all the attributes.
491         */
492        public String[] getAttributeNames() {
493            return cut("a=", ':');
494        }
495
496        /**
497         * Returns the attribute of the given name or {@code null} if it is not
498         * present.
499         */
500        public String getAttribute(String name) {
501            return get("a=" + name, ':');
502        }
503
504        /**
505         * Sets the attribute for the given name. The field will be removed if
506         * the value is {@code null}. To set a binary attribute, use an empty
507         * string as the value.
508         */
509        public void setAttribute(String name, String value) {
510            set("a=" + name, ':', value);
511        }
512
513        private void write(StringBuilder buffer) {
514            for (int i = 0; i < mOrder.length(); ++i) {
515                char type = mOrder.charAt(i);
516                for (String line : mLines) {
517                    if (line.charAt(0) == type) {
518                        buffer.append(line).append("\r\n");
519                    }
520                }
521            }
522        }
523
524        /**
525         * Invokes {@link #set} after splitting the line into three parts.
526         */
527        private void parse(String line) {
528            char type = line.charAt(0);
529            if (mOrder.indexOf(type) == -1) {
530                return;
531            }
532            char delimiter = '=';
533            if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) {
534                delimiter = ' ';
535            } else if (type == 'b' || type == 'a') {
536                delimiter = ':';
537            }
538            int i = line.indexOf(delimiter);
539            if (i == -1) {
540                set(line, delimiter, "");
541            } else {
542                set(line.substring(0, i), delimiter, line.substring(i + 1));
543            }
544        }
545
546        /**
547         * Finds the key with the given prefix and returns its suffix.
548         */
549        private String[] cut(String prefix, char delimiter) {
550            String[] names = new String[mLines.size()];
551            int length = 0;
552            for (String line : mLines) {
553                if (line.startsWith(prefix)) {
554                    int i = line.indexOf(delimiter);
555                    if (i == -1) {
556                        i = line.length();
557                    }
558                    names[length] = line.substring(prefix.length(), i);
559                    ++length;
560                }
561            }
562            return Arrays.copyOf(names, length);
563        }
564
565        /**
566         * Returns the index of the key.
567         */
568        private int find(String key, char delimiter) {
569            int length = key.length();
570            for (int i = mLines.size() - 1; i >= 0; --i) {
571                String line = mLines.get(i);
572                if (line.startsWith(key) && (line.length() == length ||
573                        line.charAt(length) == delimiter)) {
574                    return i;
575                }
576            }
577            return -1;
578        }
579
580        /**
581         * Sets the key with the value or removes the key if the value is
582         * {@code null}.
583         */
584        private void set(String key, char delimiter, String value) {
585            int index = find(key, delimiter);
586            if (value != null) {
587                if (value.length() != 0) {
588                    key = key + delimiter + value;
589                }
590                if (index == -1) {
591                    mLines.add(key);
592                } else {
593                    mLines.set(index, key);
594                }
595            } else if (index != -1) {
596                mLines.remove(index);
597            }
598        }
599
600        /**
601         * Returns the value of the key.
602         */
603        private String get(String key, char delimiter) {
604            int index = find(key, delimiter);
605            if (index == -1) {
606                return null;
607            }
608            String line = mLines.get(index);
609            int length = key.length();
610            return (line.length() == length) ? "" : line.substring(length + 1);
611        }
612    }
613}
614