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 
17 package android.net.sip;
18 
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import 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  */
57 public 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      */
SimpleSessionDescription(long sessionId, String address)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      */
SimpleSessionDescription(String message)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      */
newMedia(String type, int port, int portCount, String protocol)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      */
getMedia()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      */
encode()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      */
getAddress()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      */
setAddress(String address)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      */
getEncryptionMethod()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      */
getEncryptionKey()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      */
setEncryption(String method, String key)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      */
getBandwidthTypes()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      */
getBandwidth(String type)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      */
setBandwidth(String type, int value)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      */
getAttributeNames()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      */
getAttribute(String name)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      */
setAttribute(String name, String value)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 
Media(String type, int port, int portCount, String protocol)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          */
getType()255         public String getType() {
256             return mType;
257         }
258 
259         /**
260          * Returns the first transport port used by this media.
261          */
getPort()262         public int getPort() {
263             return mPort;
264         }
265 
266         /**
267          * Returns the number of contiguous ports used by this media.
268          */
getPortCount()269         public int getPortCount() {
270             return mPortCount;
271         }
272 
273         /**
274          * Returns the transport protocol.
275          */
getProtocol()276         public String getProtocol() {
277             return mProtocol;
278         }
279 
280         /**
281          * Returns the media formats.
282          */
getFormats()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          */
getFmtp(String format)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          */
setFormat(String format, String fmtp)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          */
removeFormat(String format)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          */
getRtpPayloadTypes()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          */
getRtpmap(int type)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          */
getFmtp(int type)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          */
setRtpPayload(int type, String rtpmap, String fmtp)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          */
removeRtpPayload(int type)364         public void removeRtpPayload(int type) {
365             removeFormat(String.valueOf(type));
366         }
367 
write(StringBuilder buffer)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 
Fields(String order)394         Fields(String order) {
395             mOrder = order;
396         }
397 
398         /**
399          * Returns the connection address or {@code null} if it is not present.
400          */
getAddress()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          */
setAddress(String address)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          */
getEncryptionMethod()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          */
getEncryptionKey()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          */
setEncryption(String method, String key)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          */
getBandwidthTypes()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          */
getBandwidth(String type)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          */
setBandwidth(String type, int value)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          */
getAttributeNames()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          */
getAttribute(String name)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          */
setAttribute(String name, String value)509         public void setAttribute(String name, String value) {
510             set("a=" + name, ':', value);
511         }
512 
write(StringBuilder buffer)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          */
parse(String line)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          */
cut(String prefix, char delimiter)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          */
find(String key, char delimiter)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          */
set(String key, char delimiter, String value)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          */
get(String key, char delimiter)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