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