• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Copyright 2010, The Android Open Source Project
2  **
3  ** Licensed under the Apache License, Version 2.0 (the "License");
4  ** you may not use this file except in compliance with the License.
5  ** You may obtain a copy of the License at
6  **
7  **     http://www.apache.org/licenses/LICENSE-2.0
8  **
9  ** Unless required by applicable law or agreed to in writing, software
10  ** distributed under the License is distributed on an "AS IS" BASIS,
11  ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  ** See the License for the specific language governing permissions and
13  ** limitations under the License.
14  */
15 
16 package com.android.exchange.utility;
17 
18 import com.android.emailcommon.utility.Utility;
19 
20 import android.text.TextUtils;
21 
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.UnsupportedEncodingException;
25 
26 /**
27  * Class to generate iCalender object (*.ics) per RFC 5545.
28  */
29 public class SimpleIcsWriter {
30     private static final int MAX_LINE_LENGTH = 75; // In bytes, excluding CRLF
31     private static final int CHAR_MAX_BYTES_IN_UTF8 = 4;  // Used to be 6, but RFC3629 limited it.
32     private final ByteArrayOutputStream mOut = new ByteArrayOutputStream();
33 
SimpleIcsWriter()34     public SimpleIcsWriter() {
35     }
36 
37     /**
38      * Low level method to write a line, performing line-folding if necessary.
39      */
writeLine(String string)40     /* package for testing */ void writeLine(String string) {
41         int numBytes = 0;
42         for (byte b : Utility.toUtf8(string)) {
43             // Fold it when necessary.
44             // To make it simple, we assume all chars are 4 bytes.
45             // If not (and usually it's not), we end up wrapping earlier than necessary, but that's
46             // completely fine.
47             if (numBytes > (MAX_LINE_LENGTH - CHAR_MAX_BYTES_IN_UTF8)
48                     && Utility.isFirstUtf8Byte(b)) { // Only wrappable if it's before the first byte
49                 mOut.write((byte) '\r');
50                 mOut.write((byte) '\n');
51                 mOut.write((byte) '\t');
52                 numBytes = 1; // for TAB
53             }
54             mOut.write(b);
55             numBytes++;
56         }
57         mOut.write((byte) '\r');
58         mOut.write((byte) '\n');
59     }
60 
61     /**
62      * Write a tag with a value.
63      */
writeTag(String name, String value)64     public void writeTag(String name, String value) {
65         // Belt and suspenders here; don't crash on null value; just return
66         if (TextUtils.isEmpty(value)) {
67             return;
68         }
69 
70         // The following properties take a TEXT value, which need to be escaped.
71         // (These property names should be all interned, so using equals() should be faster than
72         // using a hash table.)
73 
74         // TODO make constants for these literals
75         if ("CALSCALE".equals(name)
76                 || "METHOD".equals(name)
77                 || "PRODID".equals(name)
78                 || "VERSION".equals(name)
79                 || "CATEGORIES".equals(name)
80                 || "CLASS".equals(name)
81                 || "COMMENT".equals(name)
82                 || "DESCRIPTION".equals(name)
83                 || "LOCATION".equals(name)
84                 || "RESOURCES".equals(name)
85                 || "STATUS".equals(name)
86                 || "SUMMARY".equals(name)
87                 || "TRANSP".equals(name)
88                 || "TZID".equals(name)
89                 || "TZNAME".equals(name)
90                 || "CONTACT".equals(name)
91                 || "RELATED-TO".equals(name)
92                 || "UID".equals(name)
93                 || "ACTION".equals(name)
94                 || "REQUEST-STATUS".equals(name)
95                 || "X-LIC-LOCATION".equals(name)
96                 ) {
97             value = escapeTextValue(value);
98         }
99         writeLine(name + ":" + value);
100     }
101 
102     /**
103      * For debugging
104      */
105     @Override
toString()106     public String toString() {
107         return Utility.fromUtf8(getBytes());
108     }
109 
110     /**
111      * @return the entire iCalendar invitation object.
112      */
getBytes()113     public byte[] getBytes() {
114         try {
115             mOut.flush();
116         } catch (IOException wonthappen) {
117         }
118         return mOut.toByteArray();
119     }
120 
121     /**
122      * Quote a param-value string, according to RFC 5545, section 3.1
123      */
quoteParamValue(String paramValue)124     public static String quoteParamValue(String paramValue) {
125         if (paramValue == null) {
126             return null;
127         }
128         // Wrap with double quotes.
129         // The spec doesn't allow putting double-quotes in a param value, so let's use single quotes
130         // as a substitute.
131         // It's not the smartest implementation.  e.g. we don't have to wrap an empty string with
132         // double quotes.  But it works.
133         return "\"" + paramValue.replace("\"", "'") + "\"";
134     }
135 
136     /**
137      * Escape a TEXT value per RFC 5545 section 3.3.11
138      */
escapeTextValue(String s)139     /* package for testing */ static String escapeTextValue(String s) {
140         StringBuilder sb = new StringBuilder(s.length());
141         for (int i = 0; i < s.length(); i++) {
142             char ch = s.charAt(i);
143             if (ch == '\n') {
144                 sb.append("\\n");
145             } else if (ch == '\r') {
146                 // Remove CR
147             } else if (ch == ',' || ch == ';' || ch == '\\') {
148                 sb.append('\\');
149                 sb.append(ch);
150             } else {
151                 sb.append(ch);
152             }
153         }
154         return sb.toString();
155     }
156 }
157