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