Damn it. Weird symlink-in-checkout bug. There goes our commit history. Sorry, all.

Guess I should go back to svk
This commit is contained in:
Jesse Vincent 2008-11-01 21:32:06 +00:00
parent 12c6e53141
commit 5491dee81b
245 changed files with 49373 additions and 0 deletions

View File

@ -0,0 +1,117 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
import java.util.Arrays;
/**
* <p>
* Represent a base 64 mapping. The 64 characters used in the encoding can be
* specified, since modified-UTF-7 uses other characters than UTF-7 (',' instead
* of '/').
* </p>
* <p>
* The exact type of the arguments and result values is adapted to the needs of
* the encoder and decoder, as opposed to following a strict interpretation of
* base 64.
* </p>
* <p>
* Base 64, as specified in RFC 2045, is an encoding used to encode bytes as
* characters. In (modified-)UTF-7 however, it is used to encode characters as
* bytes, using some intermediate steps:
* </p>
* <ol>
* <li>Encode all characters as a 16-bit (UTF-16) integer value</li>
* <li>Write this as stream of bytes (most-significant first)</li>
* <li>Encode these bytes using (modified) base 64 encoding</li>
* <li>Write the thus formed stream of characters as a stream of bytes, using
* ASCII encoding</li>
* </ol>
*
* @author Jaap Beetstra
*/
class Base64Util {
private static final int ALPHABET_LENGTH = 64;
private final char[] alphabet;
private final int[] inverseAlphabet;
/**
* Initializes the class with the specified encoding/decoding alphabet.
*
* @param alphabet
* @throws IllegalArgumentException if alphabet is not 64 characters long or
* contains characters which are not 7-bit ASCII
*/
Base64Util(final String alphabet) {
this.alphabet = alphabet.toCharArray();
if (alphabet.length() != ALPHABET_LENGTH)
throw new IllegalArgumentException("alphabet has incorrect length (should be 64, not "
+ alphabet.length() + ")");
inverseAlphabet = new int[128];
Arrays.fill(inverseAlphabet, -1);
for (int i = 0; i < this.alphabet.length; i++) {
final char ch = this.alphabet[i];
if (ch >= 128)
throw new IllegalArgumentException("invalid character in alphabet: " + ch);
inverseAlphabet[ch] = i;
}
}
/**
* Returns the integer value of the six bits represented by the specified
* character.
*
* @param ch The character, as a ASCII encoded byte
* @return The six bits, as an integer value, or -1 if the byte is not in
* the alphabet
*/
int getSextet(final byte ch) {
if (ch >= 128)
return -1;
return inverseAlphabet[ch];
}
/**
* Tells whether the alphabet contains the specified character.
*
* @param ch The character
* @return true if the alphabet contains <code>ch</code>, false otherwise
*/
boolean contains(final char ch) {
if (ch >= 128)
return false;
return inverseAlphabet[ch] >= 0;
}
/**
* Encodes the six bit group as a character.
*
* @param sextet The six bit group to be encoded
* @return The ASCII value of the character
*/
byte getChar(final int sextet) {
return (byte)alphabet[sextet];
}
}

View File

@ -0,0 +1,90 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* <p>
* Charset service-provider class used for both variants of the UTF-7 charset
* and the modified-UTF-7 charset.
* </p>
*
* @author Jaap Beetstra
*/
public class CharsetProvider extends java.nio.charset.spi.CharsetProvider {
private static final String UTF7_NAME = "UTF-7";
private static final String UTF7_O_NAME = "X-UTF-7-OPTIONAL";
private static final String UTF7_M_NAME = "X-MODIFIED-UTF-7";
private static final String[] UTF7_ALIASES = new String[] {
"UNICODE-1-1-UTF-7", "CSUNICODE11UTF7", "X-RFC2152", "X-RFC-2152"
};
private static final String[] UTF7_O_ALIASES = new String[] {
"X-RFC2152-OPTIONAL", "X-RFC-2152-OPTIONAL"
};
private static final String[] UTF7_M_ALIASES = new String[] {
"X-IMAP-MODIFIED-UTF-7", "X-IMAP4-MODIFIED-UTF7", "X-IMAP4-MODIFIED-UTF-7",
"X-RFC3501", "X-RFC-3501"
};
private Charset utf7charset = new UTF7Charset(UTF7_NAME, UTF7_ALIASES, false);
private Charset utf7oCharset = new UTF7Charset(UTF7_O_NAME, UTF7_O_ALIASES, true);
private Charset imap4charset = new ModifiedUTF7Charset(UTF7_M_NAME, UTF7_M_ALIASES);
private List charsets;
public CharsetProvider() {
charsets = Arrays.asList(new Object[] {
utf7charset, imap4charset, utf7oCharset
});
}
/**
* {@inheritDoc}
*/
public Charset charsetForName(String charsetName) {
charsetName = charsetName.toUpperCase();
for (Iterator iter = charsets.iterator(); iter.hasNext();) {
Charset charset = (Charset)iter.next();
if (charset.name().equals(charsetName))
return charset;
}
for (Iterator iter = charsets.iterator(); iter.hasNext();) {
Charset charset = (Charset)iter.next();
if (charset.aliases().contains(charsetName))
return charset;
}
return null;
}
/**
* {@inheritDoc}
*/
public Iterator charsets() {
return charsets.iterator();
}
}

View File

@ -0,0 +1,57 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
/**
* <p>
* The character set specified in RFC 3501 to use for IMAP4rev1 mailbox name
* encoding.
* </p>
*
* @see <a href="http://tools.ietf.org/html/rfc3501">RFC 3501< /a>
* @author Jaap Beetstra
*/
class ModifiedUTF7Charset extends UTF7StyleCharset {
private static final String MODIFIED_BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz" + "0123456789+,";
ModifiedUTF7Charset(String name, String[] aliases) {
super(name, aliases, MODIFIED_BASE64_ALPHABET, true);
}
boolean canEncodeDirectly(char ch) {
if (ch == shift())
return false;
return ch >= 0x20 && ch <= 0x7E;
}
byte shift() {
return '&';
}
byte unshift() {
return '-';
}
}

View File

@ -0,0 +1,75 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
/**
* <p>
* The character set specified in RFC 2152. Two variants are supported using the
* encodeOptional constructor flag
* </p>
*
* @see <a href="http://tools.ietf.org/html/rfc2152">RFC 2152< /a>
* @author Jaap Beetstra
*/
class UTF7Charset extends UTF7StyleCharset {
private static final String BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz" + "0123456789+/";
private static final String SET_D = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'(),-./:?";
private static final String SET_O = "!\"#$%&*;<=>@[]^_`{|}";
private static final String RULE_3 = " \t\r\n";
final String directlyEncoded;
UTF7Charset(String name, String[] aliases, boolean includeOptional) {
super(name, aliases, BASE64_ALPHABET, false);
if (includeOptional)
this.directlyEncoded = SET_D + SET_O + RULE_3;
else
this.directlyEncoded = SET_D + RULE_3;
}
/*
* (non-Javadoc)
* @see com.beetstra.jutf7.UTF7StyleCharset#canEncodeDirectly(char)
*/
boolean canEncodeDirectly(char ch) {
return directlyEncoded.indexOf(ch) >= 0;
}
/*
* (non-Javadoc)
* @see com.beetstra.jutf7.UTF7StyleCharset#shift()
*/
byte shift() {
return '+';
}
/*
* (non-Javadoc)
* @see com.beetstra.jutf7.UTF7StyleCharset#unshift()
*/
byte unshift() {
return '-';
}
}

View File

@ -0,0 +1,117 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Arrays;
import java.util.List;
/**
* <p>
* Abstract base class for UTF-7 style encoding and decoding.
* </p>
*
* @author Jaap Beetstra
*/
abstract class UTF7StyleCharset extends Charset {
private static final List CONTAINED = Arrays.asList(new String[] {
"US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16", "UTF-16LE", "UTF-16BE"
});
final boolean strict;
Base64Util base64;
/**
* <p>
* Besides the name and aliases, two additional parameters are required.
* First the base 64 alphabet used; in modified UTF-7 a slightly different
* alphabet is used. Additionally, it should be specified if encoders and
* decoders should be strict about the interpretation of malformed encoded
* sequences. This is used since modified UTF-7 specifically disallows some
* constructs which are allowed (or not specifically disallowed) in UTF-7
* (RFC 2152).
* </p>
*
* @param canonicalName The name as defined in java.nio.charset.Charset
* @param aliases The aliases as defined in java.nio.charset.Charset
* @param alphabet The base 64 alphabet used
* @param strict True if strict handling of sequences is requested
*/
protected UTF7StyleCharset(String canonicalName, String[] aliases, String alphabet,
boolean strict) {
super(canonicalName, aliases);
this.base64 = new Base64Util(alphabet);
this.strict = strict;
}
/*
* (non-Javadoc)
* @see java.nio.charset.Charset#contains(java.nio.charset.Charset)
*/
public boolean contains(final Charset cs) {
return CONTAINED.contains(cs.name());
}
/*
* (non-Javadoc)
* @see java.nio.charset.Charset#newDecoder()
*/
public CharsetDecoder newDecoder() {
return new UTF7StyleCharsetDecoder(this, base64, strict);
}
/*
* (non-Javadoc)
* @see java.nio.charset.Charset#newEncoder()
*/
public CharsetEncoder newEncoder() {
return new UTF7StyleCharsetEncoder(this, base64, strict);
}
/**
* Tells if a character can be encoded using simple (US-ASCII) encoding or
* requires base 64 encoding.
*
* @param ch The character
* @return True if the character can be encoded directly, false otherwise
*/
abstract boolean canEncodeDirectly(char ch);
/**
* Returns character used to switch to base 64 encoding.
*
* @return The shift character
*/
abstract byte shift();
/**
* Returns character used to switch from base 64 encoding to simple
* encoding.
*
* @return The unshift character
*/
abstract byte unshift();
}

View File

@ -0,0 +1,195 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
/**
* <p>
* The CharsetDecoder used to decode both variants of the UTF-7 charset and the
* modified-UTF-7 charset.
* </p>
*
* @author Jaap Beetstra
*/
class UTF7StyleCharsetDecoder extends CharsetDecoder {
private final Base64Util base64;
private final byte shift;
private final byte unshift;
private final boolean strict;
private boolean base64mode;
private int bitsRead;
private int tempChar;
private boolean justShifted;
private boolean justUnshifted;
UTF7StyleCharsetDecoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) {
super(cs, 0.6f, 1.0f);
this.base64 = base64;
this.strict = strict;
this.shift = cs.shift();
this.unshift = cs.unshift();
}
/*
* (non-Javadoc)
* @see java.nio.charset.CharsetDecoder#decodeLoop(java.nio.ByteBuffer,
* java.nio.CharBuffer)
*/
protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
while (in.hasRemaining()) {
byte b = in.get();
if (base64mode) {
if (b == unshift) {
if (base64bitsWaiting())
return malformed(in);
if (justShifted) {
if (!out.hasRemaining())
return overflow(in);
out.put((char)shift);
} else
justUnshifted = true;
setUnshifted();
} else {
if (!out.hasRemaining())
return overflow(in);
CoderResult result = handleBase64(in, out, b);
if (result != null)
return result;
}
justShifted = false;
} else {
if (b == shift) {
base64mode = true;
if (justUnshifted && strict)
return malformed(in);
justShifted = true;
continue;
}
if (!out.hasRemaining())
return overflow(in);
out.put((char)b);
justUnshifted = false;
}
}
return CoderResult.UNDERFLOW;
}
private CoderResult overflow(ByteBuffer in) {
in.position(in.position() - 1);
return CoderResult.OVERFLOW;
}
/**
* <p>
* Decodes a byte in <i>base 64 mode</i>. Will directly write a character to
* the output buffer if completed.
* </p>
*
* @param in The input buffer
* @param out The output buffer
* @param lastRead Last byte read from the input buffer
* @return CoderResult.malformed if a non-base 64 character was encountered
* in strict mode, null otherwise
*/
private CoderResult handleBase64(ByteBuffer in, CharBuffer out, byte lastRead) {
CoderResult result = null;
int sextet = base64.getSextet(lastRead);
if (sextet >= 0) {
bitsRead += 6;
if (bitsRead < 16) {
tempChar += sextet << (16 - bitsRead);
} else {
bitsRead -= 16;
tempChar += sextet >> (bitsRead);
out.put((char)tempChar);
tempChar = (sextet << (16 - bitsRead)) & 0xFFFF;
}
} else {
if (strict)
return malformed(in);
out.put((char)lastRead);
if (base64bitsWaiting())
result = malformed(in);
setUnshifted();
}
return result;
}
/*
* (non-Javadoc)
* @see java.nio.charset.CharsetDecoder#implFlush(java.nio.CharBuffer)
*/
protected CoderResult implFlush(CharBuffer out) {
if ((base64mode && strict) || base64bitsWaiting())
return CoderResult.malformedForLength(1);
return CoderResult.UNDERFLOW;
}
/*
* (non-Javadoc)
* @see java.nio.charset.CharsetDecoder#implReset()
*/
protected void implReset() {
setUnshifted();
justUnshifted = false;
}
/**
* <p>
* Resets the input buffer position to just before the last byte read, and
* returns a result indicating to skip the last byte.
* </p>
*
* @param in The input buffer
* @return CoderResult.malformedForLength(1);
*/
private CoderResult malformed(ByteBuffer in) {
in.position(in.position() - 1);
return CoderResult.malformedForLength(1);
}
/**
* @return True if there are base64 encoded characters waiting to be written
*/
private boolean base64bitsWaiting() {
return tempChar != 0 || bitsRead >= 6;
}
/**
* <p>
* Updates internal state to reflect the decoder is no longer in <i>base 64
* mode</i>
* </p>
*/
private void setUnshifted() {
base64mode = false;
bitsRead = 0;
tempChar = 0;
}
}

View File

@ -0,0 +1,217 @@
/* ====================================================================
* Copyright (c) 2006 J.T. Beetstra
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ====================================================================
*/
package com.beetstra.jutf7;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
/**
* <p>
* The CharsetEncoder used to encode both variants of the UTF-7 charset and the
* modified-UTF-7 charset.
* </p>
* <p>
* <strong>Please note this class does not behave strictly according to the
* specification in Sun Java VMs before 1.6.</strong> This is done to get around
* a bug in the implementation of
* {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)}. Unfortunately,
* that method cannot be overridden.
* </p>
*
* @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056">JDK
* bug 6221056< /a>
* @author Jaap Beetstra
*/
class UTF7StyleCharsetEncoder extends CharsetEncoder {
private static final float AVG_BYTES_PER_CHAR = 1.5f;
private static final float MAX_BYTES_PER_CHAR = 5.0f;
private final UTF7StyleCharset cs;
private final Base64Util base64;
private final byte shift;
private final byte unshift;
private final boolean strict;
private boolean base64mode;
private int bitsToOutput;
private int sextet;
static boolean useUglyHackToForceCallToFlushInJava5;
static {
String version = System.getProperty("java.specification.version");
String vendor = System.getProperty("java.vm.vendor");
useUglyHackToForceCallToFlushInJava5 = "1.4".equals(version) || "1.5".equals(version);
useUglyHackToForceCallToFlushInJava5 &= "Sun Microsystems Inc.".equals(vendor);
}
UTF7StyleCharsetEncoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) {
super(cs, AVG_BYTES_PER_CHAR, MAX_BYTES_PER_CHAR);
this.cs = cs;
this.base64 = base64;
this.strict = strict;
this.shift = cs.shift();
this.unshift = cs.unshift();
}
/*
* (non-Javadoc)
* @see java.nio.charset.CharsetEncoder#implReset()
*/
protected void implReset() {
base64mode = false;
sextet = 0;
bitsToOutput = 0;
}
/**
* {@inheritDoc}
* <p>
* Note that this method might return <code>CoderResult.OVERFLOW</code> (as
* is required by the specification) if insufficient space is available in
* the output buffer. However, calling it again on JDKs before Java 6
* triggers a bug in
* {@link java.nio.charset.CharsetEncoder#flush(ByteBuffer)} causing it to
* throw an IllegalStateException (the buggy method is <code>final</code>,
* thus cannot be overridden).
* </p>
*
* @see <a
* href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6227608">
* JDK bug 6227608< /a>
* @param out The output byte buffer
* @return A coder-result object describing the reason for termination
*/
protected CoderResult implFlush(ByteBuffer out) {
if (base64mode) {
if (out.remaining() < 2)
return CoderResult.OVERFLOW;
if (bitsToOutput != 0)
out.put(base64.getChar(sextet));
out.put(unshift);
}
return CoderResult.UNDERFLOW;
}
/**
* {@inheritDoc}
* <p>
* Note that this method might return <code>CoderResult.OVERFLOW</code>,
* even though there is sufficient space available in the output buffer.
* This is done to force the broken implementation of
* {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)} to call flush
* (the buggy method is <code>final</code>, thus cannot be overridden).
* </p>
* <p>
* However, String.getBytes() fails if CoderResult.OVERFLOW is returned,
* since this assumes it always allocates sufficient bytes (maxBytesPerChar
* * nr_of_chars). Thus, as an extra check, the size of the input buffer is
* compared against the size of the output buffer. A static variable is used
* to indicate if a broken java version is used.
* </p>
* <p>
* It is not possible to directly write the last few bytes, since more bytes
* might be waiting to be encoded then those available in the input buffer.
* </p>
*
* @see <a
* href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6221056">
* JDK bug 6221056< /a>
* @param in The input character buffer
* @param out The output byte buffer
* @return A coder-result object describing the reason for termination
*/
protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
while (in.hasRemaining()) {
if (out.remaining() < 4)
return CoderResult.OVERFLOW;
char ch = in.get();
if (cs.canEncodeDirectly(ch)) {
unshift(out, ch);
out.put((byte)ch);
} else if (!base64mode && ch == shift) {
out.put(shift);
out.put(unshift);
} else
encodeBase64(ch, out);
}
/*
* <HACK type="ugly"> These lines are required to trick JDK 1.5 and
* earlier into flushing when using Charset.encode(String),
* Charset.encode(CharBuffer) or CharsetEncoder.encode(CharBuffer)
* Without them, the last few bytes may be missing.
*/
if (base64mode && useUglyHackToForceCallToFlushInJava5
&& out.limit() != MAX_BYTES_PER_CHAR * in.limit())
return CoderResult.OVERFLOW;
/* </HACK> */
return CoderResult.UNDERFLOW;
}
/**
* <p>
* Writes the bytes necessary to leave <i>base 64 mode</i>. This might
* include an unshift character.
* </p>
*
* @param out
* @param ch
*/
private void unshift(ByteBuffer out, char ch) {
if (!base64mode)
return;
if (bitsToOutput != 0)
out.put(base64.getChar(sextet));
if (base64.contains(ch) || ch == unshift || strict)
out.put(unshift);
base64mode = false;
sextet = 0;
bitsToOutput = 0;
}
/**
* <p>
* Writes the bytes necessary to encode a character in <i>base 64 mode</i>.
* All bytes which are fully determined will be written. The fields
* <code>bitsToOutput</code> and <code>sextet</code> are used to remember
* the bytes not yet fully determined.
* </p>
*
* @param out
* @param ch
*/
private void encodeBase64(char ch, ByteBuffer out) {
if (!base64mode)
out.put(shift);
base64mode = true;
bitsToOutput += 16;
while (bitsToOutput >= 6) {
bitsToOutput -= 6;
sextet += (ch >> bitsToOutput);
sextet &= 0x3F;
out.put(base64.getChar(sextet));
sextet = 0;
}
sextet = (ch << (6 - bitsToOutput)) & 0x3F;
}
}

View File

@ -0,0 +1,372 @@
package com.fsck.k9;
import java.io.Serializable;
import java.util.Arrays;
import java.util.UUID;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
/**
* Account stores all of the settings for a single account defined by the user. It is able to save
* and delete itself given a Preferences to work with. Each account is defined by a UUID.
*/
public class Account implements Serializable {
public static final int DELETE_POLICY_NEVER = 0;
public static final int DELETE_POLICY_7DAYS = 1;
public static final int DELETE_POLICY_ON_DELETE = 2;
private static final long serialVersionUID = 2975156672298625121L;
String mUuid;
String mStoreUri;
String mLocalStoreUri;
String mTransportUri;
String mDescription;
String mName;
String mEmail;
String mSignature;
String mAlwaysBcc;
int mAutomaticCheckIntervalMinutes;
long mLastAutomaticCheckTime;
boolean mNotifyNewMail;
String mDraftsFolderName;
String mSentFolderName;
String mTrashFolderName;
String mOutboxFolderName;
int mAccountNumber;
boolean mVibrate;
String mRingtoneUri;
/**
* <pre>
* 0 Never
* 1 After 7 days
* 2 When I delete from inbox
* </pre>
*/
int mDeletePolicy;
public Account(Context context) {
// TODO Change local store path to something readable / recognizable
mUuid = UUID.randomUUID().toString();
mLocalStoreUri = "local://localhost/" + context.getDatabasePath(mUuid + ".db");
mAutomaticCheckIntervalMinutes = -1;
mAccountNumber = -1;
mNotifyNewMail = true;
mSignature = "Sent from my Android phone with K-9. Please excuse my brevity.";
mVibrate = false;
mRingtoneUri = "content://settings/system/notification_sound";
}
Account(Preferences preferences, String uuid) {
this.mUuid = uuid;
refresh(preferences);
}
/**
* Refresh the account from the stored settings.
*/
public void refresh(Preferences preferences) {
mStoreUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid
+ ".storeUri", null));
mLocalStoreUri = preferences.mSharedPreferences.getString(mUuid + ".localStoreUri", null);
mTransportUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid
+ ".transportUri", null));
mDescription = preferences.mSharedPreferences.getString(mUuid + ".description", null);
mAlwaysBcc = preferences.mSharedPreferences.getString(mUuid + ".alwaysBcc", mAlwaysBcc);
mName = preferences.mSharedPreferences.getString(mUuid + ".name", mName);
mEmail = preferences.mSharedPreferences.getString(mUuid + ".email", mEmail);
mSignature = preferences.mSharedPreferences.getString(mUuid + ".signature", mSignature);
mAutomaticCheckIntervalMinutes = preferences.mSharedPreferences.getInt(mUuid
+ ".automaticCheckIntervalMinutes", -1);
mLastAutomaticCheckTime = preferences.mSharedPreferences.getLong(mUuid
+ ".lastAutomaticCheckTime", 0);
mNotifyNewMail = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyNewMail",
false);
mDeletePolicy = preferences.mSharedPreferences.getInt(mUuid + ".deletePolicy", 0);
mDraftsFolderName = preferences.mSharedPreferences.getString(mUuid + ".draftsFolderName",
"Drafts");
mSentFolderName = preferences.mSharedPreferences.getString(mUuid + ".sentFolderName",
"Sent");
mTrashFolderName = preferences.mSharedPreferences.getString(mUuid + ".trashFolderName",
"Trash");
mOutboxFolderName = preferences.mSharedPreferences.getString(mUuid + ".outboxFolderName",
"Outbox");
mAccountNumber = preferences.mSharedPreferences.getInt(mUuid + ".accountNumber", 0);
mVibrate = preferences.mSharedPreferences.getBoolean(mUuid + ".vibrate", false);
mRingtoneUri = preferences.mSharedPreferences.getString(mUuid + ".ringtone",
"content://settings/system/notification_sound");
}
public String getUuid() {
return mUuid;
}
public String getStoreUri() {
return mStoreUri;
}
public void setStoreUri(String storeUri) {
this.mStoreUri = storeUri;
}
public String getTransportUri() {
return mTransportUri;
}
public void setTransportUri(String transportUri) {
this.mTransportUri = transportUri;
}
public String getDescription() {
return mDescription;
}
public void setDescription(String description) {
this.mDescription = description;
}
public String getName() {
return mName;
}
public void setName(String name) {
this.mName = name;
}
public String getSignature() {
return mSignature;
}
public void setSignature(String signature) {
this.mSignature = signature;
}
public String getEmail() {
return mEmail;
}
public void setEmail(String email) {
this.mEmail = email;
}
public String getAlwaysBcc() {
return mAlwaysBcc;
}
public void setAlwaysBcc(String alwaysBcc) {
this.mAlwaysBcc = alwaysBcc;
}
public boolean isVibrate() {
return mVibrate;
}
public void setVibrate(boolean vibrate) {
mVibrate = vibrate;
}
public String getRingtone() {
return mRingtoneUri;
}
public void setRingtone(String ringtoneUri) {
mRingtoneUri = ringtoneUri;
}
public void delete(Preferences preferences) {
String[] uuids = preferences.mSharedPreferences.getString("accountUuids", "").split(",");
StringBuffer sb = new StringBuffer();
for (int i = 0, length = uuids.length; i < length; i++) {
if (!uuids[i].equals(mUuid)) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append(uuids[i]);
}
}
String accountUuids = sb.toString();
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
editor.putString("accountUuids", accountUuids);
editor.remove(mUuid + ".storeUri");
editor.remove(mUuid + ".localStoreUri");
editor.remove(mUuid + ".transportUri");
editor.remove(mUuid + ".description");
editor.remove(mUuid + ".name");
editor.remove(mUuid + ".email");
editor.remove(mUuid + ".alwaysBcc");
editor.remove(mUuid + ".automaticCheckIntervalMinutes");
editor.remove(mUuid + ".lastAutomaticCheckTime");
editor.remove(mUuid + ".notifyNewMail");
editor.remove(mUuid + ".deletePolicy");
editor.remove(mUuid + ".draftsFolderName");
editor.remove(mUuid + ".sentFolderName");
editor.remove(mUuid + ".trashFolderName");
editor.remove(mUuid + ".outboxFolderName");
editor.remove(mUuid + ".accountNumber");
editor.remove(mUuid + ".vibrate");
editor.remove(mUuid + ".ringtone");
editor.commit();
}
public void save(Preferences preferences) {
if (!preferences.mSharedPreferences.getString("accountUuids", "").contains(mUuid)) {
/*
* When the account is first created we assign it a unique account number. The
* account number will be unique to that account for the lifetime of the account.
* So, we get all the existing account numbers, sort them ascending, loop through
* the list and check if the number is greater than 1 + the previous number. If so
* we use the previous number + 1 as the account number. This refills gaps.
* mAccountNumber starts as -1 on a newly created account. It must be -1 for this
* algorithm to work.
*
* I bet there is a much smarter way to do this. Anyone like to suggest it?
*/
Account[] accounts = preferences.getAccounts();
int[] accountNumbers = new int[accounts.length];
for (int i = 0; i < accounts.length; i++) {
accountNumbers[i] = accounts[i].getAccountNumber();
}
Arrays.sort(accountNumbers);
for (int accountNumber : accountNumbers) {
if (accountNumber > mAccountNumber + 1) {
break;
}
mAccountNumber = accountNumber;
}
mAccountNumber++;
String accountUuids = preferences.mSharedPreferences.getString("accountUuids", "");
accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid;
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
editor.putString("accountUuids", accountUuids);
editor.commit();
}
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri));
editor.putString(mUuid + ".localStoreUri", mLocalStoreUri);
editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri));
editor.putString(mUuid + ".description", mDescription);
editor.putString(mUuid + ".name", mName);
editor.putString(mUuid + ".email", mEmail);
editor.putString(mUuid + ".signature", mSignature);
editor.putString(mUuid + ".alwaysBcc", mAlwaysBcc);
editor.putInt(mUuid + ".automaticCheckIntervalMinutes", mAutomaticCheckIntervalMinutes);
editor.putLong(mUuid + ".lastAutomaticCheckTime", mLastAutomaticCheckTime);
editor.putBoolean(mUuid + ".notifyNewMail", mNotifyNewMail);
editor.putInt(mUuid + ".deletePolicy", mDeletePolicy);
editor.putString(mUuid + ".draftsFolderName", mDraftsFolderName);
editor.putString(mUuid + ".sentFolderName", mSentFolderName);
editor.putString(mUuid + ".trashFolderName", mTrashFolderName);
editor.putString(mUuid + ".outboxFolderName", mOutboxFolderName);
editor.putInt(mUuid + ".accountNumber", mAccountNumber);
editor.putBoolean(mUuid + ".vibrate", mVibrate);
editor.putString(mUuid + ".ringtone", mRingtoneUri);
editor.commit();
}
public String toString() {
return mDescription;
}
public Uri getContentUri() {
return Uri.parse("content://accounts/" + getUuid());
}
public String getLocalStoreUri() {
return mLocalStoreUri;
}
public void setLocalStoreUri(String localStoreUri) {
this.mLocalStoreUri = localStoreUri;
}
/**
* Returns -1 for never.
*/
public int getAutomaticCheckIntervalMinutes() {
return mAutomaticCheckIntervalMinutes;
}
/**
* @param automaticCheckIntervalMinutes or -1 for never.
*/
public void setAutomaticCheckIntervalMinutes(int automaticCheckIntervalMinutes) {
this.mAutomaticCheckIntervalMinutes = automaticCheckIntervalMinutes;
}
public long getLastAutomaticCheckTime() {
return mLastAutomaticCheckTime;
}
public void setLastAutomaticCheckTime(long lastAutomaticCheckTime) {
this.mLastAutomaticCheckTime = lastAutomaticCheckTime;
}
public boolean isNotifyNewMail() {
return mNotifyNewMail;
}
public void setNotifyNewMail(boolean notifyNewMail) {
this.mNotifyNewMail = notifyNewMail;
}
public int getDeletePolicy() {
return mDeletePolicy;
}
public void setDeletePolicy(int deletePolicy) {
this.mDeletePolicy = deletePolicy;
}
public String getDraftsFolderName() {
return mDraftsFolderName;
}
public void setDraftsFolderName(String draftsFolderName) {
mDraftsFolderName = draftsFolderName;
}
public String getSentFolderName() {
return mSentFolderName;
}
public void setSentFolderName(String sentFolderName) {
mSentFolderName = sentFolderName;
}
public String getTrashFolderName() {
return mTrashFolderName;
}
public void setTrashFolderName(String trashFolderName) {
mTrashFolderName = trashFolderName;
}
public String getOutboxFolderName() {
return mOutboxFolderName;
}
public void setOutboxFolderName(String outboxFolderName) {
mOutboxFolderName = outboxFolderName;
}
public int getAccountNumber() {
return mAccountNumber;
}
@Override
public boolean equals(Object o) {
if (o instanceof Account) {
return ((Account)o).mUuid.equals(mUuid);
}
return super.equals(o);
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fsck.k9;
import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.People;
import android.view.View;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
import com.fsck.k9.mail.Address;
public class EmailAddressAdapter extends ResourceCursorAdapter {
public static final int NAME_INDEX = 1;
public static final int DATA_INDEX = 2;
private static final String SORT_ORDER = People.TIMES_CONTACTED + " DESC, " + People.NAME;
private ContentResolver mContentResolver;
private static final String[] PROJECTION = {
ContactMethods._ID, // 0
ContactMethods.NAME, // 1
ContactMethods.DATA
// 2
};
public EmailAddressAdapter(Context context) {
super(context, R.layout.recipient_dropdown_item, null);
mContentResolver = context.getContentResolver();
}
@Override
public final String convertToString(Cursor cursor) {
String name = cursor.getString(NAME_INDEX);
String address = cursor.getString(DATA_INDEX);
return new Address(address, name).toString();
}
@Override
public final void bindView(View view, Context context, Cursor cursor) {
TextView text1 = (TextView)view.findViewById(R.id.text1);
TextView text2 = (TextView)view.findViewById(R.id.text2);
text1.setText(cursor.getString(NAME_INDEX));
text2.setText(cursor.getString(DATA_INDEX));
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
String where = null;
if (constraint != null) {
String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%');
StringBuilder s = new StringBuilder();
s.append("(people.name LIKE ");
s.append(filter);
s.append(") OR (contact_methods.data LIKE ");
s.append(filter);
s.append(")");
where = s.toString();
}
return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER);
}
}

View File

@ -0,0 +1,18 @@
package com.fsck.k9;
import com.fsck.k9.mail.Address;
import android.util.Config;
import android.util.Log;
import android.widget.AutoCompleteTextView.Validator;
public class EmailAddressValidator implements Validator {
public CharSequence fixText(CharSequence invalidText) {
return "";
}
public boolean isValid(CharSequence text) {
return Address.parse(text.toString()).length > 0;
}
}

View File

@ -0,0 +1,60 @@
package com.fsck.k9;
import java.io.IOException;
import java.io.InputStream;
/**
* A filtering InputStream that stops allowing reads after the given length has been read. This
* is used to allow a client to read directly from an underlying protocol stream without reading
* past where the protocol handler intended the client to read.
*/
public class FixedLengthInputStream extends InputStream {
private InputStream mIn;
private int mLength;
private int mCount;
public FixedLengthInputStream(InputStream in, int length) {
this.mIn = in;
this.mLength = length;
}
@Override
public int available() throws IOException {
return mLength - mCount;
}
@Override
public int read() throws IOException {
if (mCount < mLength) {
mCount++;
return mIn.read();
} else {
return -1;
}
}
@Override
public int read(byte[] b, int offset, int length) throws IOException {
if (mCount < mLength) {
int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
if (d == -1) {
return -1;
} else {
mCount += d;
return d;
}
} else {
return -1;
}
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public String toString() {
return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
}
}

View File

@ -0,0 +1,14 @@
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package com.fsck.k9;
public final class Manifest {
public static final class permission {
public static final String READ_ATTACHMENT="com.fsck.k9.permission.READ_ATTACHMENT";
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
package com.fsck.k9;
import android.content.Context;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
/**
* Defines the interface that MessagingController will use to callback to requesters. This class
* is defined as non-abstract so that someone who wants to receive only a few messages can
* do so without implementing the entire interface. It is highly recommended that users of
* this interface use the @Override annotation in their implementations to avoid being caught by
* changes in this class.
*/
public class MessagingListener {
public void listFoldersStarted(Account account) {
}
public void listFolders(Account account, Folder[] folders) {
}
public void listFoldersFailed(Account account, String message) {
}
public void listFoldersFinished(Account account) {
}
public void listLocalMessagesStarted(Account account, String folder) {
}
public void listLocalMessages(Account account, String folder, Message[] messages) {
}
public void listLocalMessagesFailed(Account account, String folder, String message) {
}
public void listLocalMessagesFinished(Account account, String folder) {
}
public void synchronizeMailboxStarted(Account account, String folder) {
}
public void synchronizeMailboxNewMessage(Account account, String folder, Message message) {
}
public void synchronizeMailboxRemovedMessage(Account account, String folder,Message message) {
}
public void synchronizeMailboxFinished(Account account, String folder,
int totalMessagesInMailbox, int numNewMessages) {
}
public void synchronizeMailboxFailed(Account account, String folder,
String message) {
}
public void loadMessageForViewStarted(Account account, String folder, String uid) {
}
public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
Message message) {
}
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
Message message) {
}
public void loadMessageForViewFinished(Account account, String folder, String uid,
Message message) {
}
public void loadMessageForViewFailed(Account account, String folder, String uid, String message) {
}
public void checkMailStarted(Context context, Account account) {
}
public void checkMailFinished(Context context, Account account) {
}
public void checkMailFailed(Context context, Account account, String reason) {
}
public void sendPendingMessagesCompleted(Account account) {
}
public void emptyTrashCompleted(Account account) {
}
public void messageUidChanged(Account account, String folder, String oldUid, String newUid) {
}
public void loadAttachmentStarted(
Account account,
Message message,
Part part,
Object tag,
boolean requiresDownload)
{
}
public void loadAttachmentFinished(
Account account,
Message message,
Part part,
Object tag)
{
}
public void loadAttachmentFailed(
Account account,
Message message,
Part part,
Object tag,
String reason)
{
}
/**
* General notification messages subclasses can override to be notified that the controller
* has completed a command. This is useful for turning off progress indicators that may have
* been left over from previous commands.
* @param moreCommandsToRun True if the controller will continue on to another command
* immediately.
*/
public void controllerCommandCompleted(boolean moreCommandsToRun) {
}
}

View File

@ -0,0 +1,64 @@
package com.fsck.k9;
import java.io.IOException;
import java.io.InputStream;
/**
* A filtering InputStream that allows single byte "peeks" without consuming the byte. The
* client of this stream can call peek() to see the next available byte in the stream
* and a subsequent read will still return the peeked byte.
*/
public class PeekableInputStream extends InputStream {
private InputStream mIn;
private boolean mPeeked;
private int mPeekedByte;
public PeekableInputStream(InputStream in) {
this.mIn = in;
}
@Override
public int read() throws IOException {
if (!mPeeked) {
return mIn.read();
} else {
mPeeked = false;
return mPeekedByte;
}
}
public int peek() throws IOException {
if (!mPeeked) {
mPeekedByte = read();
mPeeked = true;
}
return mPeekedByte;
}
@Override
public int read(byte[] b, int offset, int length) throws IOException {
if (!mPeeked) {
return mIn.read(b, offset, length);
} else {
b[0] = (byte)mPeekedByte;
mPeeked = false;
int r = mIn.read(b, offset + 1, length - 1);
if (r == -1) {
return 1;
} else {
return r + 1;
}
}
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
public String toString() {
return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
mIn.toString(), mPeeked, mPeekedByte);
}
}

View File

@ -0,0 +1,123 @@
package com.fsck.k9;
import java.util.Arrays;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Config;
import android.util.Log;
public class Preferences {
private static Preferences preferences;
SharedPreferences mSharedPreferences;
private Preferences(Context context) {
mSharedPreferences = context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE);
}
/**
* TODO need to think about what happens if this gets GCed along with the
* Activity that initialized it. Do we lose ability to read Preferences in
* further Activities? Maybe this should be stored in the Application
* context.
*
* @return
*/
public static synchronized Preferences getPreferences(Context context) {
if (preferences == null) {
preferences = new Preferences(context);
}
return preferences;
}
/**
* Returns an array of the accounts on the system. If no accounts are
* registered the method returns an empty array.
*
* @return
*/
public Account[] getAccounts() {
String accountUuids = mSharedPreferences.getString("accountUuids", null);
if (accountUuids == null || accountUuids.length() == 0) {
return new Account[] {};
}
String[] uuids = accountUuids.split(",");
Account[] accounts = new Account[uuids.length];
for (int i = 0, length = uuids.length; i < length; i++) {
accounts[i] = new Account(this, uuids[i]);
}
return accounts;
}
public Account getAccountByContentUri(Uri uri) {
return new Account(this, uri.getPath().substring(1));
}
/**
* Returns the Account marked as default. If no account is marked as default
* the first account in the list is marked as default and then returned. If
* there are no accounts on the system the method returns null.
*
* @return
*/
public Account getDefaultAccount() {
String defaultAccountUuid = mSharedPreferences.getString("defaultAccountUuid", null);
Account defaultAccount = null;
Account[] accounts = getAccounts();
if (defaultAccountUuid != null) {
for (Account account : accounts) {
if (account.getUuid().equals(defaultAccountUuid)) {
defaultAccount = account;
break;
}
}
}
if (defaultAccount == null) {
if (accounts.length > 0) {
defaultAccount = accounts[0];
setDefaultAccount(defaultAccount);
}
}
return defaultAccount;
}
public void setDefaultAccount(Account account) {
mSharedPreferences.edit().putString("defaultAccountUuid", account.getUuid()).commit();
}
public void setEnableDebugLogging(boolean value) {
mSharedPreferences.edit().putBoolean("enableDebugLogging", value).commit();
}
public boolean geteEnableDebugLogging() {
return mSharedPreferences.getBoolean("enableDebugLogging", false);
}
public void setEnableSensitiveLogging(boolean value) {
mSharedPreferences.edit().putBoolean("enableSensitiveLogging", value).commit();
}
public boolean getEnableSensitiveLogging() {
return mSharedPreferences.getBoolean("enableSensitiveLogging", false);
}
public void save() {
}
public void clear() {
mSharedPreferences.edit().clear().commit();
}
public void dump() {
if (Config.LOGV) {
for (String key : mSharedPreferences.getAll().keySet()) {
Log.v(k9.LOG_TAG, key + " = " + mSharedPreferences.getAll().get(key));
}
}
}
}

452
src/com/fsck/k9/R.java Normal file
View File

@ -0,0 +1,452 @@
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
package com.fsck.k9;
public final class R {
public static final class array {
public static final int account_settings_check_frequency_entries=0x7f050000;
public static final int account_settings_check_frequency_values=0x7f050001;
}
public static final class attr {
}
public static final class color {
public static final int folder_message_list_child_background=0x7f070000;
}
public static final class dimen {
public static final int button_minWidth=0x7f080000;
}
public static final class drawable {
public static final int appointment_indicator_leftside_1=0x7f020000;
public static final int appointment_indicator_leftside_10=0x7f020001;
public static final int appointment_indicator_leftside_11=0x7f020002;
public static final int appointment_indicator_leftside_12=0x7f020003;
public static final int appointment_indicator_leftside_13=0x7f020004;
public static final int appointment_indicator_leftside_14=0x7f020005;
public static final int appointment_indicator_leftside_15=0x7f020006;
public static final int appointment_indicator_leftside_16=0x7f020007;
public static final int appointment_indicator_leftside_17=0x7f020008;
public static final int appointment_indicator_leftside_18=0x7f020009;
public static final int appointment_indicator_leftside_19=0x7f02000a;
public static final int appointment_indicator_leftside_2=0x7f02000b;
public static final int appointment_indicator_leftside_20=0x7f02000c;
public static final int appointment_indicator_leftside_21=0x7f02000d;
public static final int appointment_indicator_leftside_3=0x7f02000e;
public static final int appointment_indicator_leftside_4=0x7f02000f;
public static final int appointment_indicator_leftside_5=0x7f020010;
public static final int appointment_indicator_leftside_6=0x7f020011;
public static final int appointment_indicator_leftside_7=0x7f020012;
public static final int appointment_indicator_leftside_8=0x7f020013;
public static final int appointment_indicator_leftside_9=0x7f020014;
public static final int attached_image_placeholder=0x7f020015;
public static final int bottombar_565=0x7f020016;
public static final int btn_dialog=0x7f020017;
public static final int btn_dialog_disable=0x7f020018;
public static final int btn_dialog_disable_focused=0x7f020019;
public static final int btn_dialog_normal=0x7f02001a;
public static final int btn_dialog_pressed=0x7f02001b;
public static final int btn_dialog_selected=0x7f02001c;
public static final int button_indicator_next=0x7f02001d;
public static final int divider_horizontal_email=0x7f02001e;
public static final int email_quoted_bar=0x7f02001f;
public static final int expander_ic_folder=0x7f020020;
public static final int expander_ic_folder_maximized=0x7f020021;
public static final int expander_ic_folder_minimized=0x7f020022;
public static final int folder_message_list_child_background=0x7f020023;
public static final int ic_delete=0x7f020024;
public static final int ic_email_attachment=0x7f020025;
public static final int ic_email_attachment_small=0x7f020026;
public static final int ic_email_caret_double_light=0x7f020027;
public static final int ic_email_caret_single_light=0x7f020028;
public static final int ic_email_thread_open_bottom_default=0x7f020029;
public static final int ic_email_thread_open_top_default=0x7f02002a;
public static final int ic_menu_account_list=0x7f02002b;
public static final int ic_menu_add=0x7f02002c;
public static final int ic_menu_archive=0x7f02002d;
public static final int ic_menu_attachment=0x7f02002e;
public static final int ic_menu_cc=0x7f02002f;
public static final int ic_menu_close_clear_cancel=0x7f020030;
public static final int ic_menu_compose=0x7f020031;
public static final int ic_menu_delete=0x7f020032;
public static final int ic_menu_edit=0x7f020033;
public static final int ic_menu_forward_mail=0x7f020034;
public static final int ic_menu_inbox=0x7f020035;
public static final int ic_menu_mark=0x7f020036;
public static final int ic_menu_navigate=0x7f020037;
public static final int ic_menu_preferences=0x7f020038;
public static final int ic_menu_refresh=0x7f020039;
public static final int ic_menu_reply=0x7f02003a;
public static final int ic_menu_reply_all=0x7f02003b;
public static final int ic_menu_save_draft=0x7f02003c;
public static final int ic_menu_search=0x7f02003d;
public static final int ic_menu_send=0x7f02003e;
public static final int ic_mms_attachment_small=0x7f02003f;
public static final int icon=0x7f020040;
public static final int stat_notify_email_generic=0x7f020041;
public static final int text_box=0x7f020042;
public static final int text_box_light=0x7f020043;
}
public static final class id {
public static final int account_always_bcc=0x7f0a000b;
public static final int account_check_frequency=0x7f0a0018;
public static final int account_default=0x7f0a0005;
public static final int account_delete_policy=0x7f0a0013;
public static final int account_delete_policy_label=0x7f0a0012;
public static final int account_description=0x7f0a0016;
public static final int account_email=0x7f0a0002;
public static final int account_name=0x7f0a000a;
public static final int account_notify=0x7f0a0019;
public static final int account_password=0x7f0a0003;
public static final int account_port=0x7f0a0010;
public static final int account_require_login=0x7f0a001a;
public static final int account_require_login_settings=0x7f0a001b;
public static final int account_security_type=0x7f0a0011;
public static final int account_server=0x7f0a000f;
public static final int account_server_label=0x7f0a000e;
public static final int account_settings=0x7f0a004e;
public static final int account_signature=0x7f0a000c;
public static final int account_username=0x7f0a000d;
public static final int accounts=0x7f0a004d;
public static final int add_attachment=0x7f0a0053;
public static final int add_cc_bcc=0x7f0a004f;
public static final int add_new_account=0x7f0a001d;
public static final int attachment=0x7f0a003d;
public static final int attachment_delete=0x7f0a0033;
public static final int attachment_icon=0x7f0a0039;
public static final int attachment_info=0x7f0a003a;
public static final int attachment_name=0x7f0a0034;
public static final int attachments=0x7f0a002e;
public static final int bcc=0x7f0a002d;
public static final int cancel=0x7f0a0009;
public static final int cc=0x7f0a002c;
public static final int check_mail=0x7f0a0047;
public static final int chip=0x7f0a0024;
public static final int compose=0x7f0a0048;
public static final int date=0x7f0a0026;
public static final int debug_logging=0x7f0a0022;
public static final int delete=0x7f0a0038;
public static final int delete_account=0x7f0a0046;
public static final int description=0x7f0a001e;
public static final int discard=0x7f0a0052;
public static final int done=0x7f0a0017;
public static final int download=0x7f0a003b;
public static final int dump_settings=0x7f0a0049;
public static final int edit_account=0x7f0a0045;
public static final int email=0x7f0a001f;
public static final int empty=0x7f0a001c;
public static final int folder_name=0x7f0a0029;
public static final int folder_status=0x7f0a002a;
public static final int forward=0x7f0a004a;
public static final int from=0x7f0a0025;
public static final int imap=0x7f0a0001;
public static final int imap_path_prefix=0x7f0a0015;
public static final int imap_path_prefix_section=0x7f0a0014;
public static final int main_text=0x7f0a0028;
public static final int manual_setup=0x7f0a0006;
public static final int mark_as_read=0x7f0a004b;
public static final int mark_as_unread=0x7f0a0054;
public static final int message=0x7f0a0007;
public static final int message_content=0x7f0a002f;
public static final int new_message_count=0x7f0a0020;
public static final int next=0x7f0a0004;
public static final int open=0x7f0a0044;
public static final int pop=0x7f0a0000;
public static final int previous=0x7f0a0035;
public static final int progress=0x7f0a0008;
public static final int quoted_text=0x7f0a0032;
public static final int quoted_text_bar=0x7f0a0030;
public static final int quoted_text_delete=0x7f0a0031;
public static final int refresh=0x7f0a004c;
public static final int reply=0x7f0a0036;
public static final int reply_all=0x7f0a0037;
public static final int save=0x7f0a0051;
public static final int send=0x7f0a0050;
public static final int sensitive_logging=0x7f0a0023;
public static final int show_pictures=0x7f0a0041;
public static final int show_pictures_section=0x7f0a0040;
public static final int subject=0x7f0a0027;
public static final int text1=0x7f0a0042;
public static final int text2=0x7f0a0043;
public static final int to=0x7f0a002b;
public static final int to_container=0x7f0a003e;
public static final int to_label=0x7f0a003f;
public static final int version=0x7f0a0021;
public static final int view=0x7f0a003c;
}
public static final class layout {
public static final int account_setup_account_type=0x7f030000;
public static final int account_setup_basics=0x7f030001;
public static final int account_setup_check_settings=0x7f030002;
public static final int account_setup_composition=0x7f030003;
public static final int account_setup_incoming=0x7f030004;
public static final int account_setup_names=0x7f030005;
public static final int account_setup_options=0x7f030006;
public static final int account_setup_outgoing=0x7f030007;
public static final int accounts=0x7f030008;
public static final int accounts_item=0x7f030009;
public static final int debug=0x7f03000a;
public static final int folder_message_list_child=0x7f03000b;
public static final int folder_message_list_child_footer=0x7f03000c;
public static final int folder_message_list_group=0x7f03000d;
public static final int message_compose=0x7f03000e;
public static final int message_compose_attachment=0x7f03000f;
public static final int message_view=0x7f030010;
public static final int message_view_attachment=0x7f030011;
public static final int message_view_header=0x7f030012;
public static final int recipient_dropdown_item=0x7f030013;
}
public static final class menu {
public static final int accounts_context=0x7f090000;
public static final int accounts_option=0x7f090001;
public static final int debug_option=0x7f090002;
public static final int folder_message_list_context=0x7f090003;
public static final int folder_message_list_option=0x7f090004;
public static final int message_compose_option=0x7f090005;
public static final int message_view_option=0x7f090006;
}
public static final class string {
public static final int account_delete_dlg_instructions_fmt=0x7f0600c8;
public static final int account_delete_dlg_title=0x7f0600c7;
public static final int account_settings_action=0x7f06001b;
public static final int account_settings_add_account_label=0x7f0600b9;
public static final int account_settings_always_bcc_label=0x7f0600c3;
public static final int account_settings_always_bcc_summary=0x7f0600c4;
public static final int account_settings_composition_label=0x7f0600c2;
public static final int account_settings_composition_title=0x7f0600c1;
public static final int account_settings_default=0x7f0600ad;
public static final int account_settings_default_label=0x7f0600ae;
public static final int account_settings_default_summary=0x7f0600af;
public static final int account_settings_description_label=0x7f0600ba;
public static final int account_settings_email_label=0x7f0600b1;
public static final int account_settings_incoming_label=0x7f0600b5;
public static final int account_settings_incoming_summary=0x7f0600b6;
public static final int account_settings_mail_check_frequency_label=0x7f0600b4;
public static final int account_settings_name_label=0x7f0600bb;
public static final int account_settings_notifications=0x7f0600bc;
public static final int account_settings_notify_label=0x7f0600b0;
public static final int account_settings_notify_summary=0x7f0600b2;
public static final int account_settings_outgoing_label=0x7f0600b7;
public static final int account_settings_outgoing_summary=0x7f0600b8;
public static final int account_settings_ringtone=0x7f0600bf;
public static final int account_settings_servers=0x7f0600c0;
public static final int account_settings_show_combined_label=0x7f0600b3;
public static final int account_settings_signature_label=0x7f0600c5;
public static final int account_settings_signature_summary=0x7f0600c6;
public static final int account_settings_title_fmt=0x7f0600ac;
public static final int account_settings_vibrate_enable=0x7f0600bd;
public static final int account_settings_vibrate_summary=0x7f0600be;
public static final int account_setup_account_type_imap_action=0x7f060079;
public static final int account_setup_account_type_instructions=0x7f060077;
public static final int account_setup_account_type_pop_action=0x7f060078;
public static final int account_setup_account_type_title=0x7f060076;
public static final int account_setup_basics_default_label=0x7f060069;
public static final int account_setup_basics_email_error_duplicate_fmt=0x7f060067;
public static final int account_setup_basics_email_error_invalid_fmt=0x7f060066;
public static final int account_setup_basics_email_hint=0x7f060065;
public static final int account_setup_basics_instructions=0x7f060063;
public static final int account_setup_basics_instructions2_fmt=0x7f060064;
public static final int account_setup_basics_manual_setup_action=0x7f06006a;
public static final int account_setup_basics_password_hint=0x7f060068;
public static final int account_setup_basics_title=0x7f060062;
public static final int account_setup_check_settings_canceling_msg=0x7f060070;
public static final int account_setup_check_settings_check_incoming_msg=0x7f06006d;
public static final int account_setup_check_settings_check_outgoing_msg=0x7f06006e;
public static final int account_setup_check_settings_finishing_msg=0x7f06006f;
public static final int account_setup_check_settings_retr_info_msg=0x7f06006c;
public static final int account_setup_check_settings_title=0x7f06006b;
public static final int account_setup_failed_dlg_auth_message_fmt=0x7f0600a8;
/** Username or password incorrect\n(ERR01 Account does not exist)
*/
public static final int account_setup_failed_dlg_certificate_message_fmt=0x7f0600a9;
/** Cannot connect to server\n(Connection timed out)
*/
public static final int account_setup_failed_dlg_edit_details_action=0x7f0600ab;
/** Cannot safely connect to server\n(Invalid certificate)
*/
public static final int account_setup_failed_dlg_server_message_fmt=0x7f0600aa;
public static final int account_setup_failed_dlg_title=0x7f0600a7;
public static final int account_setup_finished_toast=0x7f060075;
public static final int account_setup_incoming_delete_policy_7days_label=0x7f060088;
public static final int account_setup_incoming_delete_policy_delete_label=0x7f060089;
public static final int account_setup_incoming_delete_policy_label=0x7f060086;
public static final int account_setup_incoming_delete_policy_never_label=0x7f060087;
public static final int account_setup_incoming_imap_path_prefix_hint=0x7f06008b;
public static final int account_setup_incoming_imap_path_prefix_label=0x7f06008a;
public static final int account_setup_incoming_imap_server_label=0x7f06007e;
public static final int account_setup_incoming_password_label=0x7f06007c;
public static final int account_setup_incoming_pop_server_label=0x7f06007d;
public static final int account_setup_incoming_port_label=0x7f06007f;
public static final int account_setup_incoming_security_label=0x7f060080;
public static final int account_setup_incoming_security_none_label=0x7f060081;
public static final int account_setup_incoming_security_ssl_label=0x7f060083;
public static final int account_setup_incoming_security_ssl_optional_label=0x7f060082;
public static final int account_setup_incoming_security_tls_label=0x7f060085;
public static final int account_setup_incoming_security_tls_optional_label=0x7f060084;
public static final int account_setup_incoming_title=0x7f06007a;
public static final int account_setup_incoming_username_label=0x7f06007b;
public static final int account_setup_names_account_name_label=0x7f060073;
public static final int account_setup_names_instructions=0x7f060072;
public static final int account_setup_names_title=0x7f060071;
public static final int account_setup_names_user_name_label=0x7f060074;
public static final int account_setup_options_default_label=0x7f0600a5;
public static final int account_setup_options_mail_check_frequency_10min=0x7f0600a1;
public static final int account_setup_options_mail_check_frequency_15min=0x7f0600a2;
public static final int account_setup_options_mail_check_frequency_1hour=0x7f0600a4;
public static final int account_setup_options_mail_check_frequency_30min=0x7f0600a3;
public static final int account_setup_options_mail_check_frequency_5min=0x7f0600a0;
public static final int account_setup_options_mail_check_frequency_label=0x7f06009e;
/** Frequency also used in account_settings_*
*/
public static final int account_setup_options_mail_check_frequency_never=0x7f06009f;
public static final int account_setup_options_notify_label=0x7f0600a6;
public static final int account_setup_options_title=0x7f06009d;
public static final int account_setup_outgoing_authentication_basic_label=0x7f060098;
public static final int account_setup_outgoing_authentication_basic_password_label=0x7f06009a;
public static final int account_setup_outgoing_authentication_basic_username_label=0x7f060099;
public static final int account_setup_outgoing_authentication_imap_before_smtp_label=0x7f06009c;
/** The authentication strings below are for a planned (hopefully) change to the above username and password options
*/
public static final int account_setup_outgoing_authentication_label=0x7f060097;
public static final int account_setup_outgoing_authentication_pop_before_smtp_label=0x7f06009b;
public static final int account_setup_outgoing_password_label=0x7f060096;
public static final int account_setup_outgoing_port_label=0x7f06008e;
public static final int account_setup_outgoing_require_login_label=0x7f060094;
public static final int account_setup_outgoing_security_label=0x7f06008f;
public static final int account_setup_outgoing_security_none_label=0x7f060090;
public static final int account_setup_outgoing_security_ssl_label=0x7f060091;
public static final int account_setup_outgoing_security_tls_label=0x7f060093;
public static final int account_setup_outgoing_security_tls_optional_label=0x7f060092;
public static final int account_setup_outgoing_smtp_server_label=0x7f06008d;
public static final int account_setup_outgoing_title=0x7f06008c;
public static final int account_setup_outgoing_username_label=0x7f060095;
public static final int accounts_action=0x7f06001d;
public static final int accounts_context_menu_title=0x7f060029;
public static final int accounts_title=0x7f060004;
public static final int accounts_welcome=0x7f06003b;
public static final int add_account_action=0x7f060016;
public static final int add_attachment_action=0x7f060026;
public static final int add_cc_bcc_action=0x7f060024;
public static final int app_name=0x7f060003;
public static final int build_number=0x7f060000;
/** User to confirm acceptance of dialog boxes, warnings, errors, etc.
*/
public static final int cancel_action=0x7f060009;
public static final int combined_inbox_label=0x7f060041;
public static final int combined_inbox_list_title=0x7f060042;
public static final int combined_inbox_title=0x7f060040;
public static final int compose_action=0x7f060017;
public static final int compose_title=0x7f060005;
public static final int continue_action=0x7f06000f;
public static final int debug_enable_debug_logging_label=0x7f06003d;
public static final int debug_enable_sensitive_logging_label=0x7f06003e;
public static final int debug_title=0x7f060006;
public static final int debug_version_fmt=0x7f06003c;
public static final int delete_action=0x7f06000d;
public static final int discard_action=0x7f060012;
public static final int done_action=0x7f060010;
public static final int dump_settings_action=0x7f060027;
public static final int edit_subject_action=0x7f060025;
public static final int empty_trash_action=0x7f060028;
public static final int folders_action=0x7f060022;
public static final int forward_action=0x7f06000e;
public static final int general_no_subject=0x7f06002a;
public static final int mailbox_select_dlg_new_mailbox_action=0x7f06005b;
public static final int mailbox_select_dlg_title=0x7f06005a;
public static final int mark_as_read_action=0x7f06001f;
public static final int mark_as_unread_action=0x7f060020;
public static final int message_compose_attachments_skipped_toast=0x7f06004e;
public static final int message_compose_bcc_hint=0x7f060047;
public static final int message_compose_cc_hint=0x7f060046;
public static final int message_compose_downloading_attachments_toast=0x7f06004d;
public static final int message_compose_error_no_recipients=0x7f06004c;
public static final int message_compose_fwd_header_fmt=0x7f060049;
public static final int message_compose_quoted_text_label=0x7f06004b;
public static final int message_compose_reply_header_fmt=0x7f06004a;
public static final int message_compose_subject_hint=0x7f060048;
public static final int message_compose_to_hint=0x7f060045;
public static final int message_copied_toast=0x7f06005d;
public static final int message_deleted_toast=0x7f06005f;
public static final int message_discarded_toast=0x7f060060;
public static final int message_header_mua=0x7f06003f;
/** Inbox (12)
*/
public static final int message_list_load_more_messages_action=0x7f060044;
/** Inbox here should be the same as mailbox_name_inbox
*/
public static final int message_list_title_fmt=0x7f060043;
public static final int message_moved_toast=0x7f06005e;
public static final int message_saved_toast=0x7f060061;
public static final int message_view_attachment_download_action=0x7f060051;
public static final int message_view_attachment_view_action=0x7f060050;
public static final int message_view_datetime_fmt=0x7f060054;
public static final int message_view_fetching_attachment_toast=0x7f060059;
public static final int message_view_next_action=0x7f060053;
public static final int message_view_prev_action=0x7f060052;
public static final int message_view_show_pictures_action=0x7f060058;
public static final int message_view_show_pictures_instructions=0x7f060057;
public static final int message_view_status_attachment_not_saved=0x7f060056;
public static final int message_view_status_attachment_saved=0x7f060055;
public static final int message_view_to_label=0x7f06004f;
public static final int move_to_action=0x7f060021;
public static final int new_mailbox_dlg_title=0x7f06005c;
/** Actions will be used as buttons and in menu items
*/
public static final int next_action=0x7f060007;
/** 279 Unread (someone@google.com)
*/
public static final int notification_new_multi_account_fmt=0x7f060034;
public static final int notification_new_one_account_fmt=0x7f060033;
public static final int notification_new_scrolling=0x7f060032;
public static final int notification_new_title=0x7f060031;
public static final int notification_unsent_title=0x7f060035;
/** Used as part of a multi-step process
*/
public static final int okay_action=0x7f060008;
public static final int open_action=0x7f06001a;
public static final int preferences_action=0x7f060019;
public static final int provider_note_live=0x7f0600ca;
public static final int provider_note_yahoo=0x7f0600c9;
public static final int read_action=0x7f06001e;
public static final int read_attachment_desc=0x7f060002;
public static final int read_attachment_label=0x7f060001;
public static final int refresh_action=0x7f060015;
public static final int remove_account_action=0x7f06001c;
/** Used to complete a multi-step process
*/
public static final int remove_action=0x7f060011;
public static final int reply_action=0x7f06000b;
public static final int reply_all_action=0x7f06000c;
public static final int retry_action=0x7f060014;
public static final int save_draft_action=0x7f060013;
public static final int search_action=0x7f060018;
public static final int send_action=0x7f06000a;
/** The following mailbox names will be used if the user has not specified one from the server
*/
public static final int special_mailbox_name_drafts=0x7f060038;
public static final int special_mailbox_name_inbox=0x7f060036;
public static final int special_mailbox_name_outbox=0x7f060037;
public static final int special_mailbox_name_sent=0x7f06003a;
public static final int special_mailbox_name_trash=0x7f060039;
public static final int status_error=0x7f06002e;
/** Shown in place of the subject when a message has no subject. Showing this in parentheses is customary.
*/
public static final int status_loading=0x7f06002b;
public static final int status_loading_more=0x7f06002c;
/** Used in Outbox when a message is currently sending
*/
public static final int status_loading_more_failed=0x7f060030;
public static final int status_network_error=0x7f06002d;
/** Used in Outbox when a message has failed to send
*/
public static final int status_sending=0x7f06002f;
public static final int view_hide_details_action=0x7f060023;
}
public static final class xml {
public static final int account_settings_preferences=0x7f040000;
public static final int providers=0x7f040001;
}
}

View File

@ -0,0 +1,176 @@
package com.fsck.k9;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import com.fsck.k9.codec.binary.Base64;
import android.text.Editable;
import android.widget.TextView;
public class Utility {
public final static String readInputStream(InputStream in, String encoding) throws IOException {
InputStreamReader reader = new InputStreamReader(in, encoding);
StringBuffer sb = new StringBuffer();
int count;
char[] buf = new char[512];
while ((count = reader.read(buf)) != -1) {
sb.append(buf, 0, count);
}
return sb.toString();
}
public final static boolean arrayContains(Object[] a, Object o) {
for (int i = 0, count = a.length; i < count; i++) {
if (a[i].equals(o)) {
return true;
}
}
return false;
}
/**
* Combines the given array of Objects into a single string using the
* seperator character and each Object's toString() method. between each
* part.
*
* @param parts
* @param seperator
* @return
*/
public static String combine(Object[] parts, char seperator) {
if (parts == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < parts.length; i++) {
sb.append(parts[i].toString());
if (i < parts.length - 1) {
sb.append(seperator);
}
}
return sb.toString();
}
public static String base64Decode(String encoded) {
if (encoded == null) {
return null;
}
byte[] decoded = new Base64().decode(encoded.getBytes());
return new String(decoded);
}
public static String base64Encode(String s) {
if (s == null) {
return s;
}
byte[] encoded = new Base64().encode(s.getBytes());
return new String(encoded);
}
public static boolean requiredFieldValid(TextView view) {
return view.getText() != null && view.getText().length() > 0;
}
public static boolean requiredFieldValid(Editable s) {
return s != null && s.length() > 0;
}
/**
* Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
* double quote character to start and end if it's not already there.
* sample -> "sample"
* "sample" -> "sample"
* ""sample"" -> "sample"
* "sample"" -> "sample"
* sa"mp"le -> "sa"mp"le"
* "sa"mp"le" -> "sa"mp"le"
* (empty string) -> ""
* " -> ""
* @param s
* @return
*/
public static String quoteString(String s) {
if (s == null) {
return null;
}
if (!s.matches("^\".*\"$")) {
return "\"" + s + "\"";
}
else {
return s;
}
}
/**
* A fast version of URLDecoder.decode() that works only with UTF-8 and does only two
* allocations. This version is around 3x as fast as the standard one and I'm using it
* hundreds of times in places that slow down the UI, so it helps.
*/
public static String fastUrlDecode(String s) {
try {
byte[] bytes = s.getBytes("UTF-8");
byte ch;
int length = 0;
for (int i = 0, count = bytes.length; i < count; i++) {
ch = bytes[i];
if (ch == '%') {
int h = (bytes[i + 1] - '0');
int l = (bytes[i + 2] - '0');
if (h > 9) {
h -= 7;
}
if (l > 9) {
l -= 7;
}
bytes[length] = (byte) ((h << 4) | l);
i += 2;
}
else if (ch == '+') {
bytes[length] = ' ';
}
else {
bytes[length] = bytes[i];
}
length++;
}
return new String(bytes, 0, length, "UTF-8");
}
catch (UnsupportedEncodingException uee) {
return null;
}
}
/**
* Returns true if the specified date is within today. Returns false otherwise.
* @param date
* @return
*/
public static boolean isDateToday(Date date) {
// TODO But Calendar is so slowwwwwww....
Date today = new Date();
if (date.getYear() == today.getYear() &&
date.getMonth() == today.getMonth() &&
date.getDate() == today.getDate()) {
return true;
}
return false;
}
/*
* TODO disabled this method globally. It is used in all the settings screens but I just
* noticed that an unrelated icon was dimmed. Android must share drawables internally.
*/
public static void setCompoundDrawablesAlpha(TextView view, int alpha) {
// Drawable[] drawables = view.getCompoundDrawables();
// for (Drawable drawable : drawables) {
// if (drawable != null) {
// drawable.setAlpha(alpha);
// }
// }
}
}

View File

@ -0,0 +1,296 @@
package com.fsck.k9.activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.MessagingController;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.AccountSetupBasics;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
public class Accounts extends ListActivity implements OnItemClickListener, OnClickListener {
private static final int DIALOG_REMOVE_ACCOUNT = 1;
/**
* Key codes used to open a debug settings screen.
*/
private static int[] secretKeyCodes = {
KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_G
};
private int mSecretKeyCodeIndex = 0;
private Account mSelectedContextAccount;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.accounts);
ListView listView = getListView();
listView.setOnItemClickListener(this);
listView.setItemsCanFocus(false);
listView.setEmptyView(findViewById(R.id.empty));
findViewById(R.id.add_new_account).setOnClickListener(this);
registerForContextMenu(listView);
if (icicle != null && icicle.containsKey("selectedContextAccount")) {
mSelectedContextAccount = (Account) icicle.getSerializable("selectedContextAccount");
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mSelectedContextAccount != null) {
outState.putSerializable("selectedContextAccount", mSelectedContextAccount);
}
}
@Override
public void onResume() {
super.onResume();
NotificationManager notifMgr = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
notifMgr.cancel(1);
refresh();
}
private void refresh() {
Account[] accounts = Preferences.getPreferences(this).getAccounts();
getListView().setAdapter(new AccountsAdapter(accounts));
}
private void onAddNewAccount() {
AccountSetupBasics.actionNewAccount(this);
}
private void onEditAccount(Account account) {
AccountSettings.actionSettings(this, account);
}
private void onRefresh() {
MessagingController.getInstance(getApplication()).checkMail(this, null, null);
}
private void onCompose() {
Account defaultAccount =
Preferences.getPreferences(this).getDefaultAccount();
if (defaultAccount != null) {
MessageCompose.actionCompose(this, defaultAccount);
}
else {
onAddNewAccount();
}
}
private void onOpenAccount(Account account) {
FolderMessageList.actionHandleAccount(this, account);
}
public void onClick(View view) {
if (view.getId() == R.id.add_new_account) {
onAddNewAccount();
}
}
private void onDeleteAccount(Account account) {
mSelectedContextAccount = account;
showDialog(DIALOG_REMOVE_ACCOUNT);
}
@Override
public Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_REMOVE_ACCOUNT:
return createRemoveAccountDialog();
}
return super.onCreateDialog(id);
}
private Dialog createRemoveAccountDialog() {
return new AlertDialog.Builder(this)
.setTitle(R.string.account_delete_dlg_title)
.setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
mSelectedContextAccount.getDescription()))
.setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dismissDialog(DIALOG_REMOVE_ACCOUNT);
try {
((LocalStore)Store.getInstance(
mSelectedContextAccount.getLocalStoreUri(),
getApplication())).delete();
} catch (Exception e) {
// Ignore
}
mSelectedContextAccount.delete(Preferences.getPreferences(Accounts.this));
k9.setServicesEnabled(Accounts.this);
refresh();
}
})
.setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dismissDialog(DIALOG_REMOVE_ACCOUNT);
}
})
.create();
}
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo)item.getMenuInfo();
Account account = (Account)getListView().getItemAtPosition(menuInfo.position);
switch (item.getItemId()) {
case R.id.delete_account:
onDeleteAccount(account);
break;
case R.id.edit_account:
onEditAccount(account);
break;
case R.id.open:
onOpenAccount(account);
break;
}
return true;
}
public void onItemClick(AdapterView parent, View view, int position, long id) {
Account account = (Account)parent.getItemAtPosition(position);
onOpenAccount(account);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.add_new_account:
onAddNewAccount();
break;
case R.id.check_mail:
onRefresh();
break;
case R.id.compose:
onCompose();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.accounts_option, menu);
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
menu.setHeaderTitle(R.string.accounts_context_menu_title);
getMenuInflater().inflate(R.menu.accounts_context, menu);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == secretKeyCodes[mSecretKeyCodeIndex]) {
mSecretKeyCodeIndex++;
if (mSecretKeyCodeIndex == secretKeyCodes.length) {
mSecretKeyCodeIndex = 0;
startActivity(new Intent(this, Debug.class));
}
} else {
mSecretKeyCodeIndex = 0;
}
return super.onKeyDown(keyCode, event);
}
class AccountsAdapter extends ArrayAdapter<Account> {
public AccountsAdapter(Account[] accounts) {
super(Accounts.this, 0, accounts);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Account account = getItem(position);
View view;
if (convertView != null) {
view = convertView;
}
else {
view = getLayoutInflater().inflate(R.layout.accounts_item, parent, false);
}
AccountViewHolder holder = (AccountViewHolder) view.getTag();
if (holder == null) {
holder = new AccountViewHolder();
holder.description = (TextView) view.findViewById(R.id.description);
holder.email = (TextView) view.findViewById(R.id.email);
holder.newMessageCount = (TextView) view.findViewById(R.id.new_message_count);
view.setTag(holder);
}
holder.description.setText(account.getDescription());
holder.email.setText(account.getEmail());
if (account.getEmail().equals(account.getDescription())) {
holder.email.setVisibility(View.GONE);
}
int unreadMessageCount = 0;
try {
LocalStore localStore = (LocalStore) Store.getInstance(
account.getLocalStoreUri(),
getApplication());
LocalFolder localFolder = (LocalFolder) localStore.getFolder(k9.INBOX);
if (localFolder.exists()) {
unreadMessageCount = localFolder.getUnreadMessageCount();
}
}
catch (MessagingException me) {
/*
* This is not expected to fail under normal circumstances.
*/
throw new RuntimeException("Unable to get unread count from local store.", me);
}
holder.newMessageCount.setText(Integer.toString(unreadMessageCount));
holder.newMessageCount.setVisibility(unreadMessageCount > 0 ? View.VISIBLE : View.GONE);
return view;
}
class AccountViewHolder {
public TextView description;
public TextView email;
public TextView newMessageCount;
}
}
}

View File

@ -0,0 +1,73 @@
package com.fsck.k9.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.k9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
public class Debug extends Activity implements OnCheckedChangeListener {
private TextView mVersionView;
private CheckBox mEnableDebugLoggingView;
private CheckBox mEnableSensitiveLoggingView;
private Preferences mPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.debug);
mPreferences = Preferences.getPreferences(this);
mVersionView = (TextView)findViewById(R.id.version);
mEnableDebugLoggingView = (CheckBox)findViewById(R.id.debug_logging);
mEnableSensitiveLoggingView = (CheckBox)findViewById(R.id.sensitive_logging);
mEnableDebugLoggingView.setOnCheckedChangeListener(this);
mEnableSensitiveLoggingView.setOnCheckedChangeListener(this);
mVersionView.setText(String.format(getString(R.string.debug_version_fmt).toString(),
getString(R.string.build_number)));
mEnableDebugLoggingView.setChecked(k9.DEBUG);
mEnableSensitiveLoggingView.setChecked(k9.DEBUG_SENSITIVE);
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.debug_logging) {
k9.DEBUG = isChecked;
mPreferences.setEnableDebugLogging(k9.DEBUG);
} else if (buttonView.getId() == R.id.sensitive_logging) {
k9.DEBUG_SENSITIVE = isChecked;
mPreferences.setEnableSensitiveLogging(k9.DEBUG_SENSITIVE);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.dump_settings) {
Preferences.getPreferences(this).dump();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.debug_option, menu);
return true;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,891 @@
package com.fsck.k9.activity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Matcher;
import org.apache.commons.io.IOUtils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Process;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.util.Regex;
import android.text.util.Linkify;
import android.util.Config;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.webkit.CacheManager;
import android.webkit.UrlInterceptHandler;
import android.webkit.WebView;
import android.webkit.CacheManager.CacheResult;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.MessagingController;
import com.fsck.k9.MessagingListener;
import com.fsck.k9.R;
import com.fsck.k9.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody;
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.provider.AttachmentProvider;
public class MessageView extends Activity
implements UrlInterceptHandler, OnClickListener {
private static final String EXTRA_ACCOUNT = "com.fsck.k9.MessageView_account";
private static final String EXTRA_FOLDER = "com.fsck.k9.MessageView_folder";
private static final String EXTRA_MESSAGE = "com.fsck.k9.MessageView_message";
private static final String EXTRA_FOLDER_UIDS = "com.fsck.k9.MessageView_folderUids";
private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next";
private TextView mFromView;
private TextView mDateView;
private TextView mToView;
private TextView mSubjectView;
private WebView mMessageContentView;
private LinearLayout mAttachments;
private View mAttachmentIcon;
private View mShowPicturesSection;
private Account mAccount;
private String mFolder;
private String mMessageUid;
private ArrayList<String> mFolderUids;
private Message mMessage;
private String mNextMessageUid = null;
private String mPreviousMessageUid = null;
private DateFormat mDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
private DateFormat mTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
private Listener mListener = new Listener();
private MessageViewHandler mHandler = new MessageViewHandler();
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL: { onDelete(); return true;}
case KeyEvent.KEYCODE_F: { onForward(); return true;}
case KeyEvent.KEYCODE_A: { onReplyAll(); return true; }
case KeyEvent.KEYCODE_R: { onReply(); return true; }
case KeyEvent.KEYCODE_J: { onPrevious(); return true; }
case KeyEvent.KEYCODE_K: { onNext(); return true; }
}
return super.onKeyDown(keyCode, event);
}
class MessageViewHandler extends Handler {
private static final int MSG_PROGRESS = 2;
private static final int MSG_ADD_ATTACHMENT = 3;
private static final int MSG_SET_ATTACHMENTS_ENABLED = 4;
private static final int MSG_SET_HEADERS = 5;
private static final int MSG_NETWORK_ERROR = 6;
private static final int MSG_ATTACHMENT_SAVED = 7;
private static final int MSG_ATTACHMENT_NOT_SAVED = 8;
private static final int MSG_SHOW_SHOW_PICTURES = 9;
private static final int MSG_FETCHING_ATTACHMENT = 10;
@Override
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MSG_PROGRESS:
setProgressBarIndeterminateVisibility(msg.arg1 != 0);
break;
case MSG_ADD_ATTACHMENT:
mAttachments.addView((View) msg.obj);
mAttachments.setVisibility(View.VISIBLE);
break;
case MSG_SET_ATTACHMENTS_ENABLED:
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
attachment.viewButton.setEnabled(msg.arg1 == 1);
attachment.downloadButton.setEnabled(msg.arg1 == 1);
}
break;
case MSG_SET_HEADERS:
String[] values = (String[]) msg.obj;
setTitle(values[0]);
mSubjectView.setText(values[0]);
mFromView.setText(values[1]);
mDateView.setText(values[2]);
mToView.setText(values[3]);
mAttachmentIcon.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
break;
case MSG_NETWORK_ERROR:
Toast.makeText(MessageView.this,
R.string.status_network_error, Toast.LENGTH_LONG).show();
break;
case MSG_ATTACHMENT_SAVED:
Toast.makeText(MessageView.this, String.format(
getString(R.string.message_view_status_attachment_saved), msg.obj),
Toast.LENGTH_LONG).show();
break;
case MSG_ATTACHMENT_NOT_SAVED:
Toast.makeText(MessageView.this,
getString(R.string.message_view_status_attachment_not_saved),
Toast.LENGTH_LONG).show();
break;
case MSG_SHOW_SHOW_PICTURES:
mShowPicturesSection.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
break;
case MSG_FETCHING_ATTACHMENT:
Toast.makeText(MessageView.this,
getString(R.string.message_view_fetching_attachment_toast),
Toast.LENGTH_SHORT).show();
break;
default:
super.handleMessage(msg);
}
}
public void progress(boolean progress) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_PROGRESS;
msg.arg1 = progress ? 1 : 0;
sendMessage(msg);
}
public void addAttachment(View attachmentView) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_ADD_ATTACHMENT;
msg.obj = attachmentView;
sendMessage(msg);
}
public void setAttachmentsEnabled(boolean enabled) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_SET_ATTACHMENTS_ENABLED;
msg.arg1 = enabled ? 1 : 0;
sendMessage(msg);
}
public void setHeaders(
String subject,
String from,
String date,
String to,
boolean hasAttachments) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_SET_HEADERS;
msg.arg1 = hasAttachments ? 1 : 0;
msg.obj = new String[] { subject, from, date, to };
sendMessage(msg);
}
public void networkError() {
sendEmptyMessage(MSG_NETWORK_ERROR);
}
public void attachmentSaved(String filename) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_ATTACHMENT_SAVED;
msg.obj = filename;
sendMessage(msg);
}
public void attachmentNotSaved() {
sendEmptyMessage(MSG_ATTACHMENT_NOT_SAVED);
}
public void fetchingAttachment() {
sendEmptyMessage(MSG_FETCHING_ATTACHMENT);
}
public void showShowPictures(boolean show) {
android.os.Message msg = new android.os.Message();
msg.what = MSG_SHOW_SHOW_PICTURES;
msg.arg1 = show ? 1 : 0;
sendMessage(msg);
}
}
class Attachment {
public String name;
public String contentType;
public long size;
public LocalAttachmentBodyPart part;
public Button viewButton;
public Button downloadButton;
public ImageView iconView;
}
public static void actionView(Context context, Account account,
String folder, String messageUid, ArrayList<String> folderUids) {
actionView(context, account, folder, messageUid, folderUids, null);
}
public static void actionView(Context context, Account account,
String folder, String messageUid, ArrayList<String> folderUids, Bundle extras) {
Intent i = new Intent(context, MessageView.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_FOLDER, folder);
i.putExtra(EXTRA_MESSAGE, messageUid);
i.putExtra(EXTRA_FOLDER_UIDS, folderUids);
if (extras != null) {
i.putExtras(extras);
}
context.startActivity(i);
}
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.message_view);
mFromView = (TextView)findViewById(R.id.from);
mToView = (TextView)findViewById(R.id.to);
mSubjectView = (TextView)findViewById(R.id.subject);
mDateView = (TextView)findViewById(R.id.date);
mMessageContentView = (WebView)findViewById(R.id.message_content);
mAttachments = (LinearLayout)findViewById(R.id.attachments);
mAttachmentIcon = findViewById(R.id.attachment);
mShowPicturesSection = findViewById(R.id.show_pictures_section);
mMessageContentView.setVerticalScrollBarEnabled(false);
mAttachments.setVisibility(View.GONE);
mAttachmentIcon.setVisibility(View.GONE);
findViewById(R.id.reply).setOnClickListener(this);
findViewById(R.id.reply_all).setOnClickListener(this);
findViewById(R.id.delete).setOnClickListener(this);
findViewById(R.id.show_pictures).setOnClickListener(this);
// UrlInterceptRegistry.registerHandler(this);
mMessageContentView.getSettings().setBlockNetworkImage(true);
mMessageContentView.getSettings().setSupportZoom(false);
setTitle("");
Intent intent = getIntent();
mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT);
mFolder = intent.getStringExtra(EXTRA_FOLDER);
mMessageUid = intent.getStringExtra(EXTRA_MESSAGE);
mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS);
View next = findViewById(R.id.next);
View previous = findViewById(R.id.previous);
/*
* Next and Previous Message are not shown in landscape mode, so
* we need to check before we use them.
*/
if (next != null && previous != null) {
next.setOnClickListener(this);
previous.setOnClickListener(this);
findSurroundingMessagesUid();
previous.setVisibility(mPreviousMessageUid != null ? View.VISIBLE : View.GONE);
next.setVisibility(mNextMessageUid != null ? View.VISIBLE : View.GONE);
boolean goNext = intent.getBooleanExtra(EXTRA_NEXT, false);
if (goNext) {
next.requestFocus();
}
}
MessagingController.getInstance(getApplication()).addListener(mListener);
new Thread() {
public void run() {
// TODO this is a spot that should be eventually handled by a MessagingController
// thread pool. We want it in a thread but it can't be blocked by the normal
// synchronization stuff in MC.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
MessagingController.getInstance(getApplication()).loadMessageForView(
mAccount,
mFolder,
mMessageUid,
mListener);
}
}.start();
}
private void findSurroundingMessagesUid() {
for (int i = 0, count = mFolderUids.size(); i < count; i++) {
String messageUid = mFolderUids.get(i);
if (messageUid.equals(mMessageUid)) {
if (i != 0) {
mPreviousMessageUid = mFolderUids.get(i - 1);
}
if (i != count - 1) {
mNextMessageUid = mFolderUids.get(i + 1);
}
break;
}
}
}
public void onResume() {
super.onResume();
MessagingController.getInstance(getApplication()).addListener(mListener);
}
public void onPause() {
super.onPause();
MessagingController.getInstance(getApplication()).removeListener(mListener);
}
private void onDelete() {
if (mMessage != null) {
MessagingController.getInstance(getApplication()).deleteMessage(
mAccount,
mFolder,
mMessage,
null);
Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
// Remove this message's Uid locally
mFolderUids.remove(mMessage.getUid());
// Check if we have previous/next messages available before choosing
// which one to display
findSurroundingMessagesUid();
if (mPreviousMessageUid != null) {
onPrevious();
} else if (mNextMessageUid != null) {
onNext();
} else {
finish();
}
}
}
private void onReply() {
if (mMessage != null) {
MessageCompose.actionReply(this, mAccount, mMessage, false);
finish();
}
}
private void onReplyAll() {
if (mMessage != null) {
MessageCompose.actionReply(this, mAccount, mMessage, true);
finish();
}
}
private void onForward() {
if (mMessage != null) {
MessageCompose.actionForward(this, mAccount, mMessage);
finish();
}
}
private void onNext() {
Bundle extras = new Bundle(1);
extras.putBoolean(EXTRA_NEXT, true);
MessageView.actionView(this, mAccount, mFolder, mNextMessageUid, mFolderUids, extras);
finish();
}
private void onPrevious() {
MessageView.actionView(this, mAccount, mFolder, mPreviousMessageUid, mFolderUids);
finish();
}
private void onMarkAsUnread() {
MessagingController.getInstance(getApplication()).markMessageRead(
mAccount,
mFolder,
mMessage.getUid(),
false);
}
/**
* Creates a unique file in the given directory by appending a hyphen
* and a number to the given filename.
* @param directory
* @param filename
* @return
*/
private File createUniqueFile(File directory, String filename) {
File file = new File(directory, filename);
if (!file.exists()) {
return file;
}
// Get the extension of the file, if any.
int index = filename.lastIndexOf('.');
String format;
if (index != -1) {
String name = filename.substring(0, index);
String extension = filename.substring(index);
format = name + "-%d" + extension;
}
else {
format = filename + "-%d";
}
for (int i = 2; i < Integer.MAX_VALUE; i++) {
file = new File(directory, String.format(format, i));
if (!file.exists()) {
return file;
}
}
return null;
}
private void onDownloadAttachment(Attachment attachment) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
/*
* Abort early if there's no place to save the attachment. We don't want to spend
* the time downloading it and then abort.
*/
Toast.makeText(this,
getString(R.string.message_view_status_attachment_not_saved),
Toast.LENGTH_SHORT).show();
return;
}
MessagingController.getInstance(getApplication()).loadAttachment(
mAccount,
mMessage,
attachment.part,
new Object[] { true, attachment },
mListener);
}
private void onViewAttachment(Attachment attachment) {
MessagingController.getInstance(getApplication()).loadAttachment(
mAccount,
mMessage,
attachment.part,
new Object[] { false, attachment },
mListener);
}
private void onShowPictures() {
mMessageContentView.getSettings().setBlockNetworkImage(false);
mShowPicturesSection.setVisibility(View.GONE);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.reply:
onReply();
break;
case R.id.reply_all:
onReplyAll();
break;
case R.id.delete:
onDelete();
break;
case R.id.next:
onNext();
break;
case R.id.previous:
onPrevious();
break;
case R.id.download:
onDownloadAttachment((Attachment) view.getTag());
break;
case R.id.view:
onViewAttachment((Attachment) view.getTag());
break;
case R.id.show_pictures:
onShowPictures();
break;
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.delete:
onDelete();
break;
case R.id.reply:
onReply();
break;
case R.id.reply_all:
onReplyAll();
break;
case R.id.forward:
onForward();
break;
case R.id.mark_as_unread:
onMarkAsUnread();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.message_view_option, menu);
return true;
}
public CacheResult service(String url, Map<String, String> headers) {
String prefix = "http://cid/";
if (url.startsWith(prefix)) {
try {
String contentId = url.substring(prefix.length());
final Part part = MimeUtility.findPartByContentId(mMessage, "<" + contentId + ">");
if (part != null) {
CacheResult cr = new CacheManager.CacheResult();
// TODO looks fixed in Mainline, cr.setInputStream
// part.getBody().writeTo(cr.getStream());
return cr;
}
}
catch (Exception e) {
// TODO
}
}
return null;
}
private Bitmap getPreviewIcon(Attachment attachment) throws MessagingException {
try {
return BitmapFactory.decodeStream(
getContentResolver().openInputStream(
AttachmentProvider.getAttachmentThumbnailUri(mAccount,
attachment.part.getAttachmentId(),
62,
62)));
}
catch (Exception e) {
/*
* We don't care what happened, we just return null for the preview icon.
*/
return null;
}
}
/*
* Formats the given size as a String in bytes, kB, MB or GB with a single digit
* of precision. Ex: 12,315,000 = 12.3 MB
*/
public static String formatSize(float size) {
long kb = 1024;
long mb = (kb * 1024);
long gb = (mb * 1024);
if (size < kb) {
return String.format("%d bytes", (int) size);
}
else if (size < mb) {
return String.format("%.1f kB", size / kb);
}
else if (size < gb) {
return String.format("%.1f MB", size / mb);
}
else {
return String.format("%.1f GB", size / gb);
}
}
private void renderAttachments(Part part, int depth) throws MessagingException {
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
String name = MimeUtility.getHeaderParameter(contentType, "name");
if (name != null) {
/*
* We're guaranteed size because LocalStore.fetch puts it there.
*/
String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
int size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size"));
Attachment attachment = new Attachment();
attachment.size = size;
attachment.contentType = part.getMimeType();
attachment.name = name;
attachment.part = (LocalAttachmentBodyPart) part;
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(R.layout.message_view_attachment, null);
TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
TextView attachmentInfo = (TextView)view.findViewById(R.id.attachment_info);
ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon);
Button attachmentView = (Button)view.findViewById(R.id.view);
Button attachmentDownload = (Button)view.findViewById(R.id.download);
if ((!MimeUtility.mimeTypeMatches(attachment.contentType,
k9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
|| (MimeUtility.mimeTypeMatches(attachment.contentType,
k9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
attachmentView.setVisibility(View.GONE);
}
if ((!MimeUtility.mimeTypeMatches(attachment.contentType,
k9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
|| (MimeUtility.mimeTypeMatches(attachment.contentType,
k9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
attachmentDownload.setVisibility(View.GONE);
}
if (attachment.size > k9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
attachmentView.setVisibility(View.GONE);
attachmentDownload.setVisibility(View.GONE);
}
attachment.viewButton = attachmentView;
attachment.downloadButton = attachmentDownload;
attachment.iconView = attachmentIcon;
view.setTag(attachment);
attachmentView.setOnClickListener(this);
attachmentView.setTag(attachment);
attachmentDownload.setOnClickListener(this);
attachmentDownload.setTag(attachment);
attachmentName.setText(name);
attachmentInfo.setText(formatSize(size));
Bitmap previewIcon = getPreviewIcon(attachment);
if (previewIcon != null) {
attachmentIcon.setImageBitmap(previewIcon);
}
mHandler.addAttachment(view);
}
if (part.getBody() instanceof Multipart) {
Multipart mp = (Multipart)part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
renderAttachments(mp.getBodyPart(i), depth + 1);
}
}
}
class Listener extends MessagingListener {
@Override
public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
final Message message) {
MessageView.this.mMessage = message;
try {
String subjectText = message.getSubject();
String fromText = Address.toFriendly(message.getFrom());
String dateText = Utility.isDateToday(message.getSentDate()) ?
mTimeFormat.format(message.getSentDate()) :
mDateTimeFormat.format(message.getSentDate());
String toText = Address.toFriendly(message.getRecipients(RecipientType.TO));
boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0;
mHandler.setHeaders(subjectText,
fromText,
dateText,
toText,
hasAttachments);
}
catch (MessagingException me) {
if (Config.LOGV) {
Log.v(k9.LOG_TAG, "loadMessageForViewHeadersAvailable", me);
}
}
}
@Override
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
Message message) {
SpannableString markup;
MessageView.this.mMessage = message;
try {
Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain");
}
if (part != null) {
String text = MimeUtility.getTextFromPart(part);
if (part.getMimeType().equalsIgnoreCase("text/html")) {
text = text.replaceAll("cid:", "http://cid/");
} else {
/*
* Convert plain text to HTML by replacing
* \r?\n with <br> and adding a html/body wrapper.
*/
text = text.replaceAll("\r?\n", "<br>");
text = "<html><body>" + text + "</body></html>";
}
/*
* TODO this should be smarter, change to regex for img, but consider how to
* get backgroung images and a million other things that HTML allows.
*/
if (text.contains("<img")) {
mHandler.showShowPictures(true);
}
markup = new SpannableString(text);
Linkify.addLinks(markup, Linkify.ALL);
mMessageContentView.loadDataWithBaseURL("email://", markup.toString(), "text/html",
"utf-8", null);
}
else {
mMessageContentView.loadUrl("file:///android_asset/empty.html");
}
renderAttachments(mMessage, 0);
}
catch (Exception e) {
if (Config.LOGV) {
Log.v(k9.LOG_TAG, "loadMessageForViewBodyAvailable", e);
}
}
}
@Override
public void loadMessageForViewFailed(Account account, String folder, String uid,
final String message) {
mHandler.post(new Runnable() {
public void run() {
setProgressBarIndeterminateVisibility(false);
mHandler.networkError();
mMessageContentView.loadUrl("file:///android_asset/empty.html");
}
});
}
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid,
Message message) {
mHandler.post(new Runnable() {
public void run() {
setProgressBarIndeterminateVisibility(false);
}
});
}
@Override
public void loadMessageForViewStarted(Account account, String folder, String uid) {
mHandler.post(new Runnable() {
public void run() {
mMessageContentView.loadUrl("file:///android_asset/loading.html");
setProgressBarIndeterminateVisibility(true);
}
});
}
@Override
public void loadAttachmentStarted(Account account, Message message,
Part part, Object tag, boolean requiresDownload) {
mHandler.setAttachmentsEnabled(false);
mHandler.progress(true);
if (requiresDownload) {
mHandler.fetchingAttachment();
}
}
@Override
public void loadAttachmentFinished(Account account, Message message,
Part part, Object tag) {
mHandler.setAttachmentsEnabled(true);
mHandler.progress(false);
Object[] params = (Object[]) tag;
boolean download = (Boolean) params[0];
Attachment attachment = (Attachment) params[1];
if (download) {
try {
File file = createUniqueFile(Environment.getExternalStorageDirectory(),
attachment.name);
Uri uri = AttachmentProvider.getAttachmentUri(
mAccount,
attachment.part.getAttachmentId());
InputStream in = getContentResolver().openInputStream(uri);
OutputStream out = new FileOutputStream(file);
IOUtils.copy(in, out);
out.flush();
out.close();
in.close();
mHandler.attachmentSaved(file.getName());
new MediaScannerNotifier(MessageView.this, file);
}
catch (IOException ioe) {
mHandler.attachmentNotSaved();
}
}
else {
Uri uri = AttachmentProvider.getAttachmentUri(
mAccount,
attachment.part.getAttachmentId());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
}
}
@Override
public void loadAttachmentFailed(Account account, Message message, Part part,
Object tag, String reason) {
mHandler.setAttachmentsEnabled(true);
mHandler.progress(false);
mHandler.networkError();
}
}
class MediaScannerNotifier implements MediaScannerConnectionClient {
private MediaScannerConnection mConnection;
private File mFile;
public MediaScannerNotifier(Context context, File file) {
mFile = file;
mConnection = new MediaScannerConnection(context, this);
mConnection.connect();
}
public void onMediaScannerConnected() {
mConnection.scanFile(mFile.getAbsolutePath(), null);
}
public void onScanCompleted(String path, Uri uri) {
try {
if (uri != null) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
startActivity(intent);
}
} finally {
mConnection.disconnect();
}
}
}
}

View File

@ -0,0 +1,36 @@
package com.fsck.k9.activity;
import android.content.Context;
/**
* A listener that the user can register for global, persistent progress events.
*/
public interface ProgressListener {
/**
* @param context
* @param title
* @param message
* @param currentProgress
* @param maxProgress
* @param indeterminate
*/
void showProgress(Context context, String title, String message, long currentProgress,
long maxProgress, boolean indeterminate);
/**
* @param context
* @param title
* @param message
* @param currentProgress
* @param maxProgress
* @param indeterminate
*/
void updateProgress(Context context, String title, String message, long currentProgress,
long maxProgress, boolean indeterminate);
/**
* @param context
*/
void hideProgress(Context context);
}

View File

@ -0,0 +1,36 @@
package com.fsck.k9.activity;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.Preferences;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/**
* The Welcome activity initializes the application and decides what Activity
* the user should start with.
* If no accounts are configured the user is taken to the Accounts Activity where they
* can configure an account.
* If a single account is configured the user is taken directly to the FolderMessageList for
* the INBOX of that account.
* If more than one account is configuref the user is takaen to the Accounts Activity so they
* can select an account.
*/
public class Welcome extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Account[] accounts = Preferences.getPreferences(this).getAccounts();
if (accounts.length == 1) {
FolderMessageList.actionHandleAccount(this, accounts[0], k9.INBOX);
} else {
startActivity(new Intent(this, Accounts.class));
}
finish();
}
}

View File

@ -0,0 +1,170 @@
package com.fsck.k9.activity.setup;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.KeyEvent;
import android.preference.PreferenceActivity;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.RingtonePreference;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
public class AccountSettings extends PreferenceActivity {
private static final String EXTRA_ACCOUNT = "account";
private static final String PREFERENCE_TOP_CATERGORY = "account_settings";
private static final String PREFERENCE_DESCRIPTION = "account_description";
private static final String PREFERENCE_COMPOSITION = "composition";
private static final String PREFERENCE_FREQUENCY = "account_check_frequency";
private static final String PREFERENCE_DEFAULT = "account_default";
private static final String PREFERENCE_NOTIFY = "account_notify";
private static final String PREFERENCE_VIBRATE = "account_vibrate";
private static final String PREFERENCE_RINGTONE = "account_ringtone";
private static final String PREFERENCE_INCOMING = "incoming";
private static final String PREFERENCE_OUTGOING = "outgoing";
private Account mAccount;
private EditTextPreference mAccountDescription;
private ListPreference mCheckFrequency;
private CheckBoxPreference mAccountDefault;
private CheckBoxPreference mAccountNotify;
private CheckBoxPreference mAccountVibrate;
private RingtonePreference mAccountRingtone;
public static void actionSettings(Context context, Account account) {
Intent i = new Intent(context, AccountSettings.class);
i.putExtra(EXTRA_ACCOUNT, account);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
addPreferencesFromResource(R.xml.account_settings_preferences);
Preference category = findPreference(PREFERENCE_TOP_CATERGORY);
category.setTitle(getString(R.string.account_settings_title_fmt));
mAccountDescription = (EditTextPreference) findPreference(PREFERENCE_DESCRIPTION);
mAccountDescription.setSummary(mAccount.getDescription());
mAccountDescription.setText(mAccount.getDescription());
mAccountDescription.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
final String summary = newValue.toString();
mAccountDescription.setSummary(summary);
mAccountDescription.setText(summary);
return false;
}
});
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
mCheckFrequency.setValue(String.valueOf(mAccount.getAutomaticCheckIntervalMinutes()));
mCheckFrequency.setSummary(mCheckFrequency.getEntry());
mCheckFrequency.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference preference, Object newValue) {
final String summary = newValue.toString();
int index = mCheckFrequency.findIndexOfValue(summary);
mCheckFrequency.setSummary(mCheckFrequency.getEntries()[index]);
mCheckFrequency.setValue(summary);
return false;
}
});
mAccountDefault = (CheckBoxPreference) findPreference(PREFERENCE_DEFAULT);
mAccountDefault.setChecked(
mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()));
mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY);
mAccountNotify.setChecked(mAccount.isNotifyNewMail());
mAccountRingtone = (RingtonePreference) findPreference(PREFERENCE_RINGTONE);
// XXX: The following two lines act as a workaround for the RingtonePreference
// which does not let us set/get the value programmatically
SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences();
prefs.edit().putString(PREFERENCE_RINGTONE, mAccount.getRingtone()).commit();
mAccountVibrate = (CheckBoxPreference) findPreference(PREFERENCE_VIBRATE);
mAccountVibrate.setChecked(mAccount.isVibrate());
findPreference(PREFERENCE_COMPOSITION).setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
onCompositionSettings();
return true;
}
});
findPreference(PREFERENCE_INCOMING).setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
onIncomingSettings();
return true;
}
});
findPreference(PREFERENCE_OUTGOING).setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
onOutgoingSettings();
return true;
}
});
}
@Override
public void onResume() {
super.onResume();
mAccount.refresh(Preferences.getPreferences(this));
}
private void saveSettings() {
if (mAccountDefault.isChecked()) {
Preferences.getPreferences(this).setDefaultAccount(mAccount);
}
mAccount.setDescription(mAccountDescription.getText());
mAccount.setNotifyNewMail(mAccountNotify.isChecked());
mAccount.setAutomaticCheckIntervalMinutes(Integer.parseInt(mCheckFrequency.getValue()));
mAccount.setVibrate(mAccountVibrate.isChecked());
SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences();
mAccount.setRingtone(prefs.getString(PREFERENCE_RINGTONE, null));
mAccount.save(Preferences.getPreferences(this));
k9.setServicesEnabled(this);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
saveSettings();
}
return super.onKeyDown(keyCode, event);
}
private void onCompositionSettings() {
AccountSetupComposition.actionEditCompositionSettings(this, mAccount);
}
private void onIncomingSettings() {
AccountSetupIncoming.actionEditIncomingSettings(this, mAccount);
}
private void onOutgoingSettings() {
AccountSetupOutgoing.actionEditOutgoingSettings(this, mAccount);
}
}

View File

@ -0,0 +1,90 @@
package com.fsck.k9.activity.setup;
import java.net.URI;
import java.net.URISyntaxException;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.fsck.k9.Account;
import com.fsck.k9.R;
/**
* Prompts the user to select an account type. The account type, along with the
* passed in email address, password and makeDefault are then passed on to the
* AccountSetupIncoming activity.
*/
public class AccountSetupAccountType extends Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private Account mAccount;
private boolean mMakeDefault;
public static void actionSelectAccountType(Context context, Account account, boolean makeDefault) {
Intent i = new Intent(context, AccountSetupAccountType.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_account_type);
((Button)findViewById(R.id.pop)).setOnClickListener(this);
((Button)findViewById(R.id.imap)).setOnClickListener(this);
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
}
private void onPop() {
try {
URI uri = new URI(mAccount.getStoreUri());
uri = new URI("pop3", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
mAccount.setStoreUri(uri.toString());
} catch (URISyntaxException use) {
/*
* This should not happen.
*/
throw new Error(use);
}
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
finish();
}
private void onImap() {
try {
URI uri = new URI(mAccount.getStoreUri());
uri = new URI("imap", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
mAccount.setStoreUri(uri.toString());
} catch (URISyntaxException use) {
/*
* This should not happen.
*/
throw new Error(use);
}
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
finish();
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.pop:
onPop();
break;
case R.id.imap:
onImap();
break;
}
}
}

View File

@ -0,0 +1,388 @@
package com.fsck.k9.activity.setup;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Contacts;
import android.provider.Contacts.People.ContactMethods;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.EmailAddressValidator;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.Utility;
/**
* Prompts the user for the email address and password. Also prompts for
* "Use this account as default" if this is the 2nd+ account being set up.
* Attempts to lookup default settings for the domain the user specified. If the
* domain is known the settings are handed off to the AccountSetupCheckSettings
* activity. If no settings are found the settings are handed off to the
* AccountSetupAccountType activity.
*/
public class AccountSetupBasics extends Activity
implements OnClickListener, TextWatcher {
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
private final static int DIALOG_NOTE = 1;
private final static String STATE_KEY_PROVIDER =
"com.fsck.k9.AccountSetupBasics.provider";
private Preferences mPrefs;
private EditText mEmailView;
private EditText mPasswordView;
private CheckBox mDefaultView;
private Button mNextButton;
private Button mManualSetupButton;
private Account mAccount;
private Provider mProvider;
private EmailAddressValidator mEmailValidator = new EmailAddressValidator();
public static void actionNewAccount(Context context) {
Intent i = new Intent(context, AccountSetupBasics.class);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_basics);
mPrefs = Preferences.getPreferences(this);
mEmailView = (EditText)findViewById(R.id.account_email);
mPasswordView = (EditText)findViewById(R.id.account_password);
mDefaultView = (CheckBox)findViewById(R.id.account_default);
mNextButton = (Button)findViewById(R.id.next);
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
mNextButton.setOnClickListener(this);
mManualSetupButton.setOnClickListener(this);
mEmailView.addTextChangedListener(this);
mPasswordView.addTextChangedListener(this);
if (mPrefs.getAccounts().length > 0) {
mDefaultView.setVisibility(View.VISIBLE);
}
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
}
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_KEY_PROVIDER)) {
mProvider = (Provider)savedInstanceState.getSerializable(STATE_KEY_PROVIDER);
}
}
@Override
public void onResume() {
super.onResume();
validateFields();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
if (mProvider != null) {
outState.putSerializable(STATE_KEY_PROVIDER, mProvider);
}
}
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
private void validateFields() {
boolean valid = Utility.requiredFieldValid(mEmailView)
&& Utility.requiredFieldValid(mPasswordView)
&& mEmailValidator.isValid(mEmailView.getText().toString());
mNextButton.setEnabled(valid);
mManualSetupButton.setEnabled(valid);
/*
* Dim the next button's icon to 50% if the button is disabled.
* TODO this can probably be done with a stateful drawable. Check into it.
* android:state_enabled
*/
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
}
private String getOwnerName() {
String name = null;
String projection[] = {
ContactMethods.NAME
};
Cursor c = getContentResolver().query(
Uri.withAppendedPath(Contacts.People.CONTENT_URI, "owner"), projection, null, null,
null);
if (c.getCount() > 0) {
c.moveToFirst();
name = c.getString(0);
c.close();
}
if (name == null || name.length() == 0) {
Account account = Preferences.getPreferences(this).getDefaultAccount();
if (account != null) {
name = account.getName();
}
}
return name;
}
@Override
public Dialog onCreateDialog(int id) {
if (id == DIALOG_NOTE) {
if (mProvider != null && mProvider.note != null) {
return new AlertDialog.Builder(this)
.setMessage(mProvider.note)
.setPositiveButton(
getString(R.string.okay_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finishAutoSetup();
}
})
.setNegativeButton(
getString(R.string.cancel_action),
null)
.create();
}
}
return null;
}
private void finishAutoSetup() {
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
String[] emailParts = email.split("@");
String user = emailParts[0];
String domain = emailParts[1];
URI incomingUri = null;
URI outgoingUri = null;
try {
String incomingUsername = mProvider.incomingUsernameTemplate;
incomingUsername = incomingUsername.replaceAll("\\$email", email);
incomingUsername = incomingUsername.replaceAll("\\$user", user);
incomingUsername = incomingUsername.replaceAll("\\$domain", domain);
URI incomingUriTemplate = mProvider.incomingUriTemplate;
incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":"
+ password, incomingUriTemplate.getHost(), incomingUriTemplate.getPort(), null,
null, null);
String outgoingUsername = mProvider.outgoingUsernameTemplate;
outgoingUsername = outgoingUsername.replaceAll("\\$email", email);
outgoingUsername = outgoingUsername.replaceAll("\\$user", user);
outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain);
URI outgoingUriTemplate = mProvider.outgoingUriTemplate;
outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":"
+ password, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null,
null, null);
} catch (URISyntaxException use) {
/*
* If there is some problem with the URI we give up and go on to
* manual setup.
*/
onManualSetup();
return;
}
mAccount = new Account(this);
mAccount.setName(getOwnerName());
mAccount.setEmail(email);
mAccount.setStoreUri(incomingUri.toString());
mAccount.setTransportUri(outgoingUri.toString());
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
if (incomingUri.toString().startsWith("imap")) {
mAccount.setDeletePolicy(Account.DELETE_POLICY_ON_DELETE);
}
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true);
}
private void onNext() {
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
String[] emailParts = email.split("@");
String user = emailParts[0];
String domain = emailParts[1];
mProvider = findProviderForDomain(domain);
if (mProvider == null) {
/*
* We don't have default settings for this account, start the manual
* setup process.
*/
onManualSetup();
return;
}
if (mProvider.note != null) {
showDialog(DIALOG_NOTE);
}
else {
finishAutoSetup();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
mAccount.setDescription(mAccount.getEmail());
mAccount.save(Preferences.getPreferences(this));
if (mDefaultView.isChecked()) {
Preferences.getPreferences(this).setDefaultAccount(mAccount);
}
k9.setServicesEnabled(this);
AccountSetupNames.actionSetNames(this, mAccount);
finish();
}
}
private void onManualSetup() {
String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
String[] emailParts = email.split("@");
String user = emailParts[0];
String domain = emailParts[1];
mAccount = new Account(this);
mAccount.setName(getOwnerName());
mAccount.setEmail(email);
try {
URI uri = new URI("placeholder", user + ":" + password, "mail." + domain, -1, null,
null, null);
mAccount.setStoreUri(uri.toString());
mAccount.setTransportUri(uri.toString());
} catch (URISyntaxException use) {
/*
* If we can't set up the URL we just continue. It's only for
* convenience.
*/
}
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
AccountSetupAccountType.actionSelectAccountType(this, mAccount, mDefaultView.isChecked());
finish();
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.next:
onNext();
break;
case R.id.manual_setup:
onManualSetup();
break;
}
}
/**
* Attempts to get the given attribute as a String resource first, and if it fails
* returns the attribute as a simple String value.
* @param xml
* @param name
* @return
*/
private String getXmlAttribute(XmlResourceParser xml, String name) {
int resId = xml.getAttributeResourceValue(null, name, 0);
if (resId == 0) {
return xml.getAttributeValue(null, name);
}
else {
return getString(resId);
}
}
private Provider findProviderForDomain(String domain) {
try {
XmlResourceParser xml = getResources().getXml(R.xml.providers);
int xmlEventType;
Provider provider = null;
while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
if (xmlEventType == XmlResourceParser.START_TAG
&& "provider".equals(xml.getName())
&& domain.equalsIgnoreCase(getXmlAttribute(xml, "domain"))) {
provider = new Provider();
provider.id = getXmlAttribute(xml, "id");
provider.label = getXmlAttribute(xml, "label");
provider.domain = getXmlAttribute(xml, "domain");
provider.note = getXmlAttribute(xml, "note");
}
else if (xmlEventType == XmlResourceParser.START_TAG
&& "incoming".equals(xml.getName())
&& provider != null) {
provider.incomingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
provider.incomingUsernameTemplate = getXmlAttribute(xml, "username");
}
else if (xmlEventType == XmlResourceParser.START_TAG
&& "outgoing".equals(xml.getName())
&& provider != null) {
provider.outgoingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
provider.outgoingUsernameTemplate = getXmlAttribute(xml, "username");
}
else if (xmlEventType == XmlResourceParser.END_TAG
&& "provider".equals(xml.getName())
&& provider != null) {
return provider;
}
}
}
catch (Exception e) {
Log.e(k9.LOG_TAG, "Error while trying to load provider settings.", e);
}
return null;
}
static class Provider implements Serializable {
private static final long serialVersionUID = 8511656164616538989L;
public String id;
public String label;
public String domain;
public URI incomingUriTemplate;
public String incomingUsernameTemplate;
public URI outgoingUriTemplate;
public String outgoingUsernameTemplate;
public String note;
}
}

View File

@ -0,0 +1,188 @@
package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Process;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.fsck.k9.Account;
import com.fsck.k9.R;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.CertificateValidationException;
/**
* Checks the given settings to make sure that they can be used to send and
* receive mail.
*
* XXX NOTE: The manifest for this app has it ignore config changes, because
* it doesn't correctly deal with restarting while its thread is running.
*/
public class AccountSetupCheckSettings extends Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_CHECK_INCOMING = "checkIncoming";
private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing";
private Handler mHandler = new Handler();
private ProgressBar mProgressBar;
private TextView mMessageView;
private Account mAccount;
private boolean mCheckIncoming;
private boolean mCheckOutgoing;
private boolean mCanceled;
private boolean mDestroyed;
public static void actionCheckSettings(Activity context, Account account,
boolean checkIncoming, boolean checkOutgoing) {
Intent i = new Intent(context, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming);
i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing);
context.startActivityForResult(i, 1);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_check_settings);
mMessageView = (TextView)findViewById(R.id.message);
mProgressBar = (ProgressBar)findViewById(R.id.progress);
((Button)findViewById(R.id.cancel)).setOnClickListener(this);
setMessage(R.string.account_setup_check_settings_retr_info_msg);
mProgressBar.setIndeterminate(true);
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
mCheckIncoming = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_INCOMING, false);
mCheckOutgoing = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_OUTGOING, false);
new Thread() {
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
if (mDestroyed) {
return;
}
if (mCanceled) {
finish();
return;
}
if (mCheckIncoming) {
setMessage(R.string.account_setup_check_settings_check_incoming_msg);
Store store = Store.getInstance(mAccount.getStoreUri(), getApplication());
store.checkSettings();
}
if (mDestroyed) {
return;
}
if (mCanceled) {
finish();
return;
}
if (mCheckOutgoing) {
setMessage(R.string.account_setup_check_settings_check_outgoing_msg);
Transport transport = Transport.getInstance(mAccount.getTransportUri());
transport.close();
transport.open();
transport.close();
}
if (mDestroyed) {
return;
}
if (mCanceled) {
finish();
return;
}
setResult(RESULT_OK);
finish();
} catch (final AuthenticationFailedException afe) {
showErrorDialog(
R.string.account_setup_failed_dlg_auth_message_fmt,
afe.getMessage() == null ? "" : afe.getMessage());
} catch (final CertificateValidationException cve) {
showErrorDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
cve.getMessage() == null ? "" : cve.getMessage());
} catch (final MessagingException me) {
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
me.getMessage() == null ? "" : me.getMessage());
}
}
}.start();
}
@Override
public void onDestroy() {
super.onDestroy();
mDestroyed = true;
mCanceled = true;
}
private void setMessage(final int resId) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
mMessageView.setText(getString(resId));
}
});
}
private void showErrorDialog(final int msgResId, final Object... args) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
mProgressBar.setIndeterminate(false);
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_title))
.setMessage(getString(msgResId, args))
.setCancelable(true)
.setPositiveButton(
getString(R.string.account_setup_failed_dlg_edit_details_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
});
}
private void onCancel() {
mCanceled = true;
setMessage(R.string.account_setup_check_settings_canceling_msg);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.cancel:
onCancel();
break;
}
}
}

View File

@ -0,0 +1,108 @@
package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.KeyEvent;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.TextView;
import com.fsck.k9.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.k9;
import com.fsck.k9.Utility;
public class AccountSetupComposition extends Activity {
private static final String EXTRA_ACCOUNT = "account";
private Account mAccount;
private EditText mAccountSignature;
private EditText mAccountEmail;
private EditText mAccountAlwaysBcc;
private EditText mAccountName;
public static void actionEditCompositionSettings(Activity context, Account account) {
Intent i = new Intent(context, AccountSetupComposition.class);
i.setAction(Intent.ACTION_EDIT);
i.putExtra(EXTRA_ACCOUNT, account);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
setContentView(R.layout.account_setup_composition);
/*
* If we're being reloaded we override the original account with the one
* we saved
*/
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
}
mAccountName = (EditText)findViewById(R.id.account_name);
mAccountName.setText(mAccount.getName());
mAccountEmail = (EditText)findViewById(R.id.account_email);
mAccountEmail.setText(mAccount.getEmail());
mAccountAlwaysBcc = (EditText)findViewById(R.id.account_always_bcc);
mAccountAlwaysBcc.setText(mAccount.getAlwaysBcc());
mAccountSignature = (EditText)findViewById(R.id.account_signature);
mAccountSignature.setText(mAccount.getSignature());
}
@Override
public void onResume() {
super.onResume();
mAccount.refresh(Preferences.getPreferences(this));
}
private void saveSettings() {
mAccount.setEmail(mAccountEmail.getText().toString());
mAccount.setAlwaysBcc(mAccountAlwaysBcc.getText().toString());
mAccount.setName(mAccountName.getText().toString());
mAccount.setSignature(mAccountSignature.getText().toString());
mAccount.save(Preferences.getPreferences(this));
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
saveSettings();
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mAccount.save(Preferences.getPreferences(this));
finish();
}
}

View File

@ -0,0 +1,325 @@
package com.fsck.k9.activity.setup;
import java.net.URI;
import java.net.URISyntaxException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.fsck.k9.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.Utility;
public class AccountSetupIncoming extends Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private static final int popPorts[] = {
110, 995, 995, 110, 110
};
private static final String popSchemes[] = {
"pop3", "pop3+ssl", "pop3+ssl+", "pop3+tls", "pop3+tls+"
};
private static final int imapPorts[] = {
143, 993, 993, 143, 143
};
private static final String imapSchemes[] = {
"imap", "imap+ssl", "imap+ssl+", "imap+tls", "imap+tls+"
};
private int mAccountPorts[];
private String mAccountSchemes[];
private EditText mUsernameView;
private EditText mPasswordView;
private EditText mServerView;
private EditText mPortView;
private Spinner mSecurityTypeView;
private Spinner mDeletePolicyView;
private EditText mImapPathPrefixView;
private Button mNextButton;
private Account mAccount;
private boolean mMakeDefault;
public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) {
Intent i = new Intent(context, AccountSetupIncoming.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
context.startActivity(i);
}
public static void actionEditIncomingSettings(Activity context, Account account) {
Intent i = new Intent(context, AccountSetupIncoming.class);
i.setAction(Intent.ACTION_EDIT);
i.putExtra(EXTRA_ACCOUNT, account);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_incoming);
mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password);
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port);
mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy);
mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix);
mNextButton = (Button)findViewById(R.id.next);
mNextButton.setOnClickListener(this);
SpinnerOption securityTypes[] = {
new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
new SpinnerOption(1,
getString(R.string.account_setup_incoming_security_ssl_optional_label)),
new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)),
new SpinnerOption(3,
getString(R.string.account_setup_incoming_security_tls_optional_label)),
new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
};
SpinnerOption deletePolicies[] = {
new SpinnerOption(0,
getString(R.string.account_setup_incoming_delete_policy_never_label)),
new SpinnerOption(1,
getString(R.string.account_setup_incoming_delete_policy_7days_label)),
new SpinnerOption(2,
getString(R.string.account_setup_incoming_delete_policy_delete_label)),
};
ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this,
android.R.layout.simple_spinner_item, securityTypes);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(this,
android.R.layout.simple_spinner_item, deletePolicies);
deletePoliciesAdapter
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mDeletePolicyView.setAdapter(deletePoliciesAdapter);
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) {
updatePortFromSecurityType();
}
public void onNothingSelected(AdapterView<?> arg0) {
}
});
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/*
* Only allow digits in the port field.
*/
mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
/*
* If we're being reloaded we override the original account with the one
* we saved
*/
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
}
try {
URI uri = new URI(mAccount.getStoreUri());
String username = null;
String password = null;
if (uri.getUserInfo() != null) {
String[] userInfoParts = uri.getUserInfo().split(":", 2);
username = userInfoParts[0];
if (userInfoParts.length > 1) {
password = userInfoParts[1];
}
}
if (username != null) {
mUsernameView.setText(username);
}
if (password != null) {
mPasswordView.setText(password);
}
if (uri.getScheme().startsWith("pop3")) {
serverLabelView.setText(R.string.account_setup_incoming_pop_server_label);
mAccountPorts = popPorts;
mAccountSchemes = popSchemes;
findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
} else if (uri.getScheme().startsWith("imap")) {
serverLabelView.setText(R.string.account_setup_incoming_imap_server_label);
mAccountPorts = imapPorts;
mAccountSchemes = imapSchemes;
findViewById(R.id.account_delete_policy_label).setVisibility(View.GONE);
mDeletePolicyView.setVisibility(View.GONE);
if (uri.getPath() != null && uri.getPath().length() > 0) {
mImapPathPrefixView.setText(uri.getPath().substring(1));
}
} else {
throw new Error("Unknown account type: " + mAccount.getStoreUri());
}
for (int i = 0; i < mAccountSchemes.length; i++) {
if (mAccountSchemes[i].equals(uri.getScheme())) {
SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
}
}
SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mAccount.getDeletePolicy());
if (uri.getHost() != null) {
mServerView.setText(uri.getHost());
}
if (uri.getPort() != -1) {
mPortView.setText(Integer.toString(uri.getPort()));
} else {
updatePortFromSecurityType();
}
} catch (URISyntaxException use) {
/*
* We should always be able to parse our own settings.
*/
throw new Error(use);
}
validateFields();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
}
private void validateFields() {
mNextButton
.setEnabled(Utility.requiredFieldValid(mUsernameView)
&& Utility.requiredFieldValid(mPasswordView)
&& Utility.requiredFieldValid(mServerView)
&& Utility.requiredFieldValid(mPortView));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
}
private void updatePortFromSecurityType() {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
mPortView.setText(Integer.toString(mAccountPorts[securityType]));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
mAccount.save(Preferences.getPreferences(this));
finish();
} else {
/*
* Set the username and password for the outgoing settings to the username and
* password the user just set for incoming.
*/
try {
URI oldUri = new URI(mAccount.getTransportUri());
URI uri = new URI(
oldUri.getScheme(),
mUsernameView.getText() + ":" + mPasswordView.getText(),
oldUri.getHost(),
oldUri.getPort(),
null,
null,
null);
mAccount.setTransportUri(uri.toString());
} catch (URISyntaxException use) {
/*
* If we can't set up the URL we just continue. It's only for
* convenience.
*/
}
AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault);
finish();
}
}
}
private void onNext() {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
try {
String path = null;
if (mAccountSchemes[securityType].startsWith("imap")) {
path = "/" + mImapPathPrefixView.getText();
}
URI uri = new URI(
mAccountSchemes[securityType],
mUsernameView.getText() + ":" + mPasswordView.getText(),
mServerView.getText().toString(),
Integer.parseInt(mPortView.getText().toString()),
path, // path
null, // query
null);
mAccount.setStoreUri(uri.toString());
} catch (URISyntaxException use) {
/*
* It's unrecoverable if we cannot create a URI from components that
* we validated to be safe.
*/
throw new Error(use);
}
mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.next:
onNext();
break;
}
}
}

View File

@ -0,0 +1,103 @@
package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.TextKeyListener;
import android.text.method.TextKeyListener.Capitalize;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.Utility;
import com.fsck.k9.activity.FolderMessageList;
public class AccountSetupNames extends Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private EditText mDescription;
private EditText mName;
private Account mAccount;
private Button mDoneButton;
public static void actionSetNames(Context context, Account account) {
Intent i = new Intent(context, AccountSetupNames.class);
i.putExtra(EXTRA_ACCOUNT, account);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_names);
mDescription = (EditText)findViewById(R.id.account_description);
mName = (EditText)findViewById(R.id.account_name);
mDoneButton = (Button)findViewById(R.id.done);
mDoneButton.setOnClickListener(this);
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mName.addTextChangedListener(validationTextWatcher);
mName.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS));
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
/*
* Since this field is considered optional, we don't set this here. If
* the user fills in a value we'll reset the current value, otherwise we
* just leave the saved value alone.
*/
// mDescription.setText(mAccount.getDescription());
if (mAccount.getName() != null) {
mName.setText(mAccount.getName());
}
if (!Utility.requiredFieldValid(mName)) {
mDoneButton.setEnabled(false);
}
}
private void validateFields() {
mDoneButton.setEnabled(Utility.requiredFieldValid(mName));
Utility.setCompoundDrawablesAlpha(mDoneButton, mDoneButton.isEnabled() ? 255 : 128);
}
private void onNext() {
if (Utility.requiredFieldValid(mDescription)) {
mAccount.setDescription(mDescription.getText().toString());
}
mAccount.setName(mName.getText().toString());
mAccount.save(Preferences.getPreferences(this));
FolderMessageList.actionHandleAccount(this, mAccount, k9.INBOX);
finish();
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.done:
onNext();
break;
}
}
}

View File

@ -0,0 +1,103 @@
package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.Spinner;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
public class AccountSetupOptions extends Activity implements OnClickListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private Spinner mCheckFrequencyView;
private CheckBox mDefaultView;
private CheckBox mNotifyView;
private Account mAccount;
public static void actionOptions(Context context, Account account, boolean makeDefault) {
Intent i = new Intent(context, AccountSetupOptions.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_options);
mCheckFrequencyView = (Spinner)findViewById(R.id.account_check_frequency);
mDefaultView = (CheckBox)findViewById(R.id.account_default);
mNotifyView = (CheckBox)findViewById(R.id.account_notify);
findViewById(R.id.next).setOnClickListener(this);
SpinnerOption checkFrequencies[] = {
new SpinnerOption(-1,
getString(R.string.account_setup_options_mail_check_frequency_never)),
new SpinnerOption(5,
getString(R.string.account_setup_options_mail_check_frequency_5min)),
new SpinnerOption(10,
getString(R.string.account_setup_options_mail_check_frequency_10min)),
new SpinnerOption(15,
getString(R.string.account_setup_options_mail_check_frequency_15min)),
new SpinnerOption(30,
getString(R.string.account_setup_options_mail_check_frequency_30min)),
new SpinnerOption(60,
getString(R.string.account_setup_options_mail_check_frequency_1hour)),
};
ArrayAdapter<SpinnerOption> checkFrequenciesAdapter = new ArrayAdapter<SpinnerOption>(this,
android.R.layout.simple_spinner_item, checkFrequencies);
checkFrequenciesAdapter
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mCheckFrequencyView.setAdapter(checkFrequenciesAdapter);
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
boolean makeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
if (mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()) || makeDefault) {
mDefaultView.setChecked(true);
}
mNotifyView.setChecked(mAccount.isNotifyNewMail());
SpinnerOption.setSpinnerOptionValue(mCheckFrequencyView, mAccount
.getAutomaticCheckIntervalMinutes());
}
private void onDone() {
mAccount.setDescription(mAccount.getEmail());
mAccount.setNotifyNewMail(mNotifyView.isChecked());
mAccount.setAutomaticCheckIntervalMinutes((Integer)((SpinnerOption)mCheckFrequencyView
.getSelectedItem()).value);
mAccount.save(Preferences.getPreferences(this));
if (mDefaultView.isChecked()) {
Preferences.getPreferences(this).setDefaultAccount(mAccount);
}
k9.setServicesEnabled(this);
AccountSetupNames.actionSetNames(this, mAccount);
finish();
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.next:
onDone();
break;
}
}
}

View File

@ -0,0 +1,266 @@
package com.fsck.k9.activity.setup;
import java.net.URI;
import java.net.URISyntaxException;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.Account;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.Utility;
public class AccountSetupOutgoing extends Activity implements OnClickListener,
OnCheckedChangeListener {
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
private static final int smtpPorts[] = {
25, 465, 465, 25, 25
};
private static final String smtpSchemes[] = {
"smtp", "smtp+ssl", "smtp+ssl+", "smtp+tls", "smtp+tls+"
};
private EditText mUsernameView;
private EditText mPasswordView;
private EditText mServerView;
private EditText mPortView;
private CheckBox mRequireLoginView;
private ViewGroup mRequireLoginSettingsView;
private Spinner mSecurityTypeView;
private Button mNextButton;
private Account mAccount;
private boolean mMakeDefault;
public static void actionOutgoingSettings(Context context, Account account, boolean makeDefault) {
Intent i = new Intent(context, AccountSetupOutgoing.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
context.startActivity(i);
}
public static void actionEditOutgoingSettings(Context context, Account account) {
Intent i = new Intent(context, AccountSetupOutgoing.class);
i.setAction(Intent.ACTION_EDIT);
i.putExtra(EXTRA_ACCOUNT, account);
context.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_outgoing);
mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password);
mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port);
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
mRequireLoginSettingsView = (ViewGroup)findViewById(R.id.account_require_login_settings);
mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
mNextButton = (Button)findViewById(R.id.next);
mNextButton.setOnClickListener(this);
mRequireLoginView.setOnCheckedChangeListener(this);
SpinnerOption securityTypes[] = {
new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
new SpinnerOption(1,
getString(R.string.account_setup_incoming_security_ssl_optional_label)),
new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)),
new SpinnerOption(3,
getString(R.string.account_setup_incoming_security_tls_optional_label)),
new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
};
ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this,
android.R.layout.simple_spinner_item, securityTypes);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
/*
* Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change.
*/
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) {
updatePortFromSecurityType();
}
public void onNothingSelected(AdapterView<?> arg0) {
}
});
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher);
/*
* Only allow digits in the port field.
*/
mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
/*
* If we're being reloaded we override the original account with the one
* we saved
*/
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
}
try {
URI uri = new URI(mAccount.getTransportUri());
String username = null;
String password = null;
if (uri.getUserInfo() != null) {
String[] userInfoParts = uri.getUserInfo().split(":", 2);
username = userInfoParts[0];
if (userInfoParts.length > 1) {
password = userInfoParts[1];
}
}
if (username != null) {
mUsernameView.setText(username);
mRequireLoginView.setChecked(true);
}
if (password != null) {
mPasswordView.setText(password);
}
for (int i = 0; i < smtpSchemes.length; i++) {
if (smtpSchemes[i].equals(uri.getScheme())) {
SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
}
}
if (uri.getHost() != null) {
mServerView.setText(uri.getHost());
}
if (uri.getPort() != -1) {
mPortView.setText(Integer.toString(uri.getPort()));
} else {
updatePortFromSecurityType();
}
} catch (URISyntaxException use) {
/*
* We should always be able to parse our own settings.
*/
throw new Error(use);
}
validateFields();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
}
private void validateFields() {
mNextButton
.setEnabled(
Utility.requiredFieldValid(mServerView) &&
Utility.requiredFieldValid(mPortView) &&
(!mRequireLoginView.isChecked() ||
(Utility.requiredFieldValid(mUsernameView) &&
Utility.requiredFieldValid(mPasswordView))));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
}
private void updatePortFromSecurityType() {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
mPortView.setText(Integer.toString(smtpPorts[securityType]));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
mAccount.save(Preferences.getPreferences(this));
finish();
} else {
AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault);
finish();
}
}
}
private void onNext() {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
URI uri;
try {
String userInfo = null;
if (mRequireLoginView.isChecked()) {
userInfo = mUsernameView.getText().toString() + ":"
+ mPasswordView.getText().toString();
}
uri = new URI(smtpSchemes[securityType], userInfo, mServerView.getText().toString(),
Integer.parseInt(mPortView.getText().toString()), null, null, null);
mAccount.setTransportUri(uri.toString());
} catch (URISyntaxException use) {
/*
* It's unrecoverable if we cannot create a URI from components that
* we validated to be safe.
*/
throw new Error(use);
}
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.next:
onNext();
break;
}
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mRequireLoginSettingsView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
validateFields();
}
}

View File

@ -0,0 +1,33 @@
/**
*
*/
package com.fsck.k9.activity.setup;
import android.widget.Spinner;
public class SpinnerOption {
public Object value;
public String label;
public static void setSpinnerOptionValue(Spinner spinner, Object value) {
for (int i = 0, count = spinner.getCount(); i < count; i++) {
SpinnerOption so = (SpinnerOption)spinner.getItemAtPosition(i);
if (so.value.equals(value)) {
spinner.setSelection(i, true);
return;
}
}
}
public SpinnerOption(Object value, String label) {
this.value = value;
this.label = label;
}
@Override
public String toString() {
return label;
}
}

View File

@ -0,0 +1,788 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fsck.k9.codec.binary;
import org.apache.commons.codec.BinaryDecoder;
import org.apache.commons.codec.BinaryEncoder;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.EncoderException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
/**
* Provides Base64 encoding and decoding as defined by RFC 2045.
*
* <p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @author Apache Software Foundation
* @since 1.0-dev
* @version $Id$
*/
public class Base64 implements BinaryEncoder, BinaryDecoder {
/**
* Chunk size per RFC 2045 section 6.8.
*
* <p>
* The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
* equal signs.
* </p>
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
*/
static final int CHUNK_SIZE = 76;
/**
* Chunk separator per RFC 2045 section 2.1.
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
*/
static final byte[] CHUNK_SEPARATOR = {'\r','\n'};
/**
* This array is a lookup table that translates 6-bit positive integer
* index values into their "Base64 Alphabet" equivalents as specified
* in Table 1 of RFC 2045.
*
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
*/
private static final byte[] intToBase64 = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
/**
* Byte used to pad output.
*/
private static final byte PAD = '=';
/**
* This array is a lookup table that translates unicode characters
* drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
* into their 6-bit positive integer equivalents. Characters that
* are not in the Base64 alphabet but fall within the bounds of the
* array are translated to -1.
*
* Thanks to "commons" project in ws.apache.org for this code.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
*/
private static final byte[] base64ToInt = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
};
/** Mask used to extract 6 bits, used when encoding */
private static final int MASK_6BITS = 0x3f;
/** Mask used to extract 8 bits, used in decoding base64 bytes */
private static final int MASK_8BITS = 0xff;
// The static final fields above are used for the original static byte[] methods on Base64.
// The private member fields below are used with the new streaming approach, which requires
// some state be preserved between calls of encode() and decode().
/**
* Line length for encoding. Not used when decoding. A value of zero or less implies
* no chunking of the base64 encoded data.
*/
private final int lineLength;
/**
* Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
*/
private final byte[] lineSeparator;
/**
* Convenience variable to help us determine when our buffer is going to run out of
* room and needs resizing. <code>decodeSize = 3 + lineSeparator.length;</code>
*/
private final int decodeSize;
/**
* Convenience variable to help us determine when our buffer is going to run out of
* room and needs resizing. <code>encodeSize = 4 + lineSeparator.length;</code>
*/
private final int encodeSize;
/**
* Buffer for streaming.
*/
private byte[] buf;
/**
* Position where next character should be written in the buffer.
*/
private int pos;
/**
* Position where next character should be read from the buffer.
*/
private int readPos;
/**
* Variable tracks how many characters have been written to the current line.
* Only used when encoding. We use it to make sure each encoded line never
* goes beyond lineLength (if lineLength > 0).
*/
private int currentLinePos;
/**
* Writes to the buffer only occur after every 3 reads when encoding, an
* every 4 reads when decoding. This variable helps track that.
*/
private int modulus;
/**
* Boolean flag to indicate the EOF has been reached. Once EOF has been
* reached, this Base64 object becomes useless, and must be thrown away.
*/
private boolean eof;
/**
* Place holder for the 3 bytes we're dealing with for our base64 logic.
* Bitwise operations store and extract the base64 encoding or decoding from
* this variable.
*/
private int x;
/**
* Default constructor: lineLength is 76, and the lineSeparator is CRLF
* when encoding, and all forms can be decoded.
*/
public Base64() {
this(CHUNK_SIZE, CHUNK_SEPARATOR);
}
/**
* <p>
* Consumer can use this constructor to choose a different lineLength
* when encoding (lineSeparator is still CRLF). All forms of data can
* be decoded.
* </p><p>
* Note: lineLengths that aren't multiples of 4 will still essentially
* end up being multiples of 4 in the encoded data.
* </p>
*
* @param lineLength each line of encoded data will be at most this long
* (rounded up to nearest multiple of 4).
* If lineLength <= 0, then the output will not be divided into lines (chunks).
* Ignored when decoding.
*/
public Base64(int lineLength) {
this(lineLength, CHUNK_SEPARATOR);
}
/**
* <p>
* Consumer can use this constructor to choose a different lineLength
* and lineSeparator when encoding. All forms of data can
* be decoded.
* </p><p>
* Note: lineLengths that aren't multiples of 4 will still essentially
* end up being multiples of 4 in the encoded data.
* </p>
* @param lineLength Each line of encoded data will be at most this long
* (rounded up to nearest multiple of 4). Ignored when decoding.
* If <= 0, then output will not be divided into lines (chunks).
* @param lineSeparator Each line of encoded data will end with this
* sequence of bytes.
* If lineLength <= 0, then the lineSeparator is not used.
* @throws IllegalArgumentException The provided lineSeparator included
* some base64 characters. That's not going to work!
*/
public Base64(int lineLength, byte[] lineSeparator) {
this.lineLength = lineLength;
this.lineSeparator = new byte[lineSeparator.length];
System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
if (lineLength > 0) {
this.encodeSize = 4 + lineSeparator.length;
} else {
this.encodeSize = 4;
}
this.decodeSize = encodeSize - 1;
if (containsBase64Byte(lineSeparator)) {
String sep;
try {
sep = new String(lineSeparator, "UTF-8");
} catch (UnsupportedEncodingException uee) {
sep = new String(lineSeparator);
}
throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]");
}
}
/**
* Returns true if this Base64 object has buffered data for reading.
*
* @return true if there is Base64 object still available for reading.
*/
boolean hasData() { return buf != null; }
/**
* Returns the amount of buffered data available for reading.
*
* @return The amount of buffered data available for reading.
*/
int avail() { return buf != null ? pos - readPos : 0; }
/** Doubles our buffer. */
private void resizeBuf() {
if (buf == null) {
buf = new byte[8192];
pos = 0;
readPos = 0;
} else {
byte[] b = new byte[buf.length * 2];
System.arraycopy(buf, 0, b, 0, buf.length);
buf = b;
}
}
/**
* Extracts buffered data into the provided byte[] array, starting
* at position bPos, up to a maximum of bAvail bytes. Returns how
* many bytes were actually extracted.
*
* @param b byte[] array to extract the buffered data into.
* @param bPos position in byte[] array to start extraction at.
* @param bAvail amount of bytes we're allowed to extract. We may extract
* fewer (if fewer are available).
* @return The number of bytes successfully extracted into the provided
* byte[] array.
*/
int readResults(byte[] b, int bPos, int bAvail) {
if (buf != null) {
int len = Math.min(avail(), bAvail);
if (buf != b) {
System.arraycopy(buf, readPos, b, bPos, len);
readPos += len;
if (readPos >= pos) {
buf = null;
}
} else {
// Re-using the original consumer's output array is only
// allowed for one round.
buf = null;
}
return len;
} else {
return eof ? -1 : 0;
}
}
/**
* Small optimization where we try to buffer directly to the consumer's
* output array for one round (if consumer calls this method first!) instead
* of starting our own buffer.
*
* @param out byte[] array to buffer directly to.
* @param outPos Position to start buffering into.
* @param outAvail Amount of bytes available for direct buffering.
*/
void setInitialBuffer(byte[] out, int outPos, int outAvail) {
// We can re-use consumer's original output array under
// special circumstances, saving on some System.arraycopy().
if (out != null && out.length == outAvail) {
buf = out;
pos = outPos;
readPos = outPos;
}
}
/**
* <p>
* Encodes all of the provided data, starting at inPos, for inAvail bytes.
* Must be called at least twice: once with the data to encode, and once
* with inAvail set to "-1" to alert encoder that EOF has been reached,
* so flush last remaining bytes (if not multiple of 3).
* </p><p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
* and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
*
* @param in byte[] array of binary data to base64 encode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for encoding.
*/
void encode(byte[] in, int inPos, int inAvail) {
if (eof) {
return;
}
// inAvail < 0 is how we're informed of EOF in the underlying data we're
// encoding.
if (inAvail < 0) {
eof = true;
if (buf == null || buf.length - pos < encodeSize) {
resizeBuf();
}
switch (modulus) {
case 1:
buf[pos++] = intToBase64[(x >> 2) & MASK_6BITS];
buf[pos++] = intToBase64[(x << 4) & MASK_6BITS];
buf[pos++] = PAD;
buf[pos++] = PAD;
break;
case 2:
buf[pos++] = intToBase64[(x >> 10) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 4) & MASK_6BITS];
buf[pos++] = intToBase64[(x << 2) & MASK_6BITS];
buf[pos++] = PAD;
break;
}
if (lineLength > 0) {
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
pos += lineSeparator.length;
}
} else {
for (int i = 0; i < inAvail; i++) {
if (buf == null || buf.length - pos < encodeSize) {
resizeBuf();
}
modulus = (++modulus) % 3;
int b = in[inPos++];
if (b < 0) { b += 256; }
x = (x << 8) + b;
if (0 == modulus) {
buf[pos++] = intToBase64[(x >> 18) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 12) & MASK_6BITS];
buf[pos++] = intToBase64[(x >> 6) & MASK_6BITS];
buf[pos++] = intToBase64[x & MASK_6BITS];
currentLinePos += 4;
if (lineLength > 0 && lineLength <= currentLinePos) {
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
pos += lineSeparator.length;
currentLinePos = 0;
}
}
}
}
}
/**
* <p>
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
* Should be called at least twice: once with the data to decode, and once
* with inAvail set to "-1" to alert decoder that EOF has been reached.
* The "-1" call is not necessary when decoding, but it doesn't hurt, either.
* </p><p>
* Ignores all non-base64 characters. This is how chunked (e.g. 76 character)
* data is handled, since CR and LF are silently ignored, but has implications
* for other bytes, too. This method subscribes to the garbage-in, garbage-out
* philosophy: it will not check the provided data for validity.
* </p><p>
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
* and general approach.
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
* </p>
* @param in byte[] array of ascii data to base64 decode.
* @param inPos Position to start reading data from.
* @param inAvail Amount of bytes available from input for encoding.
*/
void decode(byte[] in, int inPos, int inAvail) {
if (eof) {
return;
}
if (inAvail < 0) {
eof = true;
}
for (int i = 0; i < inAvail; i++) {
if (buf == null || buf.length - pos < decodeSize) {
resizeBuf();
}
byte b = in[inPos++];
if (b == PAD) {
x = x << 6;
switch (modulus) {
case 2:
x = x << 6;
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
break;
case 3:
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
break;
}
// WE'RE DONE!!!!
eof = true;
return;
} else {
if (b >= 0 && b < base64ToInt.length) {
int result = base64ToInt[b];
if (result >= 0) {
modulus = (++modulus) % 4;
x = (x << 6) + result;
if (modulus == 0) {
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
buf[pos++] = (byte) (x & MASK_8BITS);
}
}
}
}
}
}
/**
* Returns whether or not the <code>octet</code> is in the base 64 alphabet.
*
* @param octet
* The value to test
* @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
*/
public static boolean isBase64(byte octet) {
return octet == PAD || (octet >= 0 && octet < base64ToInt.length && base64ToInt[octet] != -1);
}
/**
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
* Currently the method treats whitespace as valid.
*
* @param arrayOctet
* byte array to test
* @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is
* empty; false, otherwise
*/
public static boolean isArrayByteBase64(byte[] arrayOctet) {
for (int i = 0; i < arrayOctet.length; i++) {
if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
return false;
}
}
return true;
}
/*
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
*
* @param arrayOctet
* byte array to test
* @return <code>true</code> if any byte is a valid character in the Base64 alphabet; false herwise
*/
private static boolean containsBase64Byte(byte[] arrayOctet) {
for (int i = 0; i < arrayOctet.length; i++) {
if (isBase64(arrayOctet[i])) {
return true;
}
}
return false;
}
/**
* Encodes binary data using the base64 algorithm but does not chunk the output.
*
* @param binaryData
* binary data to encode
* @return Base64 characters
*/
public static byte[] encodeBase64(byte[] binaryData) {
return encodeBase64(binaryData, false);
}
/**
* Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
*
* @param binaryData
* binary data to encode
* @return Base64 characters chunked in 76 character blocks
*/
public static byte[] encodeBase64Chunked(byte[] binaryData) {
return encodeBase64(binaryData, true);
}
/**
* Decodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
* Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[].
*
* @param pObject
* Object to decode
* @return An object (of type byte[]) containing the binary data which corresponds to the byte[] supplied.
* @throws DecoderException
* if the parameter supplied is not of type byte[]
*/
public Object decode(Object pObject) throws DecoderException {
if (!(pObject instanceof byte[])) {
throw new DecoderException("Parameter supplied to Base64 decode is not a byte[]");
}
return decode((byte[]) pObject);
}
/**
* Decodes a byte[] containing containing characters in the Base64 alphabet.
*
* @param pArray
* A byte array containing Base64 character data
* @return a byte array containing binary data
*/
public byte[] decode(byte[] pArray) {
return decodeBase64(pArray);
}
/**
* Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
*
* @param binaryData
* Array containing binary data to encode.
* @param isChunked
* if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
* @return Base64-encoded data.
* @throws IllegalArgumentException
* Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
*/
public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
if (binaryData == null || binaryData.length == 0) {
return binaryData;
}
Base64 b64 = isChunked ? new Base64() : new Base64(0);
long len = (binaryData.length * 4) / 3;
long mod = len % 4;
if (mod != 0) {
len += 4 - mod;
}
if (isChunked) {
len += (1 + (len / CHUNK_SIZE)) * CHUNK_SEPARATOR.length;
}
if (len > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Input array too big, output array would be bigger than Integer.MAX_VALUE=" + Integer.MAX_VALUE);
}
byte[] buf = new byte[(int) len];
b64.setInitialBuffer(buf, 0, buf.length);
b64.encode(binaryData, 0, binaryData.length);
b64.encode(binaryData, 0, -1); // Notify encoder of EOF.
// Encoder might have resized, even though it was unnecessary.
if (b64.buf != buf) {
b64.readResults(buf, 0, buf.length);
}
return buf;
}
/**
* Decodes Base64 data into octets
*
* @param base64Data Byte array containing Base64 data
* @return Array containing decoded data.
*/
public static byte[] decodeBase64(byte[] base64Data) {
if (base64Data == null || base64Data.length == 0) {
return base64Data;
}
Base64 b64 = new Base64();
long len = (base64Data.length * 3) / 4;
byte[] buf = new byte[(int) len];
b64.setInitialBuffer(buf, 0, buf.length);
b64.decode(base64Data, 0, base64Data.length);
b64.decode(base64Data, 0, -1); // Notify decoder of EOF.
// We have no idea what the line-length was, so we
// cannot know how much of our array wasn't used.
byte[] result = new byte[b64.pos];
b64.readResults(result, 0, result.length);
return result;
}
/**
* Discards any whitespace from a base-64 encoded block.
*
* @param data
* The base-64 encoded data to discard the whitespace from.
* @return The data, less whitespace (see RFC 2045).
* @deprecated This method is no longer needed
*/
static byte[] discardWhitespace(byte[] data) {
byte groomedData[] = new byte[data.length];
int bytesCopied = 0;
for (int i = 0; i < data.length; i++) {
switch (data[i]) {
case ' ' :
case '\n' :
case '\r' :
case '\t' :
break;
default :
groomedData[bytesCopied++] = data[i];
}
}
byte packedData[] = new byte[bytesCopied];
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
/**
* Check if a byte value is whitespace or not.
*
* @param byteToCheck the byte to check
* @return true if byte is whitespace, false otherwise
*/
private static boolean isWhiteSpace(byte byteToCheck){
switch (byteToCheck) {
case ' ' :
case '\n' :
case '\r' :
case '\t' :
return true;
default :
return false;
}
}
/**
* Discards any characters outside of the base64 alphabet, per the requirements on page 25 of RFC 2045 - "Any
* characters outside of the base64 alphabet are to be ignored in base64 encoded data."
*
* @param data
* The base-64 encoded data to groom
* @return The data, less non-base64 characters (see RFC 2045).
*/
static byte[] discardNonBase64(byte[] data) {
byte groomedData[] = new byte[data.length];
int bytesCopied = 0;
for (int i = 0; i < data.length; i++) {
if (isBase64(data[i])) {
groomedData[bytesCopied++] = data[i];
}
}
byte packedData[] = new byte[bytesCopied];
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
// Implementation of the Encoder Interface
/**
* Encodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
* Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].
*
* @param pObject
* Object to encode
* @return An object (of type byte[]) containing the base64 encoded data which corresponds to the byte[] supplied.
* @throws EncoderException
* if the parameter supplied is not of type byte[]
*/
public Object encode(Object pObject) throws EncoderException {
if (!(pObject instanceof byte[])) {
throw new EncoderException("Parameter supplied to Base64 encode is not a byte[]");
}
return encode((byte[]) pObject);
}
/**
* Encodes a byte[] containing binary data, into a byte[] containing characters in the Base64 alphabet.
*
* @param pArray
* a byte array containing binary data
* @return A byte array containing only Base64 character data
*/
public byte[] encode(byte[] pArray) {
return encodeBase64(pArray, false);
}
// Implementation of integer encoding used for crypto
/**
* Decode a byte64-encoded integer according to crypto
* standards such as W3C's XML-Signature
*
* @param pArray a byte array containing base64 character data
* @return A BigInteger
*/
public static BigInteger decodeInteger(byte[] pArray) {
return new BigInteger(1, decodeBase64(pArray));
}
/**
* Encode to a byte64-encoded integer according to crypto
* standards such as W3C's XML-Signature
*
* @param bigInt a BigInteger
* @return A byte array containing base64 character data
* @throws NullPointerException if null is passed in
*/
public static byte[] encodeInteger(BigInteger bigInt) {
if(bigInt == null) {
throw new NullPointerException("encodeInteger called with null parameter");
}
return encodeBase64(toIntegerBytes(bigInt), false);
}
/**
* Returns a byte-array representation of a <code>BigInteger</code>
* without sign bit.
*
* @param bigInt <code>BigInteger</code> to be converted
* @return a byte array representation of the BigInteger parameter
*/
static byte[] toIntegerBytes(BigInteger bigInt) {
int bitlen = bigInt.bitLength();
// round bitlen
bitlen = ((bitlen + 7) >> 3) << 3;
byte[] bigBytes = bigInt.toByteArray();
if(((bigInt.bitLength() % 8) != 0) &&
(((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
return bigBytes;
}
// set up params for copying everything but sign bit
int startSrc = 0;
int len = bigBytes.length;
// if bigInt is exactly byte-aligned, just skip signbit in copy
if((bigInt.bitLength() % 8) == 0) {
startSrc = 1;
len--;
}
int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
byte[] resizedBytes = new byte[bitlen / 8];
System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
return resizedBytes;
}
}

View File

@ -0,0 +1,179 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fsck.k9.codec.binary;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Provides Base64 encoding and decoding in a streaming fashion (unlimited size).
* When encoding the default lineLength is 76 characters and the default
* lineEnding is CRLF, but these can be overridden by using the appropriate
* constructor.
* <p>
* The default behaviour of the Base64OutputStream is to ENCODE, whereas the
* default behaviour of the Base64InputStream is to DECODE. But this behaviour
* can be overridden by using a different constructor.
* </p><p>
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
* </p>
*
* @author Apache Software Foundation
* @version $Id $
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
* @since 1.0-dev
*/
public class Base64OutputStream extends FilterOutputStream {
private final boolean doEncode;
private final Base64 base64;
private final byte[] singleByte = new byte[1];
/**
* Creates a Base64OutputStream such that all data written is Base64-encoded
* to the original provided OutputStream.
*
* @param out OutputStream to wrap.
*/
public Base64OutputStream(OutputStream out) {
this(out, true);
}
/**
* Creates a Base64OutputStream such that all data written is either
* Base64-encoded or Base64-decoded to the original provided OutputStream.
*
* @param out OutputStream to wrap.
* @param doEncode true if we should encode all data written to us,
* false if we should decode.
*/
public Base64OutputStream(OutputStream out, boolean doEncode) {
super(out);
this.doEncode = doEncode;
this.base64 = new Base64();
}
/**
* Creates a Base64OutputStream such that all data written is either
* Base64-encoded or Base64-decoded to the original provided OutputStream.
*
* @param out OutputStream to wrap.
* @param doEncode true if we should encode all data written to us,
* false if we should decode.
* @param lineLength If doEncode is true, each line of encoded
* data will contain lineLength characters.
* If lineLength <=0, the encoded data is not divided into lines.
* If doEncode is false, lineLength is ignored.
* @param lineSeparator If doEncode is true, each line of encoded
* data will be terminated with this byte sequence (e.g. \r\n).
* If lineLength <= 0, the lineSeparator is not used.
* If doEncode is false lineSeparator is ignored.
*/
public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
super(out);
this.doEncode = doEncode;
this.base64 = new Base64(lineLength, lineSeparator);
}
/**
* Writes the specified <code>byte</code> to this output stream.
*/
public void write(int i) throws IOException {
singleByte[0] = (byte) i;
write(singleByte, 0, 1);
}
/**
* Writes <code>len</code> bytes from the specified
* <code>b</code> array starting at <code>offset</code> to
* this output stream.
*
* @param b source byte array
* @param offset where to start reading the bytes
* @param len maximum number of bytes to write
*
* @throws IOException if an I/O error occurs.
* @throws NullPointerException if the byte array parameter is null
* @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
*/
public void write(byte b[], int offset, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (offset < 0 || len < 0 || offset + len < 0) {
throw new IndexOutOfBoundsException();
} else if (offset > b.length || offset + len > b.length) {
throw new IndexOutOfBoundsException();
} else if (len > 0) {
if (doEncode) {
base64.encode(b, offset, len);
} else {
base64.decode(b, offset, len);
}
flush(false);
}
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream. If propogate is true, the wrapped
* stream will also be flushed.
*
* @param propogate boolean flag to indicate whether the wrapped
* OutputStream should also be flushed.
* @throws IOException if an I/O error occurs.
*/
private void flush(boolean propogate) throws IOException {
int avail = base64.avail();
if (avail > 0) {
byte[] buf = new byte[avail];
int c = base64.readResults(buf, 0, avail);
if (c > 0) {
out.write(buf, 0, c);
}
}
if (propogate) {
out.flush();
}
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream.
*
* @throws IOException if an I/O error occurs.
*/
public void flush() throws IOException {
flush(true);
}
/**
* Closes this output stream, flushing any remaining bytes that must be encoded. The
* underlying stream is flushed but not closed.
*/
public void close() throws IOException {
// Notify encoder of EOF (-1).
if (doEncode) {
base64.encode(singleByte, 0, -1);
} else {
base64.decode(singleByte, 0, -1);
}
flush();
}
}

169
src/com/fsck/k9/k9.java Normal file
View File

@ -0,0 +1,169 @@
package com.fsck.k9;
import java.io.File;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.service.BootReceiver;
import com.fsck.k9.service.MailService;
public class k9 extends Application {
public static final String LOG_TAG = "k9";
public static File tempDirectory;
/**
* If this is enabled there will be additional logging information sent to
* Log.d, including protocol dumps.
*/
public static boolean DEBUG = false;
/**
* If this is enabled than logging that normally hides sensitive information
* like passwords will show that information.
*/
public static boolean DEBUG_SENSITIVE = false;
/**
* The MIME type(s) of attachments we're willing to send. At the moment it is not possible
* to open a chooser with a list of filter types, so the chooser is only opened with the first
* item in the list. The entire list will be used to filter down attachments that are added
* with Intent.ACTION_SEND.
*/
public static final String[] ACCEPTABLE_ATTACHMENT_SEND_TYPES = new String[] {
"image/*",
};
/**
* The MIME type(s) of attachments we're willing to view.
*/
public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
"image/*",
"audio/*",
"text/*",
};
/**
* The MIME type(s) of attachments we're not willing to view.
*/
public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
"image/gif",
};
/**
* The MIME type(s) of attachments we're willing to download to SD.
*/
public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
"image/*",
};
/**
* The MIME type(s) of attachments we're not willing to download to SD.
*/
public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
"image/gif",
};
/**
* The special name "INBOX" is used throughout the application to mean "Whatever folder
* the server refers to as the user's Inbox. Placed here to ease use.
*/
public static final String INBOX = "INBOX";
/**
* Specifies how many messages will be shown in a folder by default. This number is set
* on each new folder and can be incremented with "Load more messages..." by the
* VISIBLE_LIMIT_INCREMENT
*/
public static final int DEFAULT_VISIBLE_LIMIT = 25;
/**
* Number of additional messages to load when a user selectes "Load more messages..."
*/
public static final int VISIBLE_LIMIT_INCREMENT = 25;
/**
* The maximum size of an attachment we're willing to download (either View or Save)
* Attachments that are base64 encoded (most) will be about 1.375x their actual size
* so we should probably factor that in. A 5MB attachment will generally be around
* 6.8MB downloaded but only 5MB saved.
*/
public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
/**
* Called throughout the application when the number of accounts has changed. This method
* enables or disables the Compose activity, the boot receiver and the service based on
* whether any accounts are configured.
*/
public static void setServicesEnabled(Context context) {
setServicesEnabled(context, Preferences.getPreferences(context).getAccounts().length > 0);
}
public static void setServicesEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
if (!enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
/*
* If no accounts now exist but the service is still enabled we're about to disable it
* so we'll reschedule to kill off any existing alarms.
*/
MailService.actionReschedule(context);
}
pm.setComponentEnabledSetting(
new ComponentName(context, MessageCompose.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(
new ComponentName(context, BootReceiver.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(
new ComponentName(context, MailService.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
if (enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
/*
* And now if accounts do exist then we've just enabled the service and we want to
* schedule alarms for the new accounts.
*/
MailService.actionReschedule(context);
}
}
@Override
public void onCreate() {
super.onCreate();
Preferences prefs = Preferences.getPreferences(this);
DEBUG = prefs.geteEnableDebugLogging();
DEBUG_SENSITIVE = prefs.getEnableSensitiveLogging();
MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts());
/*
* We have to give MimeMessage a temp directory because File.createTempFile(String, String)
* doesn't work in Android and MimeMessage does not have access to a Context.
*/
BinaryTempFileBody.setTempDirectory(getCacheDir());
}
}

View File

@ -0,0 +1,215 @@
package com.fsck.k9.mail;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.james.mime4j.field.address.AddressList;
import org.apache.james.mime4j.field.address.Mailbox;
import org.apache.james.mime4j.field.address.MailboxList;
import org.apache.james.mime4j.field.address.NamedMailbox;
import org.apache.james.mime4j.field.address.parser.ParseException;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.Utility;
import com.fsck.k9.mail.internet.MimeUtility;
public class Address {
String mAddress;
String mPersonal;
public Address(String address, String personal) {
this.mAddress = address;
this.mPersonal = personal;
}
public Address(String address) {
this.mAddress = address;
}
public String getAddress() {
return mAddress;
}
public void setAddress(String address) {
this.mAddress = address;
}
public String getPersonal() {
return mPersonal;
}
public void setPersonal(String personal) {
this.mPersonal = personal;
}
/**
* Parse a comma separated list of addresses in RFC-822 format and return an
* array of Address objects.
*
* @param addressList
* @return An array of 0 or more Addresses.
*/
public static Address[] parse(String addressList) {
ArrayList<Address> addresses = new ArrayList<Address>();
if (addressList == null) {
return new Address[] {};
}
try {
MailboxList parsedList = AddressList.parse(addressList).flatten();
for (int i = 0, count = parsedList.size(); i < count; i++) {
org.apache.james.mime4j.field.address.Address address = parsedList.get(i);
if (address instanceof NamedMailbox) {
NamedMailbox namedMailbox = (NamedMailbox)address;
addresses.add(new Address(namedMailbox.getLocalPart() + "@"
+ namedMailbox.getDomain(), namedMailbox.getName()));
} else if (address instanceof Mailbox) {
Mailbox mailbox = (Mailbox)address;
addresses.add(new Address(mailbox.getLocalPart() + "@" + mailbox.getDomain()));
} else {
Log.e(k9.LOG_TAG, "Unknown address type from Mime4J: "
+ address.getClass().toString());
}
}
} catch (ParseException pe) {
}
return addresses.toArray(new Address[] {});
}
@Override
public boolean equals(Object o) {
if (o instanceof Address) {
return getAddress().equals(((Address) o).getAddress());
}
return super.equals(o);
}
public String toString() {
if (mPersonal != null) {
if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
} else {
return mPersonal + " <" + mAddress + ">";
}
} else {
return mAddress;
}
}
public static String toString(Address[] addresses) {
if (addresses == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < addresses.length; i++) {
sb.append(addresses[i].toString());
if (i < addresses.length - 1) {
sb.append(',');
}
}
return sb.toString();
}
/**
* Returns either the personal portion of the Address or the address portion if the personal
* is not available.
* @return
*/
public String toFriendly() {
if (mPersonal != null && mPersonal.length() > 0) {
return mPersonal;
}
else {
return mAddress;
}
}
public static String toFriendly(Address[] addresses) {
if (addresses == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < addresses.length; i++) {
sb.append(addresses[i].toFriendly());
if (i < addresses.length - 1) {
sb.append(',');
}
}
return sb.toString();
}
/**
* Unpacks an address list previously packed with packAddressList()
* @param list
* @return
*/
public static Address[] unpack(String addressList) {
if (addressList == null) {
return new Address[] { };
}
ArrayList<Address> addresses = new ArrayList<Address>();
int length = addressList.length();
int pairStartIndex = 0;
int pairEndIndex = 0;
int addressEndIndex = 0;
while (pairStartIndex < length) {
pairEndIndex = addressList.indexOf(',', pairStartIndex);
if (pairEndIndex == -1) {
pairEndIndex = length;
}
addressEndIndex = addressList.indexOf(';', pairStartIndex);
String address = null;
String personal = null;
if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
}
else {
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
personal = Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
}
addresses.add(new Address(address, personal));
pairStartIndex = pairEndIndex + 1;
}
return addresses.toArray(new Address[] { });
}
/**
* Packs an address list into a String that is very quick to read
* and parse. Packed lists can be unpacked with unpackAddressList()
* The packed list is a comma seperated list of:
* URLENCODE(address)[;URLENCODE(personal)]
* @param list
* @return
*/
public static String pack(Address[] addresses) {
if (addresses == null) {
return null;
}
StringBuffer sb = new StringBuffer();
for (int i = 0, count = addresses.length; i < count; i++) {
Address address = addresses[i];
try {
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
if (address.getPersonal() != null) {
sb.append(';');
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
}
if (i < count - 1) {
sb.append(',');
}
}
catch (UnsupportedEncodingException uee) {
return null;
}
}
return sb.toString();
}
}

View File

@ -0,0 +1,14 @@
package com.fsck.k9.mail;
public class AuthenticationFailedException extends MessagingException {
public static final long serialVersionUID = -1;
public AuthenticationFailedException(String message) {
super(message);
}
public AuthenticationFailedException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,11 @@
package com.fsck.k9.mail;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface Body {
public InputStream getInputStream() throws MessagingException;
public void writeTo(OutputStream out) throws IOException, MessagingException;
}

View File

@ -0,0 +1,10 @@
package com.fsck.k9.mail;
public abstract class BodyPart implements Part {
protected Multipart mParent;
public Multipart getParent() {
return mParent;
}
}

View File

@ -0,0 +1,14 @@
package com.fsck.k9.mail;
public class CertificateValidationException extends MessagingException {
public static final long serialVersionUID = -1;
public CertificateValidationException(String message) {
super(message);
}
public CertificateValidationException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,57 @@
package com.fsck.k9.mail;
import java.util.ArrayList;
/**
* <pre>
* A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
* FetchProfile can contain the following objects:
* FetchProfile.Item: Described below.
* Message: Indicates that the body of the entire message should be fetched.
* Synonymous with FetchProfile.Item.BODY.
* Part: Indicates that the given Part should be fetched. The provider
* is expected have previously created the given BodyPart and stored
* any information it needs to download the content.
* </pre>
*/
public class FetchProfile extends ArrayList {
/**
* Default items available for pre-fetching. It should be expected that any
* item fetched by using these items could potentially include all of the
* previous items.
*/
public enum Item {
/**
* Download the flags of the message.
*/
FLAGS,
/**
* Download the envelope of the message. This should include at minimum
* the size and the following headers: date, subject, from, content-type, to, cc
*/
ENVELOPE,
/**
* Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
* and may map to other providers.
* The provider should, if possible, fill in a properly formatted MIME structure in
* the message without actually downloading any message data. If the provider is not
* capable of this operation it should specifically set the body of the message to null
* so that upper levels can detect that a full body download is needed.
*/
STRUCTURE,
/**
* A sane portion of the entire message, cut off at a provider determined limit.
* This should generaly be around 50kB.
*/
BODY_SANE,
/**
* The entire message.
*/
BODY,
}
}

View File

@ -0,0 +1,48 @@
package com.fsck.k9.mail;
/**
* Flags that can be applied to Messages.
*/
public enum Flag {
DELETED,
SEEN,
ANSWERED,
FLAGGED,
DRAFT,
RECENT,
/*
* The following flags are for internal library use only.
* TODO Eventually we should creates a Flags class that extends ArrayList that allows
* these flags and Strings to represent user defined flags. At that point the below
* flags should become user defined flags.
*/
/**
* Delete and remove from the LocalStore immediately.
*/
X_DESTROYED,
/**
* Sending of an unsent message failed. It will be retried. Used to show status.
*/
X_SEND_FAILED,
/**
* Sending of an unsent message is in progress.
*/
X_SEND_IN_PROGRESS,
/**
* Indicates that a message is fully downloaded from the server and can be viewed normally.
* This does not include attachments, which are never downloaded fully.
*/
X_DOWNLOADED_FULL,
/**
* Indicates that a message is partially downloaded from the server and can be viewed but
* more content is available on the server.
* This does not include attachments, which are never downloaded fully.
*/
X_DOWNLOADED_PARTIAL,
}

View File

@ -0,0 +1,95 @@
package com.fsck.k9.mail;
public abstract class Folder {
public enum OpenMode {
READ_WRITE, READ_ONLY,
}
public enum FolderType {
HOLDS_FOLDERS, HOLDS_MESSAGES,
}
/**
* Forces an open of the MailProvider. If the provider is already open this
* function returns without doing anything.
*
* @param mode READ_ONLY or READ_WRITE
*/
public abstract void open(OpenMode mode) throws MessagingException;
/**
* Forces a close of the MailProvider. Any further access will attempt to
* reopen the MailProvider.
*
* @param expunge If true all deleted messages will be expunged.
*/
public abstract void close(boolean expunge) throws MessagingException;
/**
* @return True if further commands are not expected to have to open the
* connection.
*/
public abstract boolean isOpen();
/**
* Get the mode the folder was opened with. This may be different than the mode the open
* was requested with.
* @return
*/
public abstract OpenMode getMode() throws MessagingException;
public abstract boolean create(FolderType type) throws MessagingException;
public abstract boolean exists() throws MessagingException;
/**
* @return A count of the messages in the selected folder.
*/
public abstract int getMessageCount() throws MessagingException;
public abstract int getUnreadMessageCount() throws MessagingException;
public abstract Message getMessage(String uid) throws MessagingException;
public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener)
throws MessagingException;
/**
* Fetches the given list of messages. The specified listener is notified as
* each fetch completes. Messages are downloaded as (as) lightweight (as
* possible) objects to be filled in with later requests. In most cases this
* means that only the UID is downloaded.
*
* @param uids
* @param listener
*/
public abstract Message[] getMessages(MessageRetrievalListener listener)
throws MessagingException;
public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
throws MessagingException;
public abstract void appendMessages(Message[] messages) throws MessagingException;
public abstract void copyMessages(Message[] msgs, Folder folder) throws MessagingException;
public abstract void setFlags(Message[] messages, Flag[] flags, boolean value)
throws MessagingException;
public abstract Message[] expunge() throws MessagingException;
public abstract void fetch(Message[] messages, FetchProfile fp,
MessageRetrievalListener listener) throws MessagingException;
public abstract void delete(boolean recurse) throws MessagingException;
public abstract String getName();
public abstract Flag[] getPermanentFlags() throws MessagingException;
@Override
public String toString() {
return getName();
}
}

View File

@ -0,0 +1,118 @@
package com.fsck.k9.mail;
import java.util.Date;
import java.util.HashSet;
public abstract class Message implements Part, Body {
public enum RecipientType {
TO, CC, BCC,
}
protected String mUid;
protected HashSet<Flag> mFlags = new HashSet<Flag>();
protected Date mInternalDate;
protected Folder mFolder;
public String getUid() {
return mUid;
}
public void setUid(String uid) {
this.mUid = uid;
}
public Folder getFolder() {
return mFolder;
}
public abstract String getSubject() throws MessagingException;
public abstract void setSubject(String subject) throws MessagingException;
public Date getInternalDate() {
return mInternalDate;
}
public void setInternalDate(Date internalDate) {
this.mInternalDate = internalDate;
}
public abstract Date getReceivedDate() throws MessagingException;
public abstract Date getSentDate() throws MessagingException;
public abstract void setSentDate(Date sentDate) throws MessagingException;
public abstract Address[] getRecipients(RecipientType type) throws MessagingException;
public abstract void setRecipients(RecipientType type, Address[] addresses)
throws MessagingException;
public void setRecipient(RecipientType type, Address address) throws MessagingException {
setRecipients(type, new Address[] {
address
});
}
public abstract Address[] getFrom() throws MessagingException;
public abstract void setFrom(Address from) throws MessagingException;
public abstract Address[] getReplyTo() throws MessagingException;
public abstract void setReplyTo(Address[] from) throws MessagingException;
public abstract Body getBody() throws MessagingException;
public abstract String getContentType() throws MessagingException;
public abstract void addHeader(String name, String value) throws MessagingException;
public abstract void setHeader(String name, String value) throws MessagingException;
public abstract String[] getHeader(String name) throws MessagingException;
public abstract void removeHeader(String name) throws MessagingException;
public abstract void setBody(Body body) throws MessagingException;
public boolean isMimeType(String mimeType) throws MessagingException {
return getContentType().startsWith(mimeType);
}
/*
* TODO Refactor Flags at some point to be able to store user defined flags.
*/
public Flag[] getFlags() {
return mFlags.toArray(new Flag[] {});
}
public void setFlag(Flag flag, boolean set) throws MessagingException {
if (set) {
mFlags.add(flag);
} else {
mFlags.remove(flag);
}
}
/**
* This method calls setFlag(Flag, boolean)
* @param flags
* @param set
*/
public void setFlags(Flag[] flags, boolean set) throws MessagingException {
for (Flag flag : flags) {
setFlag(flag, set);
}
}
public boolean isSet(Flag flag) {
return mFlags.contains(flag);
}
public abstract void saveChanges() throws MessagingException;
}

View File

@ -0,0 +1,19 @@
package com.fsck.k9.mail;
import java.util.Comparator;
public class MessageDateComparator implements Comparator<Message> {
public int compare(Message o1, Message o2) {
try {
if (o1.getSentDate() == null) {
return 1;
} else if (o2.getSentDate() == null) {
return -1;
} else
return o2.getSentDate().compareTo(o1.getSentDate());
} catch (Exception e) {
return 0;
}
}
}

View File

@ -0,0 +1,8 @@
package com.fsck.k9.mail;
public interface MessageRetrievalListener {
public void messageStarted(String uid, int number, int ofTotal);
public void messageFinished(Message message, int number, int ofTotal);
}

View File

@ -0,0 +1,14 @@
package com.fsck.k9.mail;
public class MessagingException extends Exception {
public static final long serialVersionUID = -1;
public MessagingException(String message) {
super(message);
}
public MessagingException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,48 @@
package com.fsck.k9.mail;
import java.util.ArrayList;
public abstract class Multipart implements Body {
protected Part mParent;
protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
protected String mContentType;
public void addBodyPart(BodyPart part) throws MessagingException {
mParts.add(part);
}
public void addBodyPart(BodyPart part, int index) throws MessagingException {
mParts.add(index, part);
}
public BodyPart getBodyPart(int index) throws MessagingException {
return mParts.get(index);
}
public String getContentType() throws MessagingException {
return mContentType;
}
public int getCount() throws MessagingException {
return mParts.size();
}
public boolean removeBodyPart(BodyPart part) throws MessagingException {
return mParts.remove(part);
}
public void removeBodyPart(int index) throws MessagingException {
mParts.remove(index);
}
public Part getParent() throws MessagingException {
return mParent;
}
public void setParent(Part parent) throws MessagingException {
this.mParent = parent;
}
}

View File

@ -0,0 +1,14 @@
package com.fsck.k9.mail;
public class NoSuchProviderException extends MessagingException {
public static final long serialVersionUID = -1;
public NoSuchProviderException(String message) {
super(message);
}
public NoSuchProviderException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,31 @@
package com.fsck.k9.mail;
import java.io.IOException;
import java.io.OutputStream;
public interface Part {
public void addHeader(String name, String value) throws MessagingException;
public void removeHeader(String name) throws MessagingException;
public void setHeader(String name, String value) throws MessagingException;
public Body getBody() throws MessagingException;
public String getContentType() throws MessagingException;
public String getDisposition() throws MessagingException;
public String[] getHeader(String name) throws MessagingException;
public int getSize() throws MessagingException;
public boolean isMimeType(String mimeType) throws MessagingException;
public String getMimeType() throws MessagingException;
public void setBody(Body body) throws MessagingException;
public void writeTo(OutputStream out) throws IOException, MessagingException;
}

View File

@ -0,0 +1,76 @@
package com.fsck.k9.mail;
import java.util.HashMap;
import android.app.Application;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.Pop3Store;
/**
* Store is the access point for an email message store. It's location can be
* local or remote and no specific protocol is defined. Store is intended to
* loosely model in combination the JavaMail classes javax.mail.Store and
* javax.mail.Folder along with some additional functionality to improve
* performance on mobile devices. Implementations of this class should focus on
* making as few network connections as possible.
*/
public abstract class Store {
/**
* A global suggestion to Store implementors on how much of the body
* should be returned on FetchProfile.Item.BODY_SANE requests.
*/
public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (50 * 1024);
protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
protected static final int SOCKET_READ_TIMEOUT = 60000;
private static HashMap<String, Store> mStores = new HashMap<String, Store>();
/**
* Get an instance of a mail store. The URI is parsed as a standard URI and
* the scheme is used to determine which protocol will be used. The
* following schemes are currently recognized: imap - IMAP with no
* connection security. Ex: imap://username:password@host/ imap+tls - IMAP
* with TLS connection security, if the server supports it. Ex:
* imap+tls://username:password@host imap+tls+ - IMAP with required TLS
* connection security. Connection fails if TLS is not available. Ex:
* imap+tls+://username:password@host imap+ssl+ - IMAP with required SSL
* connection security. Connection fails if SSL is not available. Ex:
* imap+ssl+://username:password@host
*
* @param uri The URI of the store.
* @return
* @throws MessagingException
*/
public synchronized static Store getInstance(String uri, Application application) throws MessagingException {
Store store = mStores.get(uri);
if (store == null) {
if (uri.startsWith("imap")) {
store = new ImapStore(uri);
} else if (uri.startsWith("pop3")) {
store = new Pop3Store(uri);
} else if (uri.startsWith("local")) {
store = new LocalStore(uri, application);
}
if (store != null) {
mStores.put(uri, store);
}
}
if (store == null) {
throw new MessagingException("Unable to locate an applicable Store for " + uri);
}
return store;
}
public abstract Folder getFolder(String name) throws MessagingException;
public abstract Folder[] getPersonalNamespaces() throws MessagingException;
public abstract void checkSettings() throws MessagingException;
}

View File

@ -0,0 +1,22 @@
package com.fsck.k9.mail;
import com.fsck.k9.mail.transport.SmtpTransport;
public abstract class Transport {
protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
public synchronized static Transport getInstance(String uri) throws MessagingException {
if (uri.startsWith("smtp")) {
return new SmtpTransport(uri);
} else {
throw new MessagingException("Unable to locate an applicable Transport for " + uri);
}
}
public abstract void open() throws MessagingException;
public abstract void sendMessage(Message message) throws MessagingException;
public abstract void close() throws MessagingException;
}

View File

@ -0,0 +1,77 @@
package com.fsck.k9.mail.internet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.codec.binary.Base64OutputStream;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
/**
* A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
* the user to write to the temp file. After the write the body is available via getInputStream
* and writeTo one time. After writeTo is called, or the InputStream returned from
* getInputStream is closed the file is deleted and the Body should be considered disposed of.
*/
public class BinaryTempFileBody implements Body {
private static File mTempDirectory;
private File mFile;
public static void setTempDirectory(File tempDirectory) {
mTempDirectory = tempDirectory;
}
public BinaryTempFileBody() throws IOException {
if (mTempDirectory == null) {
throw new
RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!");
}
}
public OutputStream getOutputStream() throws IOException {
mFile = File.createTempFile("body", null, mTempDirectory);
mFile.deleteOnExit();
return new FileOutputStream(mFile);
}
public InputStream getInputStream() throws MessagingException {
try {
return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
}
catch (IOException ioe) {
throw new MessagingException("Unable to open body", ioe);
}
}
public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream();
Base64OutputStream base64Out = new Base64OutputStream(out);
IOUtils.copy(in, base64Out);
base64Out.close();
mFile.delete();
}
class BinaryTempFileBodyInputStream extends FilterInputStream {
public BinaryTempFileBodyInputStream(InputStream in) {
super(in);
}
@Override
public void close() throws IOException {
super.close();
mFile.delete();
}
}
}

View File

@ -0,0 +1,121 @@
package com.fsck.k9.mail.internet;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
/**
* TODO this is a close approximation of Message, need to update along with
* Message.
*/
public class MimeBodyPart extends BodyPart {
protected MimeHeader mHeader = new MimeHeader();
protected Body mBody;
protected int mSize;
public MimeBodyPart() throws MessagingException {
this(null);
}
public MimeBodyPart(Body body) throws MessagingException {
this(body, null);
}
public MimeBodyPart(Body body, String mimeType) throws MessagingException {
if (mimeType != null) {
setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
}
setBody(body);
}
protected String getFirstHeader(String name) throws MessagingException {
return mHeader.getFirstHeader(name);
}
public void addHeader(String name, String value) throws MessagingException {
mHeader.addHeader(name, value);
}
public void setHeader(String name, String value) throws MessagingException {
mHeader.setHeader(name, value);
}
public String[] getHeader(String name) throws MessagingException {
return mHeader.getHeader(name);
}
public void removeHeader(String name) throws MessagingException {
mHeader.removeHeader(name);
}
public Body getBody() throws MessagingException {
return mBody;
}
public void setBody(Body body) throws MessagingException {
this.mBody = body;
if (body instanceof com.fsck.k9.mail.Multipart) {
com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body);
multipart.setParent(this);
setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
}
else if (body instanceof TextBody) {
String contentType = String.format("%s;\n charset=utf-8", getMimeType());
String name = MimeUtility.getHeaderParameter(getContentType(), "name");
if (name != null) {
contentType += String.format(";\n name=\"%s\"", name);
}
setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
}
}
public String getContentType() throws MessagingException {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
if (contentType == null) {
return "text/plain";
} else {
return contentType;
}
}
public String getDisposition() throws MessagingException {
String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
if (contentDisposition == null) {
return null;
} else {
return contentDisposition;
}
}
public String getMimeType() throws MessagingException {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
public boolean isMimeType(String mimeType) throws MessagingException {
return getMimeType().equals(mimeType);
}
public int getSize() throws MessagingException {
return mSize;
}
/**
* Write the MimeMessage out in MIME format.
*/
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
mHeader.writeTo(out);
writer.write("\r\n");
writer.flush();
if (mBody != null) {
mBody.writeTo(out);
}
}
}

View File

@ -0,0 +1,105 @@
package com.fsck.k9.mail.internet;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import com.fsck.k9.Utility;
import com.fsck.k9.mail.MessagingException;
public class MimeHeader {
/**
* Application specific header that contains Store specific information about an attachment.
* In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
* retrieve the attachment at will from the server.
* The info is recorded from this header on LocalStore.appendMessages and is put back
* into the MIME data by LocalStore.fetch.
*/
public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
/**
* Fields that should be omitted when writing the header using writeTo()
*/
private static final String[] writeOmitFields = {
// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
// HEADER_ANDROID_ATTACHMENT_ID,
HEADER_ANDROID_ATTACHMENT_STORE_DATA
};
protected ArrayList<Field> mFields = new ArrayList<Field>();
public void clear() {
mFields.clear();
}
public String getFirstHeader(String name) throws MessagingException {
String[] header = getHeader(name);
if (header == null) {
return null;
}
return header[0];
}
public void addHeader(String name, String value) throws MessagingException {
mFields.add(new Field(name, MimeUtility.foldAndEncode(value)));
}
public void setHeader(String name, String value) throws MessagingException {
if (name == null || value == null) {
return;
}
removeHeader(name);
addHeader(name, value);
}
public String[] getHeader(String name) throws MessagingException {
ArrayList<String> values = new ArrayList<String>();
for (Field field : mFields) {
if (field.name.equalsIgnoreCase(name)) {
values.add(field.value);
}
}
if (values.size() == 0) {
return null;
}
return values.toArray(new String[] {});
}
public void removeHeader(String name) throws MessagingException {
ArrayList<Field> removeFields = new ArrayList<Field>();
for (Field field : mFields) {
if (field.name.equalsIgnoreCase(name)) {
removeFields.add(field);
}
}
mFields.removeAll(removeFields);
}
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) {
if (!Utility.arrayContains(writeOmitFields, field.name)) {
writer.write(field.name + ": " + field.value + "\r\n");
}
}
writer.flush();
}
class Field {
String name;
String value;
public Field(String name, String value) {
this.name = name;
this.value = value;
}
}
}

View File

@ -0,0 +1,424 @@
package com.fsck.k9.mail.internet;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Stack;
import org.apache.james.mime4j.BodyDescriptor;
import org.apache.james.mime4j.ContentHandler;
import org.apache.james.mime4j.EOLConvertingInputStream;
import org.apache.james.mime4j.MimeStreamParser;
import org.apache.james.mime4j.field.DateTimeField;
import org.apache.james.mime4j.field.Field;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
/**
* An implementation of Message that stores all of it's metadata in RFC 822 and
* RFC 2045 style headers.
*/
public class MimeMessage extends Message {
protected MimeHeader mHeader = new MimeHeader();
protected Address[] mFrom;
protected Address[] mTo;
protected Address[] mCc;
protected Address[] mBcc;
protected Address[] mReplyTo;
protected Date mSentDate;
protected SimpleDateFormat mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
protected Body mBody;
protected int mSize;
public MimeMessage() {
/*
* Every new messages gets a Message-ID
*/
try {
setHeader("Message-ID", generateMessageId());
}
catch (MessagingException me) {
throw new RuntimeException("Unable to create MimeMessage", me);
}
}
private String generateMessageId() {
StringBuffer sb = new StringBuffer();
sb.append("<");
for (int i = 0; i < 24; i++) {
sb.append(Integer.toString((int)(Math.random() * 35), 36));
}
sb.append(".");
sb.append(Long.toString(System.currentTimeMillis()));
sb.append("@email.android.com>");
return sb.toString();
}
/**
* Parse the given InputStream using Apache Mime4J to build a MimeMessage.
*
* @param in
* @throws IOException
* @throws MessagingException
*/
public MimeMessage(InputStream in) throws IOException, MessagingException {
parse(in);
}
protected void parse(InputStream in) throws IOException, MessagingException {
mHeader.clear();
mBody = null;
mBcc = null;
mTo = null;
mFrom = null;
mSentDate = null;
MimeStreamParser parser = new MimeStreamParser();
parser.setContentHandler(new MimeMessageBuilder());
parser.parse(new EOLConvertingInputStream(in));
}
public Date getReceivedDate() throws MessagingException {
return null;
}
public Date getSentDate() throws MessagingException {
if (mSentDate == null) {
try {
DateTimeField field = (DateTimeField)Field.parse("Date: "
+ MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
mSentDate = field.getDate();
} catch (Exception e) {
}
}
return mSentDate;
}
public void setSentDate(Date sentDate) throws MessagingException {
setHeader("Date", mDateFormat.format(sentDate));
this.mSentDate = sentDate;
}
public String getContentType() throws MessagingException {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
if (contentType == null) {
return "text/plain";
} else {
return contentType;
}
}
public String getDisposition() throws MessagingException {
String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
if (contentDisposition == null) {
return null;
} else {
return contentDisposition;
}
}
public String getMimeType() throws MessagingException {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
public int getSize() throws MessagingException {
return mSize;
}
/**
* Returns a list of the given recipient type from this message. If no addresses are
* found the method returns an empty array.
*/
public Address[] getRecipients(RecipientType type) throws MessagingException {
if (type == RecipientType.TO) {
if (mTo == null) {
mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
}
return mTo;
} else if (type == RecipientType.CC) {
if (mCc == null) {
mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
}
return mCc;
} else if (type == RecipientType.BCC) {
if (mBcc == null) {
mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
}
return mBcc;
} else {
throw new MessagingException("Unrecognized recipient type.");
}
}
public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
if (type == RecipientType.TO) {
if (addresses == null || addresses.length == 0) {
removeHeader("To");
this.mTo = null;
} else {
setHeader("To", Address.toString(addresses));
this.mTo = addresses;
}
} else if (type == RecipientType.CC) {
if (addresses == null || addresses.length == 0) {
removeHeader("CC");
this.mCc = null;
} else {
setHeader("CC", Address.toString(addresses));
this.mCc = addresses;
}
} else if (type == RecipientType.BCC) {
if (addresses == null || addresses.length == 0) {
removeHeader("BCC");
this.mBcc = null;
} else {
setHeader("BCC", Address.toString(addresses));
this.mBcc = addresses;
}
} else {
throw new MessagingException("Unrecognized recipient type.");
}
}
/**
* Returns the unfolded, decoded value of the Subject header.
*/
public String getSubject() throws MessagingException {
return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
}
public void setSubject(String subject) throws MessagingException {
setHeader("Subject", subject);
}
public Address[] getFrom() throws MessagingException {
if (mFrom == null) {
String list = MimeUtility.unfold(getFirstHeader("From"));
if (list == null || list.length() == 0) {
list = MimeUtility.unfold(getFirstHeader("Sender"));
}
mFrom = Address.parse(list);
}
return mFrom;
}
public void setFrom(Address from) throws MessagingException {
if (from != null) {
setHeader("From", from.toString());
this.mFrom = new Address[] {
from
};
} else {
this.mFrom = null;
}
}
public Address[] getReplyTo() throws MessagingException {
if (mReplyTo == null) {
mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
}
return mReplyTo;
}
public void setReplyTo(Address[] replyTo) throws MessagingException {
if (replyTo == null || replyTo.length == 0) {
removeHeader("Reply-to");
mReplyTo = null;
} else {
setHeader("Reply-to", Address.toString(replyTo));
mReplyTo = replyTo;
}
}
public void saveChanges() throws MessagingException {
throw new MessagingException("saveChanges not yet implemented");
}
public Body getBody() throws MessagingException {
return mBody;
}
public void setBody(Body body) throws MessagingException {
this.mBody = body;
if (body instanceof com.fsck.k9.mail.Multipart) {
com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body);
multipart.setParent(this);
setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
setHeader("MIME-Version", "1.0");
}
else if (body instanceof TextBody) {
setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
getMimeType()));
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
}
}
protected String getFirstHeader(String name) throws MessagingException {
return mHeader.getFirstHeader(name);
}
public void addHeader(String name, String value) throws MessagingException {
mHeader.addHeader(name, value);
}
public void setHeader(String name, String value) throws MessagingException {
mHeader.setHeader(name, value);
}
public String[] getHeader(String name) throws MessagingException {
return mHeader.getHeader(name);
}
public void removeHeader(String name) throws MessagingException {
mHeader.removeHeader(name);
}
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
mHeader.writeTo(out);
writer.write("\r\n");
writer.flush();
if (mBody != null) {
mBody.writeTo(out);
}
}
public InputStream getInputStream() throws MessagingException {
return null;
}
class MimeMessageBuilder implements ContentHandler {
private Stack stack = new Stack();
public MimeMessageBuilder() {
}
private void expect(Class c) {
if (!c.isInstance(stack.peek())) {
throw new IllegalStateException("Internal stack error: " + "Expected '"
+ c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
}
}
public void startMessage() {
if (stack.isEmpty()) {
stack.push(MimeMessage.this);
} else {
expect(Part.class);
try {
MimeMessage m = new MimeMessage();
((Part)stack.peek()).setBody(m);
stack.push(m);
} catch (MessagingException me) {
throw new Error(me);
}
}
}
public void endMessage() {
expect(MimeMessage.class);
stack.pop();
}
public void startHeader() {
expect(Part.class);
}
public void field(String fieldData) {
expect(Part.class);
try {
String[] tokens = fieldData.split(":", 2);
((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
} catch (MessagingException me) {
throw new Error(me);
}
}
public void endHeader() {
expect(Part.class);
}
public void startMultipart(BodyDescriptor bd) {
expect(Part.class);
Part e = (Part)stack.peek();
try {
MimeMultipart multiPart = new MimeMultipart(e.getContentType());
e.setBody(multiPart);
stack.push(multiPart);
} catch (MessagingException me) {
throw new Error(me);
}
}
public void body(BodyDescriptor bd, InputStream in) throws IOException {
expect(Part.class);
Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
try {
((Part)stack.peek()).setBody(body);
} catch (MessagingException me) {
throw new Error(me);
}
}
public void endMultipart() {
stack.pop();
}
public void startBodyPart() {
expect(MimeMultipart.class);
try {
MimeBodyPart bodyPart = new MimeBodyPart();
((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
stack.push(bodyPart);
} catch (MessagingException me) {
throw new Error(me);
}
}
public void endBodyPart() {
expect(BodyPart.class);
stack.pop();
}
public void epilogue(InputStream is) throws IOException {
expect(MimeMultipart.class);
StringBuffer sb = new StringBuffer();
int b;
while ((b = is.read()) != -1) {
sb.append((char)b);
}
// ((Multipart) stack.peek()).setEpilogue(sb.toString());
}
public void preamble(InputStream is) throws IOException {
expect(MimeMultipart.class);
StringBuffer sb = new StringBuffer();
int b;
while ((b = is.read()) != -1) {
sb.append((char)b);
}
try {
((MimeMultipart)stack.peek()).setPreamble(sb.toString());
} catch (MessagingException me) {
throw new Error(me);
}
}
public void raw(InputStream is) throws IOException {
throw new UnsupportedOperationException("Not supported");
}
}
}

View File

@ -0,0 +1,95 @@
package com.fsck.k9.mail.internet;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
public class MimeMultipart extends Multipart {
protected String mPreamble;
protected String mContentType;
protected String mBoundary;
protected String mSubType;
public MimeMultipart() throws MessagingException {
mBoundary = generateBoundary();
setSubType("mixed");
}
public MimeMultipart(String contentType) throws MessagingException {
this.mContentType = contentType;
try {
mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
if (mBoundary == null) {
throw new MessagingException("MultiPart does not contain boundary: " + contentType);
}
} catch (Exception e) {
throw new MessagingException(
"Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ contentType + ")", e);
}
}
public String generateBoundary() {
StringBuffer sb = new StringBuffer();
sb.append("----");
for (int i = 0; i < 30; i++) {
sb.append(Integer.toString((int)(Math.random() * 35), 36));
}
return sb.toString().toUpperCase();
}
public String getPreamble() throws MessagingException {
return mPreamble;
}
public void setPreamble(String preamble) throws MessagingException {
this.mPreamble = preamble;
}
public String getContentType() throws MessagingException {
return mContentType;
}
public void setSubType(String subType) throws MessagingException {
this.mSubType = subType;
mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
}
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
if (mPreamble != null) {
writer.write(mPreamble + "\r\n");
}
if(mParts.size() == 0){
writer.write("--" + mBoundary + "\r\n");
}
for (int i = 0, count = mParts.size(); i < count; i++) {
BodyPart bodyPart = (BodyPart)mParts.get(i);
writer.write("--" + mBoundary + "\r\n");
writer.flush();
bodyPart.writeTo(out);
writer.write("\r\n");
}
writer.write("--" + mBoundary + "--\r\n");
writer.flush();
}
public InputStream getInputStream() throws MessagingException {
return null;
}
}

View File

@ -0,0 +1,304 @@
package com.fsck.k9.mail.internet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.decoder.Base64InputStream;
import org.apache.james.mime4j.decoder.DecoderUtil;
import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
import org.apache.james.mime4j.util.CharsetUtil;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
public class MimeUtility {
public static String unfold(String s) {
if (s == null) {
return null;
}
return s.replaceAll("\r|\n", "");
}
public static String decode(String s) {
if (s == null) {
return null;
}
return DecoderUtil.decodeEncodedWords(s);
}
public static String unfoldAndDecode(String s) {
return decode(unfold(s));
}
// TODO implement proper foldAndEncode
public static String foldAndEncode(String s) {
return s;
}
/**
* Returns the named parameter of a header field. If name is null the first
* parameter is returned, or if there are no additional parameters in the
* field the entire field is returned. Otherwise the named parameter is
* searched for in a case insensitive fashion and returned. If the parameter
* cannot be found the method returns null.
*
* @param header
* @param name
* @return
*/
public static String getHeaderParameter(String header, String name) {
if (header == null) {
return null;
}
header = header.replaceAll("\r|\n", "");
String[] parts = header.split(";");
if (name == null) {
return parts[0];
}
for (String part : parts) {
if (part.trim().toLowerCase().startsWith(name.toLowerCase())) {
String parameter = part.split("=", 2)[1].trim();
if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
return parameter.substring(1, parameter.length() - 1);
}
else {
return parameter;
}
}
}
return null;
}
public static Part findFirstPartByMimeType(Part part, String mimeType)
throws MessagingException {
if (part.getBody() instanceof Multipart) {
Multipart multipart = (Multipart)part.getBody();
for (int i = 0, count = multipart.getCount(); i < count; i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
Part ret = findFirstPartByMimeType(bodyPart, mimeType);
if (ret != null) {
return ret;
}
}
}
else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
return part;
}
return null;
}
public static Part findPartByContentId(Part part, String contentId) throws Exception {
if (part.getBody() instanceof Multipart) {
Multipart multipart = (Multipart)part.getBody();
for (int i = 0, count = multipart.getCount(); i < count; i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
Part ret = findPartByContentId(bodyPart, contentId);
if (ret != null) {
return ret;
}
}
}
String[] header = part.getHeader("Content-ID");
if (header != null) {
for (String s : header) {
if (s.equals(contentId)) {
return part;
}
}
}
return null;
}
/**
* Reads the Part's body and returns a String based on any charset conversion that needed
* to be done.
* @param part
* @return
* @throws IOException
*/
public static String getTextFromPart(Part part) {
try {
if (part != null && part.getBody() != null) {
InputStream in = part.getBody().getInputStream();
String mimeType = part.getMimeType();
if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
/*
* Now we read the part into a buffer for further processing. Because
* the stream is now wrapped we'll remove any transfer encoding at this point.
*/
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(in, out);
byte[] bytes = out.toByteArray();
in.close();
out.close();
String charset = getHeaderParameter(part.getContentType(), "charset");
/*
* We've got a text part, so let's see if it needs to be processed further.
*/
if (charset != null) {
/*
* See if there is conversion from the MIME charset to the Java one.
*/
charset = CharsetUtil.toJavaCharset(charset);
}
if (charset != null) {
/*
* We've got a charset encoding, so decode using it.
*/
return new String(bytes, 0, bytes.length, charset);
}
else {
/*
* No encoding, so use us-ascii, which is the standard.
*/
return new String(bytes, 0, bytes.length, "ASCII");
}
}
}
}
catch (Exception e) {
/*
* If we are not able to process the body there's nothing we can do about it. Return
* null and let the upper layers handle the missing content.
*/
Log.e(k9.LOG_TAG, "Unable to getTextFromPart", e);
}
return null;
}
/**
* Returns true if the given mimeType matches the matchAgainst specification.
* @param mimeType A MIME type to check.
* @param matchAgainst A MIME type to check against. May include wildcards such as image/* or
* * /*.
* @return
*/
public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
return mimeType.matches(matchAgainst.replaceAll("\\*", "\\.\\*"));
}
/**
* Returns true if the given mimeType matches any of the matchAgainst specifications.
* @param mimeType A MIME type to check.
* @param matchAgainst An array of MIME types to check against. May include wildcards such
* as image/* or * /*.
* @return
*/
public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
for (String matchType : matchAgainst) {
if (mimeType.matches(matchType.replaceAll("\\*", "\\.\\*"))) {
return true;
}
}
return false;
}
/**
* Removes any content transfer encoding from the stream and returns a Body.
*/
public static Body decodeBody(InputStream in, String contentTransferEncoding)
throws IOException {
/*
* We'll remove any transfer encoding by wrapping the stream.
*/
if (contentTransferEncoding != null) {
contentTransferEncoding =
MimeUtility.getHeaderParameter(contentTransferEncoding, null);
if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
in = new QuotedPrintableInputStream(in);
}
else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
in = new Base64InputStream(in);
}
}
BinaryTempFileBody tempBody = new BinaryTempFileBody();
OutputStream out = tempBody.getOutputStream();
IOUtils.copy(in, out);
out.close();
return tempBody;
}
/**
* An unfortunately named method that makes decisions about a Part (usually a Message)
* as to which of it's children will be "viewable" and which will be attachments.
* The method recursively sorts the viewables and attachments into seperate
* lists for further processing.
* @param part
* @param viewables
* @param attachments
* @throws MessagingException
*/
public static void collectParts(Part part, ArrayList<Part> viewables,
ArrayList<Part> attachments) throws MessagingException {
String disposition = part.getDisposition();
String dispositionType = null;
String dispositionFilename = null;
if (disposition != null) {
dispositionType = MimeUtility.getHeaderParameter(disposition, null);
dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
}
/*
* A best guess that this part is intended to be an attachment and not inline.
*/
boolean attachment = ("attachment".equalsIgnoreCase(dispositionType))
|| (dispositionFilename != null)
&& (!"inline".equalsIgnoreCase(dispositionType));
/*
* If the part is Multipart but not alternative it's either mixed or
* something we don't know about, which means we treat it as mixed
* per the spec. We just process it's pieces recursively.
*/
if (part.getBody() instanceof Multipart) {
Multipart mp = (Multipart)part.getBody();
for (int i = 0; i < mp.getCount(); i++) {
collectParts(mp.getBodyPart(i), viewables, attachments);
}
}
/*
* If the part is an embedded message we just continue to process
* it, pulling any viewables or attachments into the running list.
*/
else if (part.getBody() instanceof Message) {
Message message = (Message)part.getBody();
collectParts(message, viewables, attachments);
}
/*
* If the part is HTML and it got this far it's part of a mixed (et
* al) and should be rendered inline.
*/
else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) {
viewables.add(part);
}
/*
* If the part is plain text and it got this far it's part of a
* mixed (et al) and should be rendered inline.
*/
else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) {
viewables.add(part);
}
/*
* Finally, if it's nothing else we will include it as an attachment.
*/
else {
attachments.add(part);
}
}
}

View File

@ -0,0 +1,47 @@
package com.fsck.k9.mail.internet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import com.fsck.k9.codec.binary.Base64;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
public class TextBody implements Body {
String mBody;
public TextBody(String body) {
this.mBody = body;
}
public void writeTo(OutputStream out) throws IOException, MessagingException {
byte[] bytes = mBody.getBytes("UTF-8");
out.write(Base64.encodeBase64Chunked(bytes));
}
/**
* Get the text of the body in it's unencoded format.
* @return
*/
public String getText() {
return mBody;
}
/**
* Returns an InputStream that reads this body's text in UTF-8 format.
*/
public InputStream getInputStream() throws MessagingException {
try {
byte[] b = mBody.getBytes("UTF-8");
return new ByteArrayInputStream(b);
}
catch (UnsupportedEncodingException usee) {
return null;
}
}
}

View File

@ -0,0 +1,356 @@
/**
*
*/
package com.fsck.k9.mail.store;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.FixedLengthInputStream;
import com.fsck.k9.PeekableInputStream;
import com.fsck.k9.mail.MessagingException;
public class ImapResponseParser {
SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z");
PeekableInputStream mIn;
InputStream mActiveLiteral;
public ImapResponseParser(PeekableInputStream in) {
this.mIn = in;
}
/**
* Reads the next response available on the stream and returns an
* ImapResponse object that represents it.
*
* @return
* @throws IOException
*/
public ImapResponse readResponse() throws IOException {
ImapResponse response = new ImapResponse();
if (mActiveLiteral != null) {
while (mActiveLiteral.read() != -1)
;
mActiveLiteral = null;
}
int ch = mIn.peek();
if (ch == '*') {
parseUntaggedResponse();
readTokens(response);
} else if (ch == '+') {
response.mCommandContinuationRequested =
parseCommandContinuationRequest();
readTokens(response);
} else {
response.mTag = parseTaggedResponse();
readTokens(response);
}
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, "<<< " + response.toString());
}
}
return response;
}
private void readTokens(ImapResponse response) throws IOException {
response.clear();
Object token;
while ((token = readToken()) != null) {
if (response != null) {
response.add(token);
}
if (mActiveLiteral != null) {
break;
}
}
response.mCompleted = token == null;
}
/**
* Reads the next token of the response. The token can be one of: String -
* for NIL, QUOTED, NUMBER, ATOM. InputStream - for LITERAL.
* InputStream.available() returns the total length of the stream.
* ImapResponseList - for PARENTHESIZED LIST. Can contain any of the above
* elements including List.
*
* @return The next token in the response or null if there are no more
* tokens.
* @throws IOException
*/
public Object readToken() throws IOException {
while (true) {
Object token = parseToken();
if (token == null || !token.equals(")")) {
return token;
}
}
}
private Object parseToken() throws IOException {
if (mActiveLiteral != null) {
while (mActiveLiteral.read() != -1)
;
mActiveLiteral = null;
}
while (true) {
int ch = mIn.peek();
if (ch == '(') {
return parseList();
} else if (ch == ')') {
expect(')');
return ")";
} else if (ch == '"') {
return parseQuoted();
} else if (ch == '{') {
mActiveLiteral = parseLiteral();
return mActiveLiteral;
} else if (ch == ' ') {
expect(' ');
} else if (ch == '\r') {
expect('\r');
expect('\n');
return null;
} else if (ch == '\n') {
expect('\n');
return null;
} else if (ch == '\t') {
expect('\t');
} else {
return parseAtom();
}
}
}
private boolean parseCommandContinuationRequest() throws IOException {
expect('+');
expect(' ');
return true;
}
// * OK [UIDNEXT 175] Predicted next UID
private void parseUntaggedResponse() throws IOException {
expect('*');
expect(' ');
}
// 3 OK [READ-WRITE] Select completed.
private String parseTaggedResponse() throws IOException {
String tag = readStringUntil(' ');
return tag;
}
private ImapList parseList() throws IOException {
expect('(');
ImapList list = new ImapList();
Object token;
while (true) {
token = parseToken();
if (token == null) {
break;
} else if (token instanceof InputStream) {
list.add(token);
break;
} else if (token.equals(")")) {
break;
} else {
list.add(token);
}
}
return list;
}
private String parseAtom() throws IOException {
StringBuffer sb = new StringBuffer();
int ch;
while (true) {
ch = mIn.peek();
if (ch == -1) {
throw new IOException("parseAtom(): end of stream reached");
} else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
// docs claim that flags are \ atom but atom isn't supposed to
// contain
// * and some falgs contain *
// ch == '%' || ch == '*' ||
ch == '%' ||
// TODO probably should not allow \ and should recognize
// it as a flag instead
// ch == '"' || ch == '\' ||
ch == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) {
if (sb.length() == 0) {
throw new IOException(String.format("parseAtom(): (%04x %c)", (int)ch, ch));
}
return sb.toString();
} else {
sb.append((char)mIn.read());
}
}
}
/**
* A { has been read, read the rest of the size string, the space and then
* notify the listener with an InputStream.
*
* @param mListener
* @throws IOException
*/
private InputStream parseLiteral() throws IOException {
expect('{');
int size = Integer.parseInt(readStringUntil('}'));
expect('\r');
expect('\n');
FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size);
return fixed;
}
/**
* A " has been read, read to the end of the quoted string and notify the
* listener.
*
* @param mListener
* @throws IOException
*/
private String parseQuoted() throws IOException {
expect('"');
return readStringUntil('"');
}
private String readStringUntil(char end) throws IOException {
StringBuffer sb = new StringBuffer();
int ch;
while ((ch = mIn.read()) != -1) {
if (ch == end) {
return sb.toString();
} else {
sb.append((char)ch);
}
}
throw new IOException("readQuotedString(): end of stream reached");
}
private int expect(char ch) throws IOException {
int d;
if ((d = mIn.read()) != ch) {
throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch,
ch, d, (char)d));
}
return d;
}
/**
* Represents an IMAP LIST response and is also the base class for the
* ImapResponse.
*/
public class ImapList extends ArrayList<Object> {
public ImapList getList(int index) {
return (ImapList)get(index);
}
public String getString(int index) {
return (String)get(index);
}
public InputStream getLiteral(int index) {
return (InputStream)get(index);
}
public int getNumber(int index) {
return Integer.parseInt(getString(index));
}
public Date getDate(int index) throws MessagingException {
try {
return mDateTimeFormat.parse(getString(index));
} catch (ParseException pe) {
throw new MessagingException("Unable to parse IMAP datetime", pe);
}
}
public Object getKeyedValue(Object key) {
for (int i = 0, count = size(); i < count; i++) {
if (get(i).equals(key)) {
return get(i + 1);
}
}
return null;
}
public ImapList getKeyedList(Object key) {
return (ImapList)getKeyedValue(key);
}
public String getKeyedString(Object key) {
return (String)getKeyedValue(key);
}
public InputStream getKeyedLiteral(Object key) {
return (InputStream)getKeyedValue(key);
}
public int getKeyedNumber(Object key) {
return Integer.parseInt(getKeyedString(key));
}
public Date getKeyedDate(Object key) throws MessagingException {
try {
String value = getKeyedString(key);
if (value == null) {
return null;
}
return mDateTimeFormat.parse(value);
} catch (ParseException pe) {
throw new MessagingException("Unable to parse IMAP datetime", pe);
}
}
}
/**
* Represents a single response from the IMAP server. Tagged responses will
* have a non-null tag. Untagged responses will have a null tag. The object
* will contain all of the available tokens at the time the response is
* received. In general, it will either contain all of the tokens of the
* response or all of the tokens up until the first LITERAL. If the object
* does not contain the entire response the caller must call more() to
* continue reading the response until more returns false.
*/
public class ImapResponse extends ImapList {
private boolean mCompleted;
boolean mCommandContinuationRequested;
String mTag;
public boolean more() throws IOException {
if (mCompleted) {
return false;
}
readTokens(this);
return true;
}
public String getAlertText() {
if (size() > 1 && "[ALERT]".equals(getString(1))) {
StringBuffer sb = new StringBuffer();
for (int i = 2, count = size(); i < count; i++) {
sb.append(get(i).toString());
sb.append(' ');
}
return sb.toString();
} else {
return null;
}
}
public String toString() {
return "#" + mTag + "# " + super.toString();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,880 @@
package com.fsck.k9.mail.store;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLException;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.Utility;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.Folder.OpenMode;
import com.fsck.k9.mail.internet.MimeMessage;
public class Pop3Store extends Store {
public static final int CONNECTION_SECURITY_NONE = 0;
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
private String mHost;
private int mPort;
private String mUsername;
private String mPassword;
private int mConnectionSecurity;
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
private Pop3Capabilities mCapabilities;
// /**
// * Detected latency, used for usage scaling.
// * Usage scaling occurs when it is neccesary to get information about
// * messages that could result in large data loads. This value allows
// * the code that loads this data to decide between using large downloads
// * (high latency) or multiple round trips (low latency) to accomplish
// * the same thing.
// * Default is Integer.MAX_VALUE implying massive latency so that the large
// * download method is used by default until latency data is collected.
// */
// private int mLatencyMs = Integer.MAX_VALUE;
//
// /**
// * Detected throughput, used for usage scaling.
// * Usage scaling occurs when it is neccesary to get information about
// * messages that could result in large data loads. This value allows
// * the code that loads this data to decide between using large downloads
// * (high latency) or multiple round trips (low latency) to accomplish
// * the same thing.
// * Default is Integer.MAX_VALUE implying massive bandwidth so that the
// * large download method is used by default until latency data is
// * collected.
// */
// private int mThroughputKbS = Integer.MAX_VALUE;
/**
* pop3://user:password@server:port CONNECTION_SECURITY_NONE
* pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
* pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
* pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
* pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
*
* @param _uri
*/
public Pop3Store(String _uri) throws MessagingException {
URI uri;
try {
uri = new URI(_uri);
} catch (URISyntaxException use) {
throw new MessagingException("Invalid Pop3Store URI", use);
}
String scheme = uri.getScheme();
if (scheme.equals("pop3")) {
mConnectionSecurity = CONNECTION_SECURITY_NONE;
mPort = 110;
} else if (scheme.equals("pop3+tls")) {
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
mPort = 110;
} else if (scheme.equals("pop3+tls+")) {
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
mPort = 110;
} else if (scheme.equals("pop3+ssl+")) {
mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
mPort = 995;
} else if (scheme.equals("pop3+ssl")) {
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
mPort = 995;
} else {
throw new MessagingException("Unsupported protocol");
}
mHost = uri.getHost();
if (uri.getPort() != -1) {
mPort = uri.getPort();
}
if (uri.getUserInfo() != null) {
String[] userInfoParts = uri.getUserInfo().split(":", 2);
mUsername = userInfoParts[0];
if (userInfoParts.length > 1) {
mPassword = userInfoParts[1];
}
}
}
@Override
public Folder getFolder(String name) throws MessagingException {
Folder folder = mFolders.get(name);
if (folder == null) {
folder = new Pop3Folder(name);
mFolders.put(folder.getName(), folder);
}
return folder;
}
@Override
public Folder[] getPersonalNamespaces() throws MessagingException {
return new Folder[] {
getFolder("INBOX"),
};
}
@Override
public void checkSettings() throws MessagingException {
Pop3Folder folder = new Pop3Folder("INBOX");
folder.open(OpenMode.READ_WRITE);
if (!mCapabilities.uidl) {
/*
* Run an additional test to see if UIDL is supported on the server. If it's not we
* can't service this account.
*/
try{
/*
* If the server doesn't support UIDL it will return a - response, which causes
* executeSimpleCommand to throw a MessagingException, exiting this method.
*/
folder.executeSimpleCommand("UIDL");
}
catch (IOException ioe) {
throw new MessagingException(null, ioe);
}
}
folder.close(false);
}
class Pop3Folder extends Folder {
private Socket mSocket;
private InputStream mIn;
private OutputStream mOut;
private HashMap<String, Pop3Message> mUidToMsgMap = new HashMap<String, Pop3Message>();
private HashMap<Integer, Pop3Message> mMsgNumToMsgMap = new HashMap<Integer, Pop3Message>();
private HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
private String mName;
private int mMessageCount;
public Pop3Folder(String name) {
this.mName = name;
if (mName.equalsIgnoreCase("INBOX")) {
mName = "INBOX";
}
}
@Override
public synchronized void open(OpenMode mode) throws MessagingException {
if (isOpen()) {
return;
}
if (!mName.equalsIgnoreCase("INBOX")) {
throw new MessagingException("Folder does not exist");
}
try {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
SSLContext sslContext = SSLContext.getInstance("TLS");
final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext.init(null, new TrustManager[] {
TrustManagerFactory.get(mHost, secure)
}, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} else {
mSocket = new Socket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
}
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
// Eat the banner
executeSimpleCommand(null);
mCapabilities = getCapabilities();
if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
|| mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
if (mCapabilities.stls) {
writeLine("STLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null, new TrustManager[] {
TrustManagerFactory.get(mHost, secure)
}, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort,
true);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
} else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
throw new MessagingException("TLS not supported but required");
}
}
try {
executeSimpleCommand("USER " + mUsername);
executeSimpleCommand("PASS " + mPassword);
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) {
throw new MessagingException(
"Unable to open connection to POP server due to security error.", gse);
} catch (IOException ioe) {
throw new MessagingException("Unable to open connection to POP server.", ioe);
}
try {
String response = executeSimpleCommand("STAT");
String[] parts = response.split(" ");
mMessageCount = Integer.parseInt(parts[1]);
}
catch (IOException ioe) {
throw new MessagingException("Unable to STAT folder.", ioe);
}
mUidToMsgMap.clear();
mMsgNumToMsgMap.clear();
mUidToMsgNumMap.clear();
}
public boolean isOpen() {
return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket
.isClosed());
}
@Override
public OpenMode getMode() throws MessagingException {
return OpenMode.READ_ONLY;
}
@Override
public void close(boolean expunge) {
try {
executeSimpleCommand("QUIT");
}
catch (Exception e) {
/*
* QUIT may fail if the connection is already closed. We don't care. It's just
* being friendly.
*/
}
try {
mIn.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
try {
mOut.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
try {
mSocket.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
mIn = null;
mOut = null;
mSocket = null;
}
@Override
public String getName() {
return mName;
}
@Override
public boolean create(FolderType type) throws MessagingException {
return false;
}
@Override
public boolean exists() throws MessagingException {
return mName.equalsIgnoreCase("INBOX");
}
@Override
public int getMessageCount() {
return mMessageCount;
}
@Override
public int getUnreadMessageCount() throws MessagingException {
return -1;
}
@Override
public Message getMessage(String uid) throws MessagingException {
Pop3Message message = mUidToMsgMap.get(uid);
if (message == null) {
message = new Pop3Message(uid, this);
}
return message;
}
@Override
public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
throws MessagingException {
if (start < 1 || end < 1 || end < start) {
throw new MessagingException(String.format("Invalid message set %d %d",
start, end));
}
try {
indexMsgNums(start, end);
} catch (IOException ioe) {
throw new MessagingException("getMessages", ioe);
}
ArrayList<Message> messages = new ArrayList<Message>();
int i = 0;
for (int msgNum = start; msgNum <= end; msgNum++) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
if (listener != null) {
listener.messageStarted(message.getUid(), i++, (end - start) + 1);
}
messages.add(message);
if (listener != null) {
listener.messageFinished(message, i++, (end - start) + 1);
}
}
return messages.toArray(new Message[messages.size()]);
}
/**
* Ensures that the given message set (from start to end inclusive)
* has been queried so that uids are available in the local cache.
* @param start
* @param end
* @throws MessagingException
* @throws IOException
*/
private void indexMsgNums(int start, int end)
throws MessagingException, IOException {
int unindexedMessageCount = 0;
for (int msgNum = start; msgNum <= end; msgNum++) {
if (mMsgNumToMsgMap.get(msgNum) == null) {
unindexedMessageCount++;
}
}
if (unindexedMessageCount == 0) {
return;
}
if (unindexedMessageCount < 50 && mMessageCount > 5000) {
/*
* In extreme cases we'll do a UIDL command per message instead of a bulk
* download.
*/
for (int msgNum = start; msgNum <= end; msgNum++) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
if (message == null) {
String response = executeSimpleCommand("UIDL " + msgNum);
int uidIndex = response.lastIndexOf(' ');
String msgUid = response.substring(uidIndex + 1);
message = new Pop3Message(msgUid, this);
indexMessage(msgNum, message);
}
}
}
else {
String response = executeSimpleCommand("UIDL");
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
String[] uidParts = response.split(" ");
Integer msgNum = Integer.valueOf(uidParts[0]);
String msgUid = uidParts[1];
if (msgNum >= start && msgNum <= end) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
if (message == null) {
message = new Pop3Message(msgUid, this);
indexMessage(msgNum, message);
}
}
}
}
}
private void indexUids(ArrayList<String> uids)
throws MessagingException, IOException {
HashSet<String> unindexedUids = new HashSet<String>();
for (String uid : uids) {
if (mUidToMsgMap.get(uid) == null) {
unindexedUids.add(uid);
}
}
if (unindexedUids.size() == 0) {
return;
}
/*
* If we are missing uids in the cache the only sure way to
* get them is to do a full UIDL list. A possible optimization
* would be trying UIDL for the latest X messages and praying.
*/
String response = executeSimpleCommand("UIDL");
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
String[] uidParts = response.split(" ");
Integer msgNum = Integer.valueOf(uidParts[0]);
String msgUid = uidParts[1];
if (unindexedUids.contains(msgUid)) {
if (Config.LOGD) {
Pop3Message message = mUidToMsgMap.get(msgUid);
if (message == null) {
message = new Pop3Message(msgUid, this);
}
indexMessage(msgNum, message);
}
}
}
}
private void indexMessage(int msgNum, Pop3Message message) {
mMsgNumToMsgMap.put(msgNum, message);
mUidToMsgMap.put(message.getUid(), message);
mUidToMsgNumMap.put(message.getUid(), msgNum);
}
@Override
public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
}
@Override
public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
throws MessagingException {
throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
}
/**
* Fetch the items contained in the FetchProfile into the given set of
* Messages in as efficient a manner as possible.
* @param messages
* @param fp
* @throws MessagingException
*/
public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
throws MessagingException {
if (messages == null || messages.length == 0) {
return;
}
ArrayList<String> uids = new ArrayList<String>();
for (Message message : messages) {
uids.add(message.getUid());
}
try {
indexUids(uids);
}
catch (IOException ioe) {
throw new MessagingException("fetch", ioe);
}
try {
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
/*
* We pass the listener only if there are other things to do in the
* FetchProfile. Since fetchEnvelop works in bulk and eveything else
* works one at a time if we let fetchEnvelope send events the
* event would get sent twice.
*/
fetchEnvelope(messages, fp.size() == 1 ? listener : null);
}
}
catch (IOException ioe) {
throw new MessagingException("fetch", ioe);
}
for (int i = 0, count = messages.length; i < count; i++) {
Message message = messages[i];
if (!(message instanceof Pop3Message)) {
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
}
Pop3Message pop3Message = (Pop3Message)message;
try {
if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) {
listener.messageStarted(pop3Message.getUid(), i, count);
}
if (fp.contains(FetchProfile.Item.BODY)) {
fetchBody(pop3Message, -1);
}
else if (fp.contains(FetchProfile.Item.BODY_SANE)) {
/*
* To convert the suggested download size we take the size
* divided by the maximum line size (76).
*/
fetchBody(pop3Message,
FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
}
else if (fp.contains(FetchProfile.Item.STRUCTURE)) {
/*
* If the user is requesting STRUCTURE we are required to set the body
* to null since we do not support the function.
*/
pop3Message.setBody(null);
}
if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) {
listener.messageFinished(message, i, count);
}
} catch (IOException ioe) {
throw new MessagingException("Unable to fetch message", ioe);
}
}
}
private void fetchEnvelope(Message[] messages,
MessageRetrievalListener listener) throws IOException, MessagingException {
int unsizedMessages = 0;
for (Message message : messages) {
if (message.getSize() == -1) {
unsizedMessages++;
}
}
if (unsizedMessages == 0) {
return;
}
if (unsizedMessages < 50 && mMessageCount > 5000) {
/*
* In extreme cases we'll do a command per message instead of a bulk request
* to hopefully save some time and bandwidth.
*/
for (int i = 0, count = messages.length; i < count; i++) {
Message message = messages[i];
if (!(message instanceof Pop3Message)) {
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
}
Pop3Message pop3Message = (Pop3Message)message;
if (listener != null) {
listener.messageStarted(pop3Message.getUid(), i, count);
}
String response = executeSimpleCommand(String.format("LIST %d",
mUidToMsgNumMap.get(pop3Message.getUid())));
String[] listParts = response.split(" ");
int msgNum = Integer.parseInt(listParts[1]);
int msgSize = Integer.parseInt(listParts[2]);
pop3Message.setSize(msgSize);
if (listener != null) {
listener.messageFinished(pop3Message, i, count);
}
}
}
else {
HashSet<String> msgUidIndex = new HashSet<String>();
for (Message message : messages) {
msgUidIndex.add(message.getUid());
}
int i = 0, count = messages.length;
String response = executeSimpleCommand("LIST");
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
String[] listParts = response.split(" ");
int msgNum = Integer.parseInt(listParts[0]);
int msgSize = Integer.parseInt(listParts[1]);
Pop3Message pop3Message = mMsgNumToMsgMap.get(msgNum);
if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
if (listener != null) {
listener.messageStarted(pop3Message.getUid(), i, count);
}
pop3Message.setSize(msgSize);
if (listener != null) {
listener.messageFinished(pop3Message, i, count);
}
i++;
}
}
}
}
/**
* Fetches the body of the given message, limiting the stored data
* to the specified number of lines. If lines is -1 the entire message
* is fetched. This is implemented with RETR for lines = -1 or TOP
* for any other value. If the server does not support TOP it is
* emulated with RETR and extra lines are thrown away.
* @param message
* @param lines
*/
private void fetchBody(Pop3Message message, int lines)
throws IOException, MessagingException {
String response = null;
if (lines == -1 || !mCapabilities.top) {
response = executeSimpleCommand(String.format("RETR %d",
mUidToMsgNumMap.get(message.getUid())));
}
else {
response = executeSimpleCommand(String.format("TOP %d %d",
mUidToMsgNumMap.get(message.getUid()),
lines));
}
if (response != null) {
try {
message.parse(new Pop3ResponseInputStream(mIn));
}
catch (MessagingException me) {
/*
* If we're only downloading headers it's possible
* we'll get a broken MIME message which we're not
* real worried about. If we've downloaded the body
* and can't parse it we need to let the user know.
*/
if (lines == -1) {
throw me;
}
}
}
}
@Override
public Flag[] getPermanentFlags() throws MessagingException {
return PERMANENT_FLAGS;
}
public void appendMessages(Message[] messages) throws MessagingException {
}
public void delete(boolean recurse) throws MessagingException {
}
public Message[] expunge() throws MessagingException {
return null;
}
public void setFlags(Message[] messages, Flag[] flags, boolean value)
throws MessagingException {
if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
/*
* The only flagging we support is setting the Deleted flag.
*/
return;
}
try {
for (Message message : messages) {
executeSimpleCommand(String.format("DELE %s",
mUidToMsgNumMap.get(message.getUid())));
}
}
catch (IOException ioe) {
throw new MessagingException("setFlags()", ioe);
}
}
@Override
public void copyMessages(Message[] msgs, Folder folder) throws MessagingException {
throw new UnsupportedOperationException("copyMessages is not supported in POP3");
}
// private boolean isRoundTripModeSuggested() {
// long roundTripMethodMs =
// (uncachedMessageCount * 2 * mLatencyMs);
// long bulkMethodMs =
// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000;
// }
private String readLine() throws IOException {
StringBuffer sb = new StringBuffer();
int d = mIn.read();
if (d == -1) {
throw new IOException("End of stream reached while trying to read line.");
}
do {
if (((char)d) == '\r') {
continue;
} else if (((char)d) == '\n') {
break;
} else {
sb.append((char)d);
}
} while ((d = mIn.read()) != -1);
String ret = sb.toString();
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, "<<< " + ret);
}
}
return ret;
}
private void writeLine(String s) throws IOException {
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, ">>> " + s);
}
}
mOut.write(s.getBytes());
mOut.write('\r');
mOut.write('\n');
mOut.flush();
}
private Pop3Capabilities getCapabilities() throws IOException, MessagingException {
Pop3Capabilities capabilities = new Pop3Capabilities();
try {
String response = executeSimpleCommand("CAPA");
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
if (response.equalsIgnoreCase("STLS")){
capabilities.stls = true;
}
else if (response.equalsIgnoreCase("UIDL")) {
capabilities.uidl = true;
}
else if (response.equalsIgnoreCase("PIPELINING")) {
capabilities.pipelining = true;
}
else if (response.equalsIgnoreCase("USER")) {
capabilities.user = true;
}
else if (response.equalsIgnoreCase("TOP")) {
capabilities.top = true;
}
}
}
catch (MessagingException me) {
/*
* The server may not support the CAPA command, so we just eat this Exception
* and allow the empty capabilities object to be returned.
*/
}
return capabilities;
}
private String executeSimpleCommand(String command) throws IOException, MessagingException {
open(OpenMode.READ_WRITE);
if (command != null) {
writeLine(command);
}
String response = readLine();
if (response.length() > 1 && response.charAt(0) == '-') {
throw new MessagingException(response);
}
return response;
}
@Override
public boolean equals(Object o) {
if (o instanceof Pop3Folder) {
return ((Pop3Folder) o).mName.equals(mName);
}
return super.equals(o);
}
}
class Pop3Message extends MimeMessage {
public Pop3Message(String uid, Pop3Folder folder) throws MessagingException {
mUid = uid;
mFolder = folder;
mSize = -1;
}
public void setSize(int size) {
mSize = size;
}
protected void parse(InputStream in) throws IOException, MessagingException {
super.parse(in);
}
@Override
public void setFlag(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
}
}
class Pop3Capabilities {
public boolean stls;
public boolean top;
public boolean user;
public boolean uidl;
public boolean pipelining;
public String toString() {
return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
stls,
top,
user,
uidl,
pipelining);
}
}
class Pop3ResponseInputStream extends InputStream {
InputStream mIn;
boolean mStartOfLine = true;
boolean mFinished;
public Pop3ResponseInputStream(InputStream in) {
mIn = in;
}
@Override
public int read() throws IOException {
if (mFinished) {
return -1;
}
int d = mIn.read();
if (mStartOfLine && d == '.') {
d = mIn.read();
if (d == '\r') {
mFinished = true;
mIn.read();
return -1;
}
}
mStartOfLine = (d == '\n');
return d;
}
}
}

View File

@ -0,0 +1,95 @@
package com.fsck.k9.mail.store;
import android.util.Log;
import android.net.http.DomainNameChecker;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateException;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.TrustManager;
public final class TrustManagerFactory {
private static final String LOG_TAG = "TrustManagerFactory";
private static X509TrustManager sSecureTrustManager;
private static X509TrustManager sUnsecureTrustManager;
private static class SimpleX509TrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
private static class SecureX509TrustManager implements X509TrustManager {
private X509TrustManager mTrustManager;
private String mHost;
SecureX509TrustManager(X509TrustManager trustManager, String host) {
mTrustManager = trustManager;
mHost = host;
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
mTrustManager.checkClientTrusted(chain, authType);
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
mTrustManager.checkServerTrusted(chain, authType);
if (!DomainNameChecker.match(chain[0], mHost)) {
throw new CertificateException("Certificate domain name does not match "
+ mHost);
}
}
public X509Certificate[] getAcceptedIssuers() {
return mTrustManager.getAcceptedIssuers();
}
}
static {
try {
javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
TrustManager[] tms = tmf.getTrustManagers();
if (tms != null) {
for (TrustManager tm : tms) {
if (tm instanceof X509TrustManager) {
sSecureTrustManager = (X509TrustManager) tm;
break;
}
}
}
} catch (NoSuchAlgorithmException e) {
Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
} catch (KeyStoreException e) {
Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
}
sUnsecureTrustManager = new SimpleX509TrustManager();
}
private TrustManagerFactory() {
}
public static X509TrustManager get(String host, boolean secure) {
return secure ? new SecureX509TrustManager(sSecureTrustManager, host) :
sUnsecureTrustManager;
}
}

View File

@ -0,0 +1,24 @@
package com.fsck.k9.mail.transport;
import java.io.IOException;
import java.io.OutputStream;
/**
* A simple OutputStream that does nothing but count how many bytes are written to it and
* makes that count available to callers.
*/
public class CountingOutputStream extends OutputStream {
private long mCount;
public CountingOutputStream() {
}
public long getCount() {
return mCount;
}
@Override
public void write(int oneByte) throws IOException {
mCount++;
}
}

View File

@ -0,0 +1,33 @@
package com.fsck.k9.mail.transport;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class EOLConvertingOutputStream extends FilterOutputStream {
int lastChar;
public EOLConvertingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int oneByte) throws IOException {
if (oneByte == '\n') {
if (lastChar != '\r') {
super.write('\r');
}
}
super.write(oneByte);
lastChar = oneByte;
}
@Override
public void flush() throws IOException {
if (lastChar == '\r') {
super.write('\n');
lastChar = '\n';
}
super.flush();
}
}

View File

@ -0,0 +1,376 @@
package com.fsck.k9.mail.transport;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLException;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.k9;
import com.fsck.k9.PeekableInputStream;
import com.fsck.k9.codec.binary.Base64;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.store.TrustManagerFactory;
public class SmtpTransport extends Transport {
public static final int CONNECTION_SECURITY_NONE = 0;
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
String mHost;
int mPort;
String mUsername;
String mPassword;
int mConnectionSecurity;
boolean mSecure;
Socket mSocket;
PeekableInputStream mIn;
OutputStream mOut;
/**
* smtp://user:password@server:port CONNECTION_SECURITY_NONE
* smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
* smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
* smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
* smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
*
* @param _uri
*/
public SmtpTransport(String _uri) throws MessagingException {
URI uri;
try {
uri = new URI(_uri);
} catch (URISyntaxException use) {
throw new MessagingException("Invalid SmtpTransport URI", use);
}
String scheme = uri.getScheme();
if (scheme.equals("smtp")) {
mConnectionSecurity = CONNECTION_SECURITY_NONE;
mPort = 25;
} else if (scheme.equals("smtp+tls")) {
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
mPort = 25;
} else if (scheme.equals("smtp+tls+")) {
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
mPort = 25;
} else if (scheme.equals("smtp+ssl+")) {
mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
mPort = 465;
} else if (scheme.equals("smtp+ssl")) {
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
mPort = 465;
} else {
throw new MessagingException("Unsupported protocol");
}
mHost = uri.getHost();
if (uri.getPort() != -1) {
mPort = uri.getPort();
}
if (uri.getUserInfo() != null) {
String[] userInfoParts = uri.getUserInfo().split(":", 2);
mUsername = userInfoParts[0];
if (userInfoParts.length > 1) {
mPassword = userInfoParts[1];
}
}
}
public void open() throws MessagingException {
try {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext.init(null, new TrustManager[] {
TrustManagerFactory.get(mHost, secure)
}, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
mSecure = true;
} else {
mSocket = new Socket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
}
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024));
mOut = mSocket.getOutputStream();
// Eat the banner
executeSimpleCommand(null);
String localHost = "localhost.localdomain";
try {
InetAddress localAddress = InetAddress.getLocalHost();
if (! localAddress.isLoopbackAddress()) {
// The loopback address will resolve to 'localhost'
// some mail servers only accept qualified hostnames, so make sure
// never to override "localhost.localdomain" with "localhost"
// TODO - this is a hack. but a better hack than what was there before
localHost = localAddress.getHostName();
}
} catch (Exception e) {
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, "Unable to look up localhost");
}
}
}
String result = executeSimpleCommand("EHLO " + localHost);
/*
* TODO may need to add code to fall back to HELO I switched it from
* using HELO on non STARTTLS connections because of AOL's mail
* server. It won't let you use AUTH without EHLO.
* We should really be paying more attention to the capabilities
* and only attempting auth if it's available, and warning the user
* if not.
*/
if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
|| mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
if (result.contains("-STARTTLS")) {
executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null, new TrustManager[] {
TrustManagerFactory.get(mHost, secure)
}, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort,
true);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
mOut = mSocket.getOutputStream();
mSecure = true;
/*
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
* Exim.
*/
result = executeSimpleCommand("EHLO " + localHost);
} else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
throw new MessagingException("TLS not supported but required");
}
}
/*
* result contains the results of the EHLO in concatenated form
*/
boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
if (mUsername != null && mUsername.length() > 0 && mPassword != null
&& mPassword.length() > 0) {
if (authPlainSupported) {
saslAuthPlain(mUsername, mPassword);
}
else if (authLoginSupported) {
saslAuthLogin(mUsername, mPassword);
}
else {
throw new MessagingException("No valid authentication mechanism found.");
}
}
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) {
throw new MessagingException(
"Unable to open connection to SMTP server due to security error.", gse);
} catch (IOException ioe) {
throw new MessagingException("Unable to open connection to SMTP server.", ioe);
}
}
public void sendMessage(Message message) throws MessagingException {
close();
open();
Address[] from = message.getFrom();
try {
executeSimpleCommand("MAIL FROM: " + "<" + from[0].getAddress() + ">");
for (Address address : message.getRecipients(RecipientType.TO)) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
for (Address address : message.getRecipients(RecipientType.CC)) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
for (Address address : message.getRecipients(RecipientType.BCC)) {
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
}
message.setRecipients(RecipientType.BCC, null);
executeSimpleCommand("DATA");
// TODO byte stuffing
message.writeTo(
new EOLConvertingOutputStream(
new BufferedOutputStream(mOut, 1024)));
executeSimpleCommand("\r\n.");
} catch (IOException ioe) {
throw new MessagingException("Unable to send message", ioe);
}
}
public void close() {
try {
mIn.close();
} catch (Exception e) {
}
try {
mOut.close();
} catch (Exception e) {
}
try {
mSocket.close();
} catch (Exception e) {
}
mIn = null;
mOut = null;
mSocket = null;
}
private String readLine() throws IOException {
StringBuffer sb = new StringBuffer();
int d;
while ((d = mIn.read()) != -1) {
if (((char)d) == '\r') {
continue;
} else if (((char)d) == '\n') {
break;
} else {
sb.append((char)d);
}
}
String ret = sb.toString();
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, "<<< " + ret);
}
}
return ret;
}
private void writeLine(String s) throws IOException {
if (Config.LOGD) {
if (k9.DEBUG) {
Log.d(k9.LOG_TAG, ">>> " + s);
}
}
mOut.write(s.getBytes());
mOut.write('\r');
mOut.write('\n');
mOut.flush();
}
private String executeSimpleCommand(String command) throws IOException, MessagingException {
if (command != null) {
writeLine(command);
}
String line = readLine();
String result = line;
while (line.length() >= 4 && line.charAt(3) == '-') {
line = readLine();
result += line.substring(3);
}
char c = result.charAt(0);
if ((c == '4') || (c == '5')) {
throw new MessagingException(result);
}
return result;
}
// C: AUTH LOGIN
// S: 334 VXNlcm5hbWU6
// C: d2VsZG9u
// S: 334 UGFzc3dvcmQ6
// C: dzNsZDBu
// S: 235 2.0.0 OK Authenticated
//
// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
//
//
// C: AUTH LOGIN
// S: 334 Username:
// C: weldon
// S: 334 Password:
// C: w3ld0n
// S: 235 2.0.0 OK Authenticated
private void saslAuthLogin(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
try {
executeSimpleCommand("AUTH LOGIN");
executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes())));
executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes())));
}
catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage()
+ ")");
}
throw me;
}
}
private void saslAuthPlain(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
byte[] data = ("\000" + username + "\000" + password).getBytes();
data = new Base64().encode(data);
try {
executeSimpleCommand("AUTH PLAIN " + new String(data));
}
catch (MessagingException me) {
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage()
+ ")");
}
throw me;
}
}
}

View File

@ -0,0 +1,29 @@
package com.fsck.k9.mail.transport;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import com.fsck.k9.k9;
import android.util.Config;
import android.util.Log;
public class StatusOutputStream extends FilterOutputStream {
private long mCount = 0;
public StatusOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int oneByte) throws IOException {
super.write(oneByte);
mCount++;
if (Config.LOGV) {
if (mCount % 1024 == 0) {
Log.v(k9.LOG_TAG, "# " + mCount);
}
}
}
}

View File

@ -0,0 +1,272 @@
package com.fsck.k9.provider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Config;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.Utility;
import com.fsck.k9.mail.internet.MimeUtility;
/*
* A simple ContentProvider that allows file access to Email's attachments.
*/
public class AttachmentProvider extends ContentProvider {
public static final Uri CONTENT_URI = Uri.parse( "content://com.fsck.k9.attachmentprovider");
private static final String FORMAT_RAW = "RAW";
private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
public static class AttachmentProviderColumns {
public static final String _ID = "_id";
public static final String DATA = "_data";
public static final String DISPLAY_NAME = "_display_name";
public static final String SIZE = "_size";
}
public static Uri getAttachmentUri(Account account, long id) {
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid() + ".db")
.appendPath(Long.toString(id))
.appendPath(FORMAT_RAW)
.build();
}
public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) {
return CONTENT_URI.buildUpon()
.appendPath(account.getUuid() + ".db")
.appendPath(Long.toString(id))
.appendPath(FORMAT_THUMBNAIL)
.appendPath(Integer.toString(width))
.appendPath(Integer.toString(height))
.build();
}
public static Uri getAttachmentUri(String db, long id) {
return CONTENT_URI.buildUpon()
.appendPath(db)
.appendPath(Long.toString(id))
.appendPath(FORMAT_RAW)
.build();
}
@Override
public boolean onCreate() {
/*
* We use the cache dir as a temporary directory (since Android doesn't give us one) so
* on startup we'll clean up any .tmp files from the last run.
*/
File[] files = getContext().getCacheDir().listFiles();
for (File file : files) {
if (file.getName().endsWith(".tmp")) {
file.delete();
}
}
return true;
}
@Override
public String getType(Uri uri) {
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String id = segments.get(1);
String format = segments.get(2);
if (FORMAT_THUMBNAIL.equals(format)) {
return "image/png";
}
else {
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
SQLiteDatabase db = null;
Cursor cursor = null;
try {
db = SQLiteDatabase.openDatabase(path, null, 0);
cursor = db.query(
"attachments",
new String[] { "mime_type" },
"id = ?",
new String[] { id },
null,
null,
null);
cursor.moveToFirst();
String type = cursor.getString(0);
cursor.close();
db.close();
return type;
}
finally {
if (cursor != null) {
cursor.close();
}
if (db != null) {
db.close();
}
}
}
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String id = segments.get(1);
String format = segments.get(2);
if (FORMAT_THUMBNAIL.equals(format)) {
int width = Integer.parseInt(segments.get(3));
int height = Integer.parseInt(segments.get(4));
String filename = "thmb_" + dbName + "_" + id;
File dir = getContext().getCacheDir();
File file = new File(dir, filename);
if (!file.exists()) {
Uri attachmentUri = getAttachmentUri(dbName, Long.parseLong(id));
String type = getType(attachmentUri);
try {
FileInputStream in = new FileInputStream(
new File(getContext().getDatabasePath(dbName + "_att"), id));
Bitmap thumbnail = createThumbnail(type, in);
thumbnail = thumbnail.createScaledBitmap(thumbnail, width, height, true);
FileOutputStream out = new FileOutputStream(file);
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
in.close();
}
catch (IOException ioe) {
return null;
}
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
else {
return ParcelFileDescriptor.open(
new File(getContext().getDatabasePath(dbName + "_att"), id),
ParcelFileDescriptor.MODE_READ_ONLY);
}
}
@Override
public int delete(Uri uri, String arg1, String[] arg2) {
return 0;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (projection == null) {
projection =
new String[] {
AttachmentProviderColumns._ID,
AttachmentProviderColumns.DATA,
};
}
List<String> segments = uri.getPathSegments();
String dbName = segments.get(0);
String id = segments.get(1);
String format = segments.get(2);
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
String name = null;
int size = -1;
SQLiteDatabase db = null;
Cursor cursor = null;
try {
db = SQLiteDatabase.openDatabase(path, null, 0);
cursor = db.query(
"attachments",
new String[] { "name", "size" },
"id = ?",
new String[] { id },
null,
null,
null);
if (!cursor.moveToFirst()) {
return null;
}
name = cursor.getString(0);
size = cursor.getInt(1);
}
finally {
if (cursor != null) {
cursor.close();
}
if (db != null) {
db.close();
}
}
MatrixCursor ret = new MatrixCursor(projection);
Object[] values = new Object[projection.length];
for (int i = 0, count = projection.length; i < count; i++) {
String column = projection[i];
if (AttachmentProviderColumns._ID.equals(column)) {
values[i] = id;
}
else if (AttachmentProviderColumns.DATA.equals(column)) {
values[i] = uri.toString();
}
else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
values[i] = name;
}
else if (AttachmentProviderColumns.SIZE.equals(column)) {
values[i] = size;
}
}
ret.addRow(values);
return ret;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
private Bitmap createThumbnail(String type, InputStream data) {
if(MimeUtility.mimeTypeMatches(type, "image/*")) {
return createImageThumbnail(data);
}
return null;
}
private Bitmap createImageThumbnail(InputStream data) {
try {
Bitmap bitmap = BitmapFactory.decodeStream(data);
return bitmap;
}
catch (OutOfMemoryError oome) {
/*
* Improperly downloaded images, corrupt bitmaps and the like can commonly
* cause OOME due to invalid allocation sizes. We're happy with a null bitmap in
* that case. If the system is really out of memory we'll know about it soon
* enough.
*/
return null;
}
catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,22 @@
package com.fsck.k9.service;
import com.fsck.k9.MessagingController;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class BootReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
MailService.actionReschedule(context);
}
else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
MailService.actionCancel(context);
}
else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
MailService.actionReschedule(context);
}
}
}

View File

@ -0,0 +1,193 @@
package com.fsck.k9.service;
import java.util.HashMap;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Config;
import android.util.Log;
import android.text.TextUtils;
import android.net.Uri;
import com.fsck.k9.Account;
import com.fsck.k9.k9;
import com.fsck.k9.MessagingController;
import com.fsck.k9.MessagingListener;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.Accounts;
import com.fsck.k9.activity.FolderMessageList;
/**
*/
public class MailService extends Service {
private static final String ACTION_CHECK_MAIL = "com.fsck.k9.intent.action.MAIL_SERVICE_WAKEUP";
private static final String ACTION_RESCHEDULE = "com.fsck.k9.intent.action.MAIL_SERVICE_RESCHEDULE";
private static final String ACTION_CANCEL = "com.fsck.k9.intent.action.MAIL_SERVICE_CANCEL";
private Listener mListener = new Listener();
private int mStartId;
public static void actionReschedule(Context context) {
Intent i = new Intent();
i.setClass(context, MailService.class);
i.setAction(MailService.ACTION_RESCHEDULE);
context.startService(i);
}
public static void actionCancel(Context context) {
Intent i = new Intent();
i.setClass(context, MailService.class);
i.setAction(MailService.ACTION_CANCEL);
context.startService(i);
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
this.mStartId = startId;
MessagingController.getInstance(getApplication()).addListener(mListener);
if (ACTION_CHECK_MAIL.equals(intent.getAction())) {
if (Config.LOGV) {
Log.v(k9.LOG_TAG, "***** MailService *****: checking mail");
}
MessagingController.getInstance(getApplication()).checkMail(this, null, mListener);
}
else if (ACTION_CANCEL.equals(intent.getAction())) {
if (Config.LOGV) {
Log.v(k9.LOG_TAG, "***** MailService *****: cancel");
}
cancel();
stopSelf(startId);
}
else if (ACTION_RESCHEDULE.equals(intent.getAction())) {
if (Config.LOGV) {
Log.v(k9.LOG_TAG, "***** MailService *****: reschedule");
}
reschedule();
stopSelf(startId);
}
}
@Override
public void onDestroy() {
super.onDestroy();
MessagingController.getInstance(getApplication()).removeListener(mListener);
}
private void cancel() {
AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent();
i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService");
i.setAction(ACTION_CHECK_MAIL);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
alarmMgr.cancel(pi);
}
private void reschedule() {
AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent();
i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService");
i.setAction(ACTION_CHECK_MAIL);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
int shortestInterval = -1;
for (Account account : Preferences.getPreferences(this).getAccounts()) {
if (account.getAutomaticCheckIntervalMinutes() != -1
&& (account.getAutomaticCheckIntervalMinutes() < shortestInterval || shortestInterval == -1)) {
shortestInterval = account.getAutomaticCheckIntervalMinutes();
}
}
if (shortestInterval == -1) {
alarmMgr.cancel(pi);
}
else {
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()
+ (shortestInterval * (60 * 1000)), pi);
}
}
public IBinder onBind(Intent intent) {
return null;
}
class Listener extends MessagingListener {
HashMap<Account, Integer> accountsWithNewMail = new HashMap<Account, Integer>();
@Override
public void checkMailStarted(Context context, Account account) {
accountsWithNewMail.clear();
}
@Override
public void checkMailFailed(Context context, Account account, String reason) {
reschedule();
stopSelf(mStartId);
}
@Override
public void synchronizeMailboxFinished(
Account account,
String folder,
int totalMessagesInMailbox,
int numNewMessages) {
if (account.isNotifyNewMail() && numNewMessages > 0) {
accountsWithNewMail.put(account, numNewMessages);
}
}
@Override
public void checkMailFinished(Context context, Account account) {
NotificationManager notifMgr = (NotificationManager)context
.getSystemService(Context.NOTIFICATION_SERVICE);
if (accountsWithNewMail.size() > 0) {
Notification notif = new Notification(R.drawable.stat_notify_email_generic,
getString(R.string.notification_new_title), System.currentTimeMillis());
boolean vibrate = false;
String ringtone = null;
if (accountsWithNewMail.size() > 1) {
for (Account account1 : accountsWithNewMail.keySet()) {
if (account1.isVibrate()) vibrate = true;
ringtone = account1.getRingtone();
}
Intent i = new Intent(context, Accounts.class);
PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
getString(R.string.notification_new_multi_account_fmt,
accountsWithNewMail.size()), pi);
} else {
Account account1 = accountsWithNewMail.keySet().iterator().next();
int totalNewMails = accountsWithNewMail.get(account1);
Intent i = FolderMessageList.actionHandleAccountIntent(context, account1, k9.INBOX);
PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
getString(R.string.notification_new_one_account_fmt, totalNewMails,
account1.getDescription()), pi);
vibrate = account1.isVibrate();
ringtone = account1.getRingtone();
}
notif.defaults = Notification.DEFAULT_LIGHTS;
notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone);
if (vibrate) {
notif.defaults |= Notification.DEFAULT_VIBRATE;
}
notifMgr.notify(1, notif);
}
reschedule();
stopSelf(mStartId);
}
}
}

View File

@ -0,0 +1,332 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
/**
* This class provides static utility methods for buffered
* copying between sources (<code>InputStream</code>, <code>Reader</code>,
* <code>String</code> and <code>byte[]</code>) and destinations
* (<code>OutputStream</code>, <code>Writer</code>, <code>String</code> and
* <code>byte[]</code>).
* <p>
* Unless otherwise noted, these <code>copy</code> methods do <em>not</em>
* flush or close the streams. Often doing so would require making non-portable
* assumptions about the streams' origin and further use. This means that both
* streams' <code>close()</code> methods must be called after copying. if one
* omits this step, then the stream resources (sockets, file descriptors) are
* released when the associated Stream is garbage-collected. It is not a good
* idea to rely on this mechanism. For a good overview of the distinction
* between "memory management" and "resource management", see
* <a href="http://www.unixreview.com/articles/1998/9804/9804ja/ja.htm">this
* UnixReview article</a>.
* <p>
* For byte-to-char methods, a <code>copy</code> variant allows the encoding
* to be selected (otherwise the platform default is used). We would like to
* encourage you to always specify the encoding because relying on the platform
* default can lead to unexpected results.
* <p
* We don't provide special variants for the <code>copy</code> methods that
* let you specify the buffer size because in modern VMs the impact on speed
* seems to be minimal. We're using a default buffer size of 4 KB.
* <p>
* The <code>copy</code> methods use an internal buffer when copying. It is
* therefore advisable <em>not</em> to deliberately wrap the stream arguments
* to the <code>copy</code> methods in <code>Buffered*</code> streams. For
* example, don't do the following:
* <pre>
* copy( new BufferedInputStream( in ), new BufferedOutputStream( out ) );
* </pre>
* The rationale is as follows:
* <p>
* Imagine that an InputStream's read() is a very expensive operation, which
* would usually suggest wrapping in a BufferedInputStream. The
* BufferedInputStream works by issuing infrequent
* {@link java.io.InputStream#read(byte[] b, int off, int len)} requests on the
* underlying InputStream, to fill an internal buffer, from which further
* <code>read</code> requests can inexpensively get their data (until the buffer
* runs out).
* <p>
* However, the <code>copy</code> methods do the same thing, keeping an
* internal buffer, populated by
* {@link InputStream#read(byte[] b, int off, int len)} requests. Having two
* buffers (or three if the destination stream is also buffered) is pointless,
* and the unnecessary buffer management hurts performance slightly (about 3%,
* according to some simple experiments).
* <p>
* Behold, intrepid explorers; a map of this class:
* <pre>
* Method Input Output Dependency
* ------ ----- ------ -------
* 1 copy InputStream OutputStream (primitive)
* 2 copy Reader Writer (primitive)
*
* 3 copy InputStream Writer 2
*
* 4 copy Reader OutputStream 2
*
* 5 copy String OutputStream 2
* 6 copy String Writer (trivial)
*
* 7 copy byte[] Writer 3
* 8 copy byte[] OutputStream (trivial)
* </pre>
* <p>
* Note that only the first two methods shuffle bytes; the rest use these
* two, or (if possible) copy using native Java copy methods. As there are
* method variants to specify the encoding, each row may
* correspond to up to 2 methods.
* <p>
* Origin of code: Excalibur.
*
* @author Peter Donald
* @author Jeff Turner
* @author Matthew Hawthorne
* @version $Id: CopyUtils.java 437680 2006-08-28 11:57:00Z scolebourne $
* @deprecated Use IOUtils. Will be removed in 2.0.
* Methods renamed to IOUtils.write() or IOUtils.copy().
* Null handling behaviour changed in IOUtils (null data does not
* throw NullPointerException).
*/
public class CopyUtils {
/**
* The default size of the buffer.
*/
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
/**
* Instances should NOT be constructed in standard programming.
*/
public CopyUtils() { }
// ----------------------------------------------------------------
// byte[] -> OutputStream
// ----------------------------------------------------------------
/**
* Copy bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
* @param input the byte array to read from
* @param output the <code>OutputStream</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(byte[] input, OutputStream output)
throws IOException {
output.write(input);
}
// ----------------------------------------------------------------
// byte[] -> Writer
// ----------------------------------------------------------------
/**
* Copy and convert bytes from a <code>byte[]</code> to chars on a
* <code>Writer</code>.
* The platform's default encoding is used for the byte-to-char conversion.
* @param input the byte array to read from
* @param output the <code>Writer</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(byte[] input, Writer output)
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(input);
copy(in, output);
}
/**
* Copy and convert bytes from a <code>byte[]</code> to chars on a
* <code>Writer</code>, using the specified encoding.
* @param input the byte array to read from
* @param output the <code>Writer</code> to write to
* @param encoding The name of a supported character encoding. See the
* <a href="http://www.iana.org/assignments/character-sets">IANA
* Charset Registry</a> for a list of valid encoding types.
* @throws IOException In case of an I/O problem
*/
public static void copy(
byte[] input,
Writer output,
String encoding)
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(input);
copy(in, output, encoding);
}
// ----------------------------------------------------------------
// Core copy methods
// ----------------------------------------------------------------
/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
* @param input the <code>InputStream</code> to read from
* @param output the <code>OutputStream</code> to write to
* @return the number of bytes copied
* @throws IOException In case of an I/O problem
*/
public static int copy(
InputStream input,
OutputStream output)
throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
// ----------------------------------------------------------------
// Reader -> Writer
// ----------------------------------------------------------------
/**
* Copy chars from a <code>Reader</code> to a <code>Writer</code>.
* @param input the <code>Reader</code> to read from
* @param output the <code>Writer</code> to write to
* @return the number of characters copied
* @throws IOException In case of an I/O problem
*/
public static int copy(
Reader input,
Writer output)
throws IOException {
char[] buffer = new char[DEFAULT_BUFFER_SIZE];
int count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
// ----------------------------------------------------------------
// InputStream -> Writer
// ----------------------------------------------------------------
/**
* Copy and convert bytes from an <code>InputStream</code> to chars on a
* <code>Writer</code>.
* The platform's default encoding is used for the byte-to-char conversion.
* @param input the <code>InputStream</code> to read from
* @param output the <code>Writer</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(
InputStream input,
Writer output)
throws IOException {
InputStreamReader in = new InputStreamReader(input);
copy(in, output);
}
/**
* Copy and convert bytes from an <code>InputStream</code> to chars on a
* <code>Writer</code>, using the specified encoding.
* @param input the <code>InputStream</code> to read from
* @param output the <code>Writer</code> to write to
* @param encoding The name of a supported character encoding. See the
* <a href="http://www.iana.org/assignments/character-sets">IANA
* Charset Registry</a> for a list of valid encoding types.
* @throws IOException In case of an I/O problem
*/
public static void copy(
InputStream input,
Writer output,
String encoding)
throws IOException {
InputStreamReader in = new InputStreamReader(input, encoding);
copy(in, output);
}
// ----------------------------------------------------------------
// Reader -> OutputStream
// ----------------------------------------------------------------
/**
* Serialize chars from a <code>Reader</code> to bytes on an
* <code>OutputStream</code>, and flush the <code>OutputStream</code>.
* @param input the <code>Reader</code> to read from
* @param output the <code>OutputStream</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(
Reader input,
OutputStream output)
throws IOException {
OutputStreamWriter out = new OutputStreamWriter(output);
copy(input, out);
// XXX Unless anyone is planning on rewriting OutputStreamWriter, we
// have to flush here.
out.flush();
}
// ----------------------------------------------------------------
// String -> OutputStream
// ----------------------------------------------------------------
/**
* Serialize chars from a <code>String</code> to bytes on an
* <code>OutputStream</code>, and
* flush the <code>OutputStream</code>.
* @param input the <code>String</code> to read from
* @param output the <code>OutputStream</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(
String input,
OutputStream output)
throws IOException {
StringReader in = new StringReader(input);
OutputStreamWriter out = new OutputStreamWriter(output);
copy(in, out);
// XXX Unless anyone is planning on rewriting OutputStreamWriter, we
// have to flush here.
out.flush();
}
// ----------------------------------------------------------------
// String -> Writer
// ----------------------------------------------------------------
/**
* Copy chars from a <code>String</code> to a <code>Writer</code>.
* @param input the <code>String</code> to read from
* @param output the <code>Writer</code> to write to
* @throws IOException In case of an I/O problem
*/
public static void copy(String input, Writer output)
throws IOException {
output.write(input);
}
}

View File

@ -0,0 +1,620 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Collection;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
/**
* Abstract class that walks through a directory hierarchy and provides
* subclasses with convenient hooks to add specific behaviour.
* <p>
* This class operates with a {@link FileFilter} and maximum depth to
* limit the files and direcories visited.
* Commons IO supplies many common filter implementations in the
* <a href="filefilter/package-summary.html"> filefilter</a> package.
* <p>
* The following sections describe:
* <ul>
* <li><a href="#example">1. Example Implementation</a> - example
* <code>FileCleaner</code> implementation.</li>
* <li><a href="#filter">2. Filter Example</a> - using
* {@link FileFilter}(s) with <code>DirectoryWalker</code>.</li>
* <li><a href="#cancel">3. Cancellation</a> - how to implement cancellation
* behaviour.</li>
* </ul>
*
* <a name="example"></a>
* <h3>1. Example Implementation</h3>
*
* There are many possible extensions, for example, to delete all
* files and '.svn' directories, and return a list of deleted files:
* <pre>
* public class FileCleaner extends DirectoryWalker {
*
* public FileCleaner() {
* super();
* }
*
* public List clean(File startDirectory) {
* List results = new ArrayList();
* walk(startDirectory, results);
* return results;
* }
*
* protected boolean handleDirectory(File directory, int depth, Collection results) {
* // delete svn directories and then skip
* if (".svn".equals(directory.getName())) {
* directory.delete();
* return false;
* } else {
* return true;
* }
*
* }
*
* protected void handleFile(File file, int depth, Collection results) {
* // delete file and add to list of deleted
* file.delete();
* results.add(file);
* }
* }
* </pre>
*
* <a name="filter"></a>
* <h3>2. Filter Example</h3>
*
* Choosing which directories and files to process can be a key aspect
* of using this class. This information can be setup in three ways,
* via three different constructors.
* <p>
* The first option is to visit all directories and files.
* This is achieved via the no-args constructor.
* <p>
* The second constructor option is to supply a single {@link FileFilter}
* that describes the files and directories to visit. Care must be taken
* with this option as the same filter is used for both directories
* and files.
* <p>
* For example, if you wanted all directories which are not hidden
* and files which end in ".txt":
* <pre>
* public class FooDirectoryWalker extends DirectoryWalker {
* public FooDirectoryWalker(FileFilter filter) {
* super(filter, -1);
* }
* }
*
* // Build up the filters and create the walker
* // Create a filter for Non-hidden directories
* IOFileFilter fooDirFilter =
* FileFilterUtils.andFileFilter(FileFilterUtils.directoryFileFilter,
* HiddenFileFilter.VISIBLE);
*
* // Create a filter for Files ending in ".txt"
* IOFileFilter fooFileFilter =
* FileFilterUtils.andFileFilter(FileFilterUtils.fileFileFilter,
* FileFilterUtils.suffixFileFilter(".txt"));
*
* // Combine the directory and file filters using an OR condition
* java.io.FileFilter fooFilter =
* FileFilterUtils.orFileFilter(fooDirFilter, fooFileFilter);
*
* // Use the filter to construct a DirectoryWalker implementation
* FooDirectoryWalker walker = new FooDirectoryWalker(fooFilter);
* </pre>
* <p>
* The third constructor option is to specify separate filters, one for
* directories and one for files. These are combined internally to form
* the correct <code>FileFilter</code>, something which is very easy to
* get wrong when attempted manually, particularly when trying to
* express constructs like 'any file in directories named docs'.
* <p>
* For example, if you wanted all directories which are not hidden
* and files which end in ".txt":
* <pre>
* public class FooDirectoryWalker extends DirectoryWalker {
* public FooDirectoryWalker(IOFileFilter dirFilter, IOFileFilter fileFilter) {
* super(dirFilter, fileFilter, -1);
* }
* }
*
* // Use the filters to construct the walker
* FooDirectoryWalker walker = new FooDirectoryWalker(
* HiddenFileFilter.VISIBLE,
* FileFilterUtils.suffixFileFilter(".txt"),
* );
* </pre>
* This is much simpler than the previous example, and is why it is the preferred
* option for filtering.
*
* <a name="cancel"></a>
* <h3>3. Cancellation</h3>
*
* The DirectoryWalker contains some of the logic required for cancel processing.
* Subclasses must complete the implementation.
* <p>
* What <code>DirectoryWalker</code> does provide for cancellation is:
* <ul>
* <li>{@link CancelException} which can be thrown in any of the
* <i>lifecycle</i> methods to stop processing.</li>
* <li>The <code>walk()</code> method traps thrown {@link CancelException}
* and calls the <code>handleCancelled()</code> method, providing
* a place for custom cancel processing.</li>
* </ul>
* <p>
* Implementations need to provide:
* <ul>
* <li>The decision logic on whether to cancel processing or not.</li>
* <li>Constructing and throwing a {@link CancelException}.</li>
* <li>Custom cancel processing in the <code>handleCancelled()</code> method.
* </ul>
* <p>
* Two possible scenarios are envisaged for cancellation:
* <ul>
* <li><a href="#external">3.1 External / Mult-threaded</a> - cancellation being
* decided/initiated by an external process.</li>
* <li><a href="#internal">3.2 Internal</a> - cancellation being decided/initiated
* from within a DirectoryWalker implementation.</li>
* </ul>
* <p>
* The following sections provide example implementations for these two different
* scenarios.
*
* <a name="external"></a>
* <h4>3.1 External / Multi-threaded</h4>
*
* This example provides a public <code>cancel()</code> method that can be
* called by another thread to stop the processing. A typical example use-case
* would be a cancel button on a GUI. Calling this method sets a
* <a href="http://java.sun.com/docs/books/jls/second_edition/html/classes.doc.html#36930">
* volatile</a> flag to ensure it will work properly in a multi-threaded environment.
* The flag is returned by the <code>handleIsCancelled()</code> method, which
* will cause the walk to stop immediately. The <code>handleCancelled()</code>
* method will be the next, and last, callback method received once cancellation
* has occurred.
*
* <pre>
* public class FooDirectoryWalker extends DirectoryWalker {
*
* private volatile boolean cancelled = false;
*
* public void cancel() {
* cancelled = true;
* }
*
* private void handleIsCancelled(File file, int depth, Collection results) {
* return cancelled;
* }
*
* protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) {
* // implement processing required when a cancellation occurs
* }
* }
* </pre>
*
* <a name="internal"></a>
* <h4>3.2 Internal</h4>
*
* This shows an example of how internal cancellation processing could be implemented.
* <b>Note</b> the decision logic and throwing a {@link CancelException} could be implemented
* in any of the <i>lifecycle</i> methods.
*
* <pre>
* public class BarDirectoryWalker extends DirectoryWalker {
*
* protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException {
* // cancel if hidden directory
* if (directory.isHidden()) {
* throw new CancelException(file, depth);
* }
* return true;
* }
*
* protected void handleFile(File file, int depth, Collection results) throws IOException {
* // cancel if read-only file
* if (!file.canWrite()) {
* throw new CancelException(file, depth);
* }
* results.add(file);
* }
*
* protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) {
* // implement processing required when a cancellation occurs
* }
* }
* </pre>
*
* @since Commons IO 1.3
* @version $Revision: 424748 $
*/
public abstract class DirectoryWalker {
/**
* The file filter to use to filter files and directories.
*/
private final FileFilter filter;
/**
* The limit on the directory depth to walk.
*/
private final int depthLimit;
/**
* Construct an instance with no filtering and unlimited <i>depth</i>.
*/
protected DirectoryWalker() {
this(null, -1);
}
/**
* Construct an instance with a filter and limit the <i>depth</i> navigated to.
* <p>
* The filter controls which files and directories will be navigated to as
* part of the walk. The {@link FileFilterUtils} class is useful for combining
* various filters together. A <code>null</code> filter means that no
* filtering should occur and all files and directories will be visited.
*
* @param filter the filter to apply, null means visit all files
* @param depthLimit controls how <i>deep</i> the hierarchy is
* navigated to (less than 0 means unlimited)
*/
protected DirectoryWalker(FileFilter filter, int depthLimit) {
this.filter = filter;
this.depthLimit = depthLimit;
}
/**
* Construct an instance with a directory and a file filter and an optional
* limit on the <i>depth</i> navigated to.
* <p>
* The filters control which files and directories will be navigated to as part
* of the walk. This constructor uses {@link FileFilterUtils#makeDirectoryOnly(IOFileFilter)}
* and {@link FileFilterUtils#makeFileOnly(IOFileFilter)} internally to combine the filters.
* A <code>null</code> filter means that no filtering should occur.
*
* @param directoryFilter the filter to apply to directories, null means visit all directories
* @param fileFilter the filter to apply to files, null means visit all files
* @param depthLimit controls how <i>deep</i> the hierarchy is
* navigated to (less than 0 means unlimited)
*/
protected DirectoryWalker(IOFileFilter directoryFilter, IOFileFilter fileFilter, int depthLimit) {
if (directoryFilter == null && fileFilter == null) {
this.filter = null;
} else {
directoryFilter = (directoryFilter != null ? directoryFilter : TrueFileFilter.TRUE);
fileFilter = (fileFilter != null ? fileFilter : TrueFileFilter.TRUE);
directoryFilter = FileFilterUtils.makeDirectoryOnly(directoryFilter);
fileFilter = FileFilterUtils.makeFileOnly(fileFilter);
this.filter = FileFilterUtils.orFileFilter(directoryFilter, fileFilter);
}
this.depthLimit = depthLimit;
}
//-----------------------------------------------------------------------
/**
* Internal method that walks the directory hierarchy in a depth-first manner.
* <p>
* Users of this class do not need to call this method. This method will
* be called automatically by another (public) method on the specific subclass.
* <p>
* Writers of subclasses should call this method to start the directory walk.
* Once called, this method will emit events as it walks the hierarchy.
* The event methods have the prefix <code>handle</code>.
*
* @param startDirectory the directory to start from, not null
* @param results the collection of result objects, may be updated
* @throws NullPointerException if the start directory is null
* @throws IOException if an I/O Error occurs
*/
protected final void walk(File startDirectory, Collection results) throws IOException {
if (startDirectory == null) {
throw new NullPointerException("Start Directory is null");
}
try {
handleStart(startDirectory, results);
walk(startDirectory, 0, results);
handleEnd(results);
} catch(CancelException cancel) {
handleCancelled(startDirectory, results, cancel);
}
}
/**
* Main recursive method to examine the directory hierarchy.
*
* @param directory the directory to examine, not null
* @param depth the directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
private void walk(File directory, int depth, Collection results) throws IOException {
checkIfCancelled(directory, depth, results);
if (handleDirectory(directory, depth, results)) {
handleDirectoryStart(directory, depth, results);
int childDepth = depth + 1;
if (depthLimit < 0 || childDepth <= depthLimit) {
checkIfCancelled(directory, depth, results);
File[] childFiles = (filter == null ? directory.listFiles() : directory.listFiles(filter));
if (childFiles == null) {
handleRestricted(directory, childDepth, results);
} else {
for (int i = 0; i < childFiles.length; i++) {
File childFile = childFiles[i];
if (childFile.isDirectory()) {
walk(childFile, childDepth, results);
} else {
checkIfCancelled(childFile, childDepth, results);
handleFile(childFile, childDepth, results);
checkIfCancelled(childFile, childDepth, results);
}
}
}
}
handleDirectoryEnd(directory, depth, results);
}
checkIfCancelled(directory, depth, results);
}
//-----------------------------------------------------------------------
/**
* Checks whether the walk has been cancelled by calling {@link #handleIsCancelled},
* throwing a <code>CancelException</code> if it has.
* <p>
* Writers of subclasses should not normally call this method as it is called
* automatically by the walk of the tree. However, sometimes a single method,
* typically {@link #handleFile}, may take a long time to run. In that case,
* you may wish to check for cancellation by calling this method.
*
* @param file the current file being processed
* @param depth the current file level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected final void checkIfCancelled(File file, int depth, Collection results) throws IOException {
if (handleIsCancelled(file, depth, results)) {
throw new CancelException(file, depth);
}
}
/**
* Overridable callback method invoked to determine if the entire walk
* operation should be immediately cancelled.
* <p>
* This method should be implemented by those subclasses that want to
* provide a public <code>cancel()</code> method available from another
* thread. The design pattern for the subclass should be as follows:
* <pre>
* public class FooDirectoryWalker extends DirectoryWalker {
* private volatile boolean cancelled = false;
*
* public void cancel() {
* cancelled = true;
* }
* private void handleIsCancelled(File file, int depth, Collection results) {
* return cancelled;
* }
* protected void handleCancelled(File startDirectory,
* Collection results, CancelException cancel) {
* // implement processing required when a cancellation occurs
* }
* }
* </pre>
* <p>
* If this method returns true, then the directory walk is immediately
* cancelled. The next callback method will be {@link #handleCancelled}.
* <p>
* This implementation returns false.
*
* @param file the file or directory being processed
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @return true if the walk has been cancelled
* @throws IOException if an I/O Error occurs
*/
protected boolean handleIsCancelled(
File file, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
return false; // not cancelled
}
/**
* Overridable callback method invoked when the operation is cancelled.
* The file being processed when the cancellation occurred can be
* obtained from the exception.
* <p>
* This implementation just re-throws the {@link CancelException}.
*
* @param startDirectory the directory that the walk started from
* @param results the collection of result objects, may be updated
* @param cancel the exception throw to cancel further processing
* containing details at the point of cancellation.
* @throws IOException if an I/O Error occurs
*/
protected void handleCancelled(File startDirectory, Collection results,
CancelException cancel) throws IOException {
// re-throw exception - overridable by subclass
throw cancel;
}
//-----------------------------------------------------------------------
/**
* Overridable callback method invoked at the start of processing.
* <p>
* This implementation does nothing.
*
* @param startDirectory the directory to start from
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleStart(File startDirectory, Collection results) throws IOException {
// do nothing - overridable by subclass
}
/**
* Overridable callback method invoked to determine if a directory should be processed.
* <p>
* This method returns a boolean to indicate if the directory should be examined or not.
* If you return false, the entire directory and any subdirectories will be skipped.
* Note that this functionality is in addition to the filtering by file filter.
* <p>
* This implementation does nothing and returns true.
*
* @param directory the current directory being processed
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @return true to process this directory, false to skip this directory
* @throws IOException if an I/O Error occurs
*/
protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
return true; // process directory
}
/**
* Overridable callback method invoked at the start of processing each directory.
* <p>
* This implementation does nothing.
*
* @param directory the current directory being processed
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleDirectoryStart(File directory, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
}
/**
* Overridable callback method invoked for each (non-directory) file.
* <p>
* This implementation does nothing.
*
* @param file the current file being processed
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleFile(File file, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
}
/**
* Overridable callback method invoked for each restricted directory.
* <p>
* This implementation does nothing.
*
* @param directory the restricted directory
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleRestricted(File directory, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
}
/**
* Overridable callback method invoked at the end of processing each directory.
* <p>
* This implementation does nothing.
*
* @param directory the directory being processed
* @param depth the current directory level (starting directory = 0)
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleDirectoryEnd(File directory, int depth, Collection results) throws IOException {
// do nothing - overridable by subclass
}
/**
* Overridable callback method invoked at the end of processing.
* <p>
* This implementation does nothing.
*
* @param results the collection of result objects, may be updated
* @throws IOException if an I/O Error occurs
*/
protected void handleEnd(Collection results) throws IOException {
// do nothing - overridable by subclass
}
//-----------------------------------------------------------------------
/**
* CancelException is thrown in DirectoryWalker to cancel the current
* processing.
*/
public static class CancelException extends IOException {
/** Serialization id. */
private static final long serialVersionUID = 1347339620135041008L;
/** The file being processed when the exception was thrown. */
private File file;
/** The file depth when the exception was thrown. */
private int depth = -1;
/**
* Constructs a <code>CancelException</code> with
* the file and depth when cancellation occurred.
*
* @param file the file when the operation was cancelled, may be null
* @param depth the depth when the operation was cancelled, may be null
*/
public CancelException(File file, int depth) {
this("Operation Cancelled", file, depth);
}
/**
* Constructs a <code>CancelException</code> with
* an appropriate message and the file and depth when
* cancellation occurred.
*
* @param message the detail message
* @param file the file when the operation was cancelled
* @param depth the depth when the operation was cancelled
*/
public CancelException(String message, File file, int depth) {
super(message);
this.file = file;
this.depth = depth;
}
/**
* Return the file when the operation was cancelled.
*
* @return the file when the operation was cancelled
*/
public File getFile() {
return file;
}
/**
* Return the depth when the operation was cancelled.
*
* @return the depth when the operation was cancelled
*/
public int getDepth() {
return depth;
}
}
}

View File

@ -0,0 +1,489 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Utility code for dealing with different endian systems.
* <p>
* Different computer architectures adopt different conventions for
* byte ordering. In so-called "Little Endian" architectures (eg Intel),
* the low-order byte is stored in memory at the lowest address, and
* subsequent bytes at higher addresses. For "Big Endian" architectures
* (eg Motorola), the situation is reversed.
* This class helps you solve this incompatability.
* <p>
* Origin of code: Excalibur
*
* @author <a href="mailto:peter@apache.org">Peter Donald</a>
* @version $Id: EndianUtils.java 539632 2007-05-18 23:37:59Z bayard $
* @see org.apache.commons.io.input.SwappedDataInputStream
*/
public class EndianUtils {
/**
* Instances should NOT be constructed in standard programming.
*/
public EndianUtils() {
super();
}
// ========================================== Swapping routines
/**
* Converts a "short" value between endian systems.
* @param value value to convert
* @return the converted value
*/
public static short swapShort(short value) {
return (short) ( ( ( ( value >> 0 ) & 0xff ) << 8 ) +
( ( ( value >> 8 ) & 0xff ) << 0 ) );
}
/**
* Converts a "int" value between endian systems.
* @param value value to convert
* @return the converted value
*/
public static int swapInteger(int value) {
return
( ( ( value >> 0 ) & 0xff ) << 24 ) +
( ( ( value >> 8 ) & 0xff ) << 16 ) +
( ( ( value >> 16 ) & 0xff ) << 8 ) +
( ( ( value >> 24 ) & 0xff ) << 0 );
}
/**
* Converts a "long" value between endian systems.
* @param value value to convert
* @return the converted value
*/
public static long swapLong(long value) {
return
( ( ( value >> 0 ) & 0xff ) << 56 ) +
( ( ( value >> 8 ) & 0xff ) << 48 ) +
( ( ( value >> 16 ) & 0xff ) << 40 ) +
( ( ( value >> 24 ) & 0xff ) << 32 ) +
( ( ( value >> 32 ) & 0xff ) << 24 ) +
( ( ( value >> 40 ) & 0xff ) << 16 ) +
( ( ( value >> 48 ) & 0xff ) << 8 ) +
( ( ( value >> 56 ) & 0xff ) << 0 );
}
/**
* Converts a "float" value between endian systems.
* @param value value to convert
* @return the converted value
*/
public static float swapFloat(float value) {
return Float.intBitsToFloat( swapInteger( Float.floatToIntBits( value ) ) );
}
/**
* Converts a "double" value between endian systems.
* @param value value to convert
* @return the converted value
*/
public static double swapDouble(double value) {
return Double.longBitsToDouble( swapLong( Double.doubleToLongBits( value ) ) );
}
// ========================================== Swapping read/write routines
/**
* Writes a "short" value to a byte array at a given offset. The value is
* converted to the opposed endian system while writing.
* @param data target byte array
* @param offset starting offset in the byte array
* @param value value to write
*/
public static void writeSwappedShort(byte[] data, int offset, short value) {
data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff );
data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff );
}
/**
* Reads a "short" value from a byte array at a given offset. The value is
* converted to the opposed endian system while reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static short readSwappedShort(byte[] data, int offset) {
return (short)( ( ( data[ offset + 0 ] & 0xff ) << 0 ) +
( ( data[ offset + 1 ] & 0xff ) << 8 ) );
}
/**
* Reads an unsigned short (16-bit) value from a byte array at a given
* offset. The value is converted to the opposed endian system while
* reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static int readSwappedUnsignedShort(byte[] data, int offset) {
return ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) +
( ( data[ offset + 1 ] & 0xff ) << 8 ) );
}
/**
* Writes a "int" value to a byte array at a given offset. The value is
* converted to the opposed endian system while writing.
* @param data target byte array
* @param offset starting offset in the byte array
* @param value value to write
*/
public static void writeSwappedInteger(byte[] data, int offset, int value) {
data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff );
data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff );
data[ offset + 2 ] = (byte)( ( value >> 16 ) & 0xff );
data[ offset + 3 ] = (byte)( ( value >> 24 ) & 0xff );
}
/**
* Reads a "int" value from a byte array at a given offset. The value is
* converted to the opposed endian system while reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static int readSwappedInteger(byte[] data, int offset) {
return ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) +
( ( data[ offset + 1 ] & 0xff ) << 8 ) +
( ( data[ offset + 2 ] & 0xff ) << 16 ) +
( ( data[ offset + 3 ] & 0xff ) << 24 ) );
}
/**
* Reads an unsigned integer (32-bit) value from a byte array at a given
* offset. The value is converted to the opposed endian system while
* reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static long readSwappedUnsignedInteger(byte[] data, int offset) {
long low = ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) +
( ( data[ offset + 1 ] & 0xff ) << 8 ) +
( ( data[ offset + 2 ] & 0xff ) << 16 ) );
long high = data[ offset + 3 ] & 0xff;
return (high << 24) + (0xffffffffL & low);
}
/**
* Writes a "long" value to a byte array at a given offset. The value is
* converted to the opposed endian system while writing.
* @param data target byte array
* @param offset starting offset in the byte array
* @param value value to write
*/
public static void writeSwappedLong(byte[] data, int offset, long value) {
data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff );
data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff );
data[ offset + 2 ] = (byte)( ( value >> 16 ) & 0xff );
data[ offset + 3 ] = (byte)( ( value >> 24 ) & 0xff );
data[ offset + 4 ] = (byte)( ( value >> 32 ) & 0xff );
data[ offset + 5 ] = (byte)( ( value >> 40 ) & 0xff );
data[ offset + 6 ] = (byte)( ( value >> 48 ) & 0xff );
data[ offset + 7 ] = (byte)( ( value >> 56 ) & 0xff );
}
/**
* Reads a "long" value from a byte array at a given offset. The value is
* converted to the opposed endian system while reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static long readSwappedLong(byte[] data, int offset) {
long low =
( ( data[ offset + 0 ] & 0xff ) << 0 ) +
( ( data[ offset + 1 ] & 0xff ) << 8 ) +
( ( data[ offset + 2 ] & 0xff ) << 16 ) +
( ( data[ offset + 3 ] & 0xff ) << 24 );
long high =
( ( data[ offset + 4 ] & 0xff ) << 0 ) +
( ( data[ offset + 5 ] & 0xff ) << 8 ) +
( ( data[ offset + 6 ] & 0xff ) << 16 ) +
( ( data[ offset + 7 ] & 0xff ) << 24 );
return (high << 32) + (0xffffffffL & low);
}
/**
* Writes a "float" value to a byte array at a given offset. The value is
* converted to the opposed endian system while writing.
* @param data target byte array
* @param offset starting offset in the byte array
* @param value value to write
*/
public static void writeSwappedFloat(byte[] data, int offset, float value) {
writeSwappedInteger( data, offset, Float.floatToIntBits( value ) );
}
/**
* Reads a "float" value from a byte array at a given offset. The value is
* converted to the opposed endian system while reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static float readSwappedFloat(byte[] data, int offset) {
return Float.intBitsToFloat( readSwappedInteger( data, offset ) );
}
/**
* Writes a "double" value to a byte array at a given offset. The value is
* converted to the opposed endian system while writing.
* @param data target byte array
* @param offset starting offset in the byte array
* @param value value to write
*/
public static void writeSwappedDouble(byte[] data, int offset, double value) {
writeSwappedLong( data, offset, Double.doubleToLongBits( value ) );
}
/**
* Reads a "double" value from a byte array at a given offset. The value is
* converted to the opposed endian system while reading.
* @param data source byte array
* @param offset starting offset in the byte array
* @return the value read
*/
public static double readSwappedDouble(byte[] data, int offset) {
return Double.longBitsToDouble( readSwappedLong( data, offset ) );
}
/**
* Writes a "short" value to an OutputStream. The value is
* converted to the opposed endian system while writing.
* @param output target OutputStream
* @param value value to write
* @throws IOException in case of an I/O problem
*/
public static void writeSwappedShort(OutputStream output, short value)
throws IOException
{
output.write( (byte)( ( value >> 0 ) & 0xff ) );
output.write( (byte)( ( value >> 8 ) & 0xff ) );
}
/**
* Reads a "short" value from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static short readSwappedShort(InputStream input)
throws IOException
{
return (short)( ( ( read( input ) & 0xff ) << 0 ) +
( ( read( input ) & 0xff ) << 8 ) );
}
/**
* Reads a unsigned short (16-bit) from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static int readSwappedUnsignedShort(InputStream input)
throws IOException
{
int value1 = read( input );
int value2 = read( input );
return ( ( ( value1 & 0xff ) << 0 ) +
( ( value2 & 0xff ) << 8 ) );
}
/**
* Writes a "int" value to an OutputStream. The value is
* converted to the opposed endian system while writing.
* @param output target OutputStream
* @param value value to write
* @throws IOException in case of an I/O problem
*/
public static void writeSwappedInteger(OutputStream output, int value)
throws IOException
{
output.write( (byte)( ( value >> 0 ) & 0xff ) );
output.write( (byte)( ( value >> 8 ) & 0xff ) );
output.write( (byte)( ( value >> 16 ) & 0xff ) );
output.write( (byte)( ( value >> 24 ) & 0xff ) );
}
/**
* Reads a "int" value from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static int readSwappedInteger(InputStream input)
throws IOException
{
int value1 = read( input );
int value2 = read( input );
int value3 = read( input );
int value4 = read( input );
return ( ( value1 & 0xff ) << 0 ) +
( ( value2 & 0xff ) << 8 ) +
( ( value3 & 0xff ) << 16 ) +
( ( value4 & 0xff ) << 24 );
}
/**
* Reads a unsigned integer (32-bit) from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static long readSwappedUnsignedInteger(InputStream input)
throws IOException
{
int value1 = read( input );
int value2 = read( input );
int value3 = read( input );
int value4 = read( input );
long low = ( ( ( value1 & 0xff ) << 0 ) +
( ( value2 & 0xff ) << 8 ) +
( ( value3 & 0xff ) << 16 ) );
long high = value4 & 0xff;
return (high << 24) + (0xffffffffL & low);
}
/**
* Writes a "long" value to an OutputStream. The value is
* converted to the opposed endian system while writing.
* @param output target OutputStream
* @param value value to write
* @throws IOException in case of an I/O problem
*/
public static void writeSwappedLong(OutputStream output, long value)
throws IOException
{
output.write( (byte)( ( value >> 0 ) & 0xff ) );
output.write( (byte)( ( value >> 8 ) & 0xff ) );
output.write( (byte)( ( value >> 16 ) & 0xff ) );
output.write( (byte)( ( value >> 24 ) & 0xff ) );
output.write( (byte)( ( value >> 32 ) & 0xff ) );
output.write( (byte)( ( value >> 40 ) & 0xff ) );
output.write( (byte)( ( value >> 48 ) & 0xff ) );
output.write( (byte)( ( value >> 56 ) & 0xff ) );
}
/**
* Reads a "long" value from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static long readSwappedLong(InputStream input)
throws IOException
{
byte[] bytes = new byte[8];
for ( int i=0; i<8; i++ ) {
bytes[i] = (byte) read( input );
}
return readSwappedLong( bytes, 0 );
}
/**
* Writes a "float" value to an OutputStream. The value is
* converted to the opposed endian system while writing.
* @param output target OutputStream
* @param value value to write
* @throws IOException in case of an I/O problem
*/
public static void writeSwappedFloat(OutputStream output, float value)
throws IOException
{
writeSwappedInteger( output, Float.floatToIntBits( value ) );
}
/**
* Reads a "float" value from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static float readSwappedFloat(InputStream input)
throws IOException
{
return Float.intBitsToFloat( readSwappedInteger( input ) );
}
/**
* Writes a "double" value to an OutputStream. The value is
* converted to the opposed endian system while writing.
* @param output target OutputStream
* @param value value to write
* @throws IOException in case of an I/O problem
*/
public static void writeSwappedDouble(OutputStream output, double value)
throws IOException
{
writeSwappedLong( output, Double.doubleToLongBits( value ) );
}
/**
* Reads a "double" value from an InputStream. The value is
* converted to the opposed endian system while reading.
* @param input source InputStream
* @return the value just read
* @throws IOException in case of an I/O problem
*/
public static double readSwappedDouble(InputStream input)
throws IOException
{
return Double.longBitsToDouble( readSwappedLong( input ) );
}
/**
* Reads the next byte from the input stream.
* @param input the stream
* @return the byte
* @throws IOException if the end of file is reached
*/
private static int read(InputStream input)
throws IOException
{
int value = input.read();
if( -1 == value ) {
throw new EOFException( "Unexpected EOF reached" );
}
return value;
}
}

View File

@ -0,0 +1,154 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.File;
/**
* Keeps track of files awaiting deletion, and deletes them when an associated
* marker object is reclaimed by the garbage collector.
* <p>
* This utility creates a background thread to handle file deletion.
* Each file to be deleted is registered with a handler object.
* When the handler object is garbage collected, the file is deleted.
* <p>
* In an environment with multiple class loaders (a servlet container, for
* example), you should consider stopping the background thread if it is no
* longer needed. This is done by invoking the method
* {@link #exitWhenFinished}, typically in
* {@link javax.servlet.ServletContextListener#contextDestroyed} or similar.
*
* @author Noel Bergman
* @author Martin Cooper
* @version $Id: FileCleaner.java 553012 2007-07-03 23:01:07Z ggregory $
* @deprecated Use {@link FileCleaningTracker}
*/
public class FileCleaner {
/**
* The instance to use for the deprecated, static methods.
*/
static final FileCleaningTracker theInstance = new FileCleaningTracker();
//-----------------------------------------------------------------------
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
*
* @param file the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @throws NullPointerException if the file is null
* @deprecated Use {@link FileCleaningTracker#track(File, Object)}.
*/
public static void track(File file, Object marker) {
theInstance.track(file, marker);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The speified deletion strategy is used.
*
* @param file the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @param deleteStrategy the strategy to delete the file, null means normal
* @throws NullPointerException if the file is null
* @deprecated Use {@link FileCleaningTracker#track(File, Object, FileDeleteStrategy)}.
*/
public static void track(File file, Object marker, FileDeleteStrategy deleteStrategy) {
theInstance.track(file, marker, deleteStrategy);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
*
* @param path the full path to the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @throws NullPointerException if the path is null
* @deprecated Use {@link FileCleaningTracker#track(String, Object)}.
*/
public static void track(String path, Object marker) {
theInstance.track(path, marker);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The speified deletion strategy is used.
*
* @param path the full path to the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @param deleteStrategy the strategy to delete the file, null means normal
* @throws NullPointerException if the path is null
* @deprecated Use {@link FileCleaningTracker#track(String, Object, FileDeleteStrategy)}.
*/
public static void track(String path, Object marker, FileDeleteStrategy deleteStrategy) {
theInstance.track(path, marker, deleteStrategy);
}
//-----------------------------------------------------------------------
/**
* Retrieve the number of files currently being tracked, and therefore
* awaiting deletion.
*
* @return the number of files being tracked
* @deprecated Use {@link FileCleaningTracker#getTrackCount()}.
*/
public static int getTrackCount() {
return theInstance.getTrackCount();
}
/**
* Call this method to cause the file cleaner thread to terminate when
* there are no more objects being tracked for deletion.
* <p>
* In a simple environment, you don't need this method as the file cleaner
* thread will simply exit when the JVM exits. In a more complex environment,
* with multiple class loaders (such as an application server), you should be
* aware that the file cleaner thread will continue running even if the class
* loader it was started from terminates. This can consitute a memory leak.
* <p>
* For example, suppose that you have developed a web application, which
* contains the commons-io jar file in your WEB-INF/lib directory. In other
* words, the FileCleaner class is loaded through the class loader of your
* web application. If the web application is terminated, but the servlet
* container is still running, then the file cleaner thread will still exist,
* posing a memory leak.
* <p>
* This method allows the thread to be terminated. Simply call this method
* in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}.
* One called, no new objects can be tracked by the file cleaner.
* @deprecated Use {@link FileCleaningTracker#exitWhenFinished()}.
*/
public static synchronized void exitWhenFinished() {
theInstance.exitWhenFinished();
}
/**
* Returns the singleton instance, which is used by the deprecated, static methods.
* This is mainly useful for code, which wants to support the new
* {@link FileCleaningTracker} class while maintain compatibility with the
* deprecated {@link FileCleaner}.
*
* @return the singleton instance
*/
public static FileCleaningTracker getInstance() {
return theInstance;
}
}

View File

@ -0,0 +1,258 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.File;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.Collection;
import java.util.Vector;
/**
* Keeps track of files awaiting deletion, and deletes them when an associated
* marker object is reclaimed by the garbage collector.
* <p>
* This utility creates a background thread to handle file deletion.
* Each file to be deleted is registered with a handler object.
* When the handler object is garbage collected, the file is deleted.
* <p>
* In an environment with multiple class loaders (a servlet container, for
* example), you should consider stopping the background thread if it is no
* longer needed. This is done by invoking the method
* {@link #exitWhenFinished}, typically in
* {@link javax.servlet.ServletContextListener#contextDestroyed} or similar.
*
* @author Noel Bergman
* @author Martin Cooper
* @version $Id: FileCleaner.java 490987 2006-12-29 12:11:48Z scolebourne $
*/
public class FileCleaningTracker {
/**
* Queue of <code>Tracker</code> instances being watched.
*/
ReferenceQueue /* Tracker */ q = new ReferenceQueue();
/**
* Collection of <code>Tracker</code> instances in existence.
*/
final Collection /* Tracker */ trackers = new Vector(); // synchronized
/**
* Whether to terminate the thread when the tracking is complete.
*/
volatile boolean exitWhenFinished = false;
/**
* The thread that will clean up registered files.
*/
Thread reaper;
//-----------------------------------------------------------------------
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
*
* @param file the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @throws NullPointerException if the file is null
*/
public void track(File file, Object marker) {
track(file, marker, (FileDeleteStrategy) null);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The speified deletion strategy is used.
*
* @param file the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @param deleteStrategy the strategy to delete the file, null means normal
* @throws NullPointerException if the file is null
*/
public void track(File file, Object marker, FileDeleteStrategy deleteStrategy) {
if (file == null) {
throw new NullPointerException("The file must not be null");
}
addTracker(file.getPath(), marker, deleteStrategy);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
*
* @param path the full path to the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @throws NullPointerException if the path is null
*/
public void track(String path, Object marker) {
track(path, marker, (FileDeleteStrategy) null);
}
/**
* Track the specified file, using the provided marker, deleting the file
* when the marker instance is garbage collected.
* The speified deletion strategy is used.
*
* @param path the full path to the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @param deleteStrategy the strategy to delete the file, null means normal
* @throws NullPointerException if the path is null
*/
public void track(String path, Object marker, FileDeleteStrategy deleteStrategy) {
if (path == null) {
throw new NullPointerException("The path must not be null");
}
addTracker(path, marker, deleteStrategy);
}
/**
* Adds a tracker to the list of trackers.
*
* @param path the full path to the file to be tracked, not null
* @param marker the marker object used to track the file, not null
* @param deleteStrategy the strategy to delete the file, null means normal
*/
private synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) {
// synchronized block protects reaper
if (exitWhenFinished) {
throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
}
if (reaper == null) {
reaper = new Reaper();
reaper.start();
}
trackers.add(new Tracker(path, deleteStrategy, marker, q));
}
//-----------------------------------------------------------------------
/**
* Retrieve the number of files currently being tracked, and therefore
* awaiting deletion.
*
* @return the number of files being tracked
*/
public int getTrackCount() {
return trackers.size();
}
/**
* Call this method to cause the file cleaner thread to terminate when
* there are no more objects being tracked for deletion.
* <p>
* In a simple environment, you don't need this method as the file cleaner
* thread will simply exit when the JVM exits. In a more complex environment,
* with multiple class loaders (such as an application server), you should be
* aware that the file cleaner thread will continue running even if the class
* loader it was started from terminates. This can consitute a memory leak.
* <p>
* For example, suppose that you have developed a web application, which
* contains the commons-io jar file in your WEB-INF/lib directory. In other
* words, the FileCleaner class is loaded through the class loader of your
* web application. If the web application is terminated, but the servlet
* container is still running, then the file cleaner thread will still exist,
* posing a memory leak.
* <p>
* This method allows the thread to be terminated. Simply call this method
* in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}.
* One called, no new objects can be tracked by the file cleaner.
*/
public synchronized void exitWhenFinished() {
// synchronized block protects reaper
exitWhenFinished = true;
if (reaper != null) {
synchronized (reaper) {
reaper.interrupt();
}
}
}
//-----------------------------------------------------------------------
/**
* The reaper thread.
*/
private final class Reaper extends Thread {
/** Construct a new Reaper */
Reaper() {
super("File Reaper");
setPriority(Thread.MAX_PRIORITY);
setDaemon(true);
}
/**
* Run the reaper thread that will delete files as their associated
* marker objects are reclaimed by the garbage collector.
*/
public void run() {
// thread exits when exitWhenFinished is true and there are no more tracked objects
while (exitWhenFinished == false || trackers.size() > 0) {
Tracker tracker = null;
try {
// Wait for a tracker to remove.
tracker = (Tracker) q.remove();
} catch (Exception e) {
continue;
}
if (tracker != null) {
tracker.delete();
tracker.clear();
trackers.remove(tracker);
}
}
}
}
//-----------------------------------------------------------------------
/**
* Inner class which acts as the reference for a file pending deletion.
*/
private static final class Tracker extends PhantomReference {
/**
* The full path to the file being tracked.
*/
private final String path;
/**
* The strategy for deleting files.
*/
private final FileDeleteStrategy deleteStrategy;
/**
* Constructs an instance of this class from the supplied parameters.
*
* @param path the full path to the file to be tracked, not null
* @param deleteStrategy the strategy to delete the file, null means normal
* @param marker the marker object used to track the file, not null
* @param queue the queue on to which the tracker will be pushed, not null
*/
Tracker(String path, FileDeleteStrategy deleteStrategy, Object marker, ReferenceQueue queue) {
super(marker, queue);
this.path = path;
this.deleteStrategy = (deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy);
}
/**
* Deletes the file associated with this tracker instance.
*
* @return <code>true</code> if the file was deleted successfully;
* <code>false</code> otherwise.
*/
public boolean delete() {
return deleteStrategy.deleteQuietly(new File(path));
}
}
}

View File

@ -0,0 +1,156 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.File;
import java.io.IOException;
/**
* Strategy for deleting files.
* <p>
* There is more than one way to delete a file.
* You may want to limit access to certain directories, to only delete
* directories if they are empty, or maybe to force deletion.
* <p>
* This class captures the strategy to use and is designed for user subclassing.
*
* @author Stephen Colebourne
* @version $Id: FileDeleteStrategy.java 453903 2006-10-07 13:47:06Z scolebourne $
* @since Commons IO 1.3
*/
public class FileDeleteStrategy {
/**
* The singleton instance for normal file deletion, which does not permit
* the deletion of directories that are not empty.
*/
public static final FileDeleteStrategy NORMAL = new FileDeleteStrategy("Normal");
/**
* The singleton instance for forced file deletion, which always deletes,
* even if the file represents a non-empty directory.
*/
public static final FileDeleteStrategy FORCE = new ForceFileDeleteStrategy();
/** The name of the strategy. */
private final String name;
//-----------------------------------------------------------------------
/**
* Restricted constructor.
*
* @param name the name by which the strategy is known
*/
protected FileDeleteStrategy(String name) {
this.name = name;
}
//-----------------------------------------------------------------------
/**
* Deletes the file object, which may be a file or a directory.
* All <code>IOException</code>s are caught and false returned instead.
* If the file does not exist or is null, true is returned.
* <p>
* Subclass writers should override {@link #doDelete(File)}, not this method.
*
* @param fileToDelete the file to delete, null returns true
* @return true if the file was deleted, or there was no such file
*/
public boolean deleteQuietly(File fileToDelete) {
if (fileToDelete == null || fileToDelete.exists() == false) {
return true;
}
try {
return doDelete(fileToDelete);
} catch (IOException ex) {
return false;
}
}
/**
* Deletes the file object, which may be a file or a directory.
* If the file does not exist, the method just returns.
* <p>
* Subclass writers should override {@link #doDelete(File)}, not this method.
*
* @param fileToDelete the file to delete, not null
* @throws NullPointerException if the file is null
* @throws IOException if an error occurs during file deletion
*/
public void delete(File fileToDelete) throws IOException {
if (fileToDelete.exists() && doDelete(fileToDelete) == false) {
throw new IOException("Deletion failed: " + fileToDelete);
}
}
/**
* Actually deletes the file object, which may be a file or a directory.
* <p>
* This method is designed for subclasses to override.
* The implementation may return either false or an <code>IOException</code>
* when deletion fails. The {@link #delete(File)} and {@link #deleteQuietly(File)}
* methods will handle either response appropriately.
* A check has been made to ensure that the file will exist.
* <p>
* This implementation uses {@link File#delete()}.
*
* @param fileToDelete the file to delete, exists, not null
* @return true if the file was deleteds
* @throws NullPointerException if the file is null
* @throws IOException if an error occurs during file deletion
*/
protected boolean doDelete(File fileToDelete) throws IOException {
return fileToDelete.delete();
}
//-----------------------------------------------------------------------
/**
* Gets a string describing the delete strategy.
*
* @return a string describing the delete strategy
*/
public String toString() {
return "FileDeleteStrategy[" + name + "]";
}
//-----------------------------------------------------------------------
/**
* Force file deletion strategy.
*/
static class ForceFileDeleteStrategy extends FileDeleteStrategy {
/** Default Constructor */
ForceFileDeleteStrategy() {
super("Force");
}
/**
* Deletes the file object.
* <p>
* This implementation uses <code>FileUtils.forceDelete() <code>
* if the file exists.
*
* @param fileToDelete the file to delete, not null
* @return Always returns <code>true</code>
* @throws NullPointerException if the file is null
* @throws IOException if an error occurs during file deletion
*/
protected boolean doDelete(File fileToDelete) throws IOException {
FileUtils.forceDelete(fileToDelete);
return true;
}
}
}

View File

@ -0,0 +1,457 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
/**
* General File System utilities.
* <p>
* This class provides static utility methods for general file system
* functions not provided via the JDK {@link java.io.File File} class.
* <p>
* The current functions provided are:
* <ul>
* <li>Get the free space on a drive
* </ul>
*
* @author Frank W. Zammetti
* @author Stephen Colebourne
* @author Thomas Ledoux
* @author James Urie
* @author Magnus Grimsell
* @author Thomas Ledoux
* @version $Id: FileSystemUtils.java 453889 2006-10-07 11:56:25Z scolebourne $
* @since Commons IO 1.1
*/
public class FileSystemUtils {
/** Singleton instance, used mainly for testing. */
private static final FileSystemUtils INSTANCE = new FileSystemUtils();
/** Operating system state flag for error. */
private static final int INIT_PROBLEM = -1;
/** Operating system state flag for neither Unix nor Windows. */
private static final int OTHER = 0;
/** Operating system state flag for Windows. */
private static final int WINDOWS = 1;
/** Operating system state flag for Unix. */
private static final int UNIX = 2;
/** Operating system state flag for Posix flavour Unix. */
private static final int POSIX_UNIX = 3;
/** The operating system flag. */
private static final int OS;
static {
int os = OTHER;
try {
String osName = System.getProperty("os.name");
if (osName == null) {
throw new IOException("os.name not found");
}
osName = osName.toLowerCase();
// match
if (osName.indexOf("windows") != -1) {
os = WINDOWS;
} else if (osName.indexOf("linux") != -1 ||
osName.indexOf("sun os") != -1 ||
osName.indexOf("sunos") != -1 ||
osName.indexOf("solaris") != -1 ||
osName.indexOf("mpe/ix") != -1 ||
osName.indexOf("freebsd") != -1 ||
osName.indexOf("irix") != -1 ||
osName.indexOf("digital unix") != -1 ||
osName.indexOf("unix") != -1 ||
osName.indexOf("mac os x") != -1) {
os = UNIX;
} else if (osName.indexOf("hp-ux") != -1 ||
osName.indexOf("aix") != -1) {
os = POSIX_UNIX;
} else {
os = OTHER;
}
} catch (Exception ex) {
os = INIT_PROBLEM;
}
OS = os;
}
/**
* Instances should NOT be constructed in standard programming.
*/
public FileSystemUtils() {
super();
}
//-----------------------------------------------------------------------
/**
* Returns the free space on a drive or volume by invoking
* the command line.
* This method does not normalize the result, and typically returns
* bytes on Windows, 512 byte units on OS X and kilobytes on Unix.
* As this is not very useful, this method is deprecated in favour
* of {@link #freeSpaceKb(String)} which returns a result in kilobytes.
* <p>
* Note that some OS's are NOT currently supported, including OS/390,
* OpenVMS and and SunOS 5. (SunOS is supported by <code>freeSpaceKb</code>.)
* <pre>
* FileSystemUtils.freeSpace("C:"); // Windows
* FileSystemUtils.freeSpace("/volume"); // *nix
* </pre>
* The free space is calculated via the command line.
* It uses 'dir /-c' on Windows and 'df' on *nix.
*
* @param path the path to get free space for, not null, not empty on Unix
* @return the amount of free drive space on the drive or volume
* @throws IllegalArgumentException if the path is invalid
* @throws IllegalStateException if an error occurred in initialisation
* @throws IOException if an error occurs when finding the free space
* @since Commons IO 1.1, enhanced OS support in 1.2 and 1.3
* @deprecated Use freeSpaceKb(String)
* Deprecated from 1.3, may be removed in 2.0
*/
public static long freeSpace(String path) throws IOException {
return INSTANCE.freeSpaceOS(path, OS, false);
}
//-----------------------------------------------------------------------
/**
* Returns the free space on a drive or volume in kilobytes by invoking
* the command line.
* <pre>
* FileSystemUtils.freeSpaceKb("C:"); // Windows
* FileSystemUtils.freeSpaceKb("/volume"); // *nix
* </pre>
* The free space is calculated via the command line.
* It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix.
* <p>
* In order to work, you must be running Windows, or have a implementation of
* Unix df that supports GNU format when passed -k (or -kP). If you are going
* to rely on this code, please check that it works on your OS by running
* some simple tests to compare the command line with the output from this class.
* If your operating system isn't supported, please raise a JIRA call detailing
* the exact result from df -k and as much other detail as possible, thanks.
*
* @param path the path to get free space for, not null, not empty on Unix
* @return the amount of free drive space on the drive or volume in kilobytes
* @throws IllegalArgumentException if the path is invalid
* @throws IllegalStateException if an error occurred in initialisation
* @throws IOException if an error occurs when finding the free space
* @since Commons IO 1.2, enhanced OS support in 1.3
*/
public static long freeSpaceKb(String path) throws IOException {
return INSTANCE.freeSpaceOS(path, OS, true);
}
//-----------------------------------------------------------------------
/**
* Returns the free space on a drive or volume in a cross-platform manner.
* Note that some OS's are NOT currently supported, including OS/390.
* <pre>
* FileSystemUtils.freeSpace("C:"); // Windows
* FileSystemUtils.freeSpace("/volume"); // *nix
* </pre>
* The free space is calculated via the command line.
* It uses 'dir /-c' on Windows and 'df' on *nix.
*
* @param path the path to get free space for, not null, not empty on Unix
* @param os the operating system code
* @param kb whether to normalize to kilobytes
* @return the amount of free drive space on the drive or volume
* @throws IllegalArgumentException if the path is invalid
* @throws IllegalStateException if an error occurred in initialisation
* @throws IOException if an error occurs when finding the free space
*/
long freeSpaceOS(String path, int os, boolean kb) throws IOException {
if (path == null) {
throw new IllegalArgumentException("Path must not be empty");
}
switch (os) {
case WINDOWS:
return (kb ? freeSpaceWindows(path) / 1024 : freeSpaceWindows(path));
case UNIX:
return freeSpaceUnix(path, kb, false);
case POSIX_UNIX:
return freeSpaceUnix(path, kb, true);
case OTHER:
throw new IllegalStateException("Unsupported operating system");
default:
throw new IllegalStateException(
"Exception caught when determining operating system");
}
}
//-----------------------------------------------------------------------
/**
* Find free space on the Windows platform using the 'dir' command.
*
* @param path the path to get free space for, including the colon
* @return the amount of free drive space on the drive
* @throws IOException if an error occurs
*/
long freeSpaceWindows(String path) throws IOException {
path = FilenameUtils.normalize(path);
if (path.length() > 2 && path.charAt(1) == ':') {
path = path.substring(0, 2); // seems to make it work
}
// build and run the 'dir' command
String[] cmdAttribs = new String[] {"cmd.exe", "/C", "dir /-c " + path};
// read in the output of the command to an ArrayList
List lines = performCommand(cmdAttribs, Integer.MAX_VALUE);
// now iterate over the lines we just read and find the LAST
// non-empty line (the free space bytes should be in the last element
// of the ArrayList anyway, but this will ensure it works even if it's
// not, still assuming it is on the last non-blank line)
for (int i = lines.size() - 1; i >= 0; i--) {
String line = (String) lines.get(i);
if (line.length() > 0) {
return parseDir(line, path);
}
}
// all lines are blank
throw new IOException(
"Command line 'dir /-c' did not return any info " +
"for path '" + path + "'");
}
/**
* Parses the Windows dir response last line
*
* @param line the line to parse
* @param path the path that was sent
* @return the number of bytes
* @throws IOException if an error occurs
*/
long parseDir(String line, String path) throws IOException {
// read from the end of the line to find the last numeric
// character on the line, then continue until we find the first
// non-numeric character, and everything between that and the last
// numeric character inclusive is our free space bytes count
int bytesStart = 0;
int bytesEnd = 0;
int j = line.length() - 1;
innerLoop1: while (j >= 0) {
char c = line.charAt(j);
if (Character.isDigit(c)) {
// found the last numeric character, this is the end of
// the free space bytes count
bytesEnd = j + 1;
break innerLoop1;
}
j--;
}
innerLoop2: while (j >= 0) {
char c = line.charAt(j);
if (!Character.isDigit(c) && c != ',' && c != '.') {
// found the next non-numeric character, this is the
// beginning of the free space bytes count
bytesStart = j + 1;
break innerLoop2;
}
j--;
}
if (j < 0) {
throw new IOException(
"Command line 'dir /-c' did not return valid info " +
"for path '" + path + "'");
}
// remove commas and dots in the bytes count
StringBuffer buf = new StringBuffer(line.substring(bytesStart, bytesEnd));
for (int k = 0; k < buf.length(); k++) {
if (buf.charAt(k) == ',' || buf.charAt(k) == '.') {
buf.deleteCharAt(k--);
}
}
return parseBytes(buf.toString(), path);
}
//-----------------------------------------------------------------------
/**
* Find free space on the *nix platform using the 'df' command.
*
* @param path the path to get free space for
* @param kb whether to normalize to kilobytes
* @param posix whether to use the posix standard format flag
* @return the amount of free drive space on the volume
* @throws IOException if an error occurs
*/
long freeSpaceUnix(String path, boolean kb, boolean posix) throws IOException {
if (path.length() == 0) {
throw new IllegalArgumentException("Path must not be empty");
}
path = FilenameUtils.normalize(path);
// build and run the 'dir' command
String flags = "-";
if (kb) {
flags += "k";
}
if (posix) {
flags += "P";
}
String[] cmdAttribs =
(flags.length() > 1 ? new String[] {"df", flags, path} : new String[] {"df", path});
// perform the command, asking for up to 3 lines (header, interesting, overflow)
List lines = performCommand(cmdAttribs, 3);
if (lines.size() < 2) {
// unknown problem, throw exception
throw new IOException(
"Command line 'df' did not return info as expected " +
"for path '" + path + "'- response was " + lines);
}
String line2 = (String) lines.get(1); // the line we're interested in
// Now, we tokenize the string. The fourth element is what we want.
StringTokenizer tok = new StringTokenizer(line2, " ");
if (tok.countTokens() < 4) {
// could be long Filesystem, thus data on third line
if (tok.countTokens() == 1 && lines.size() >= 3) {
String line3 = (String) lines.get(2); // the line may be interested in
tok = new StringTokenizer(line3, " ");
} else {
throw new IOException(
"Command line 'df' did not return data as expected " +
"for path '" + path + "'- check path is valid");
}
} else {
tok.nextToken(); // Ignore Filesystem
}
tok.nextToken(); // Ignore 1K-blocks
tok.nextToken(); // Ignore Used
String freeSpace = tok.nextToken();
return parseBytes(freeSpace, path);
}
//-----------------------------------------------------------------------
/**
* Parses the bytes from a string.
*
* @param freeSpace the free space string
* @param path the path
* @return the number of bytes
* @throws IOException if an error occurs
*/
long parseBytes(String freeSpace, String path) throws IOException {
try {
long bytes = Long.parseLong(freeSpace);
if (bytes < 0) {
throw new IOException(
"Command line 'df' did not find free space in response " +
"for path '" + path + "'- check path is valid");
}
return bytes;
} catch (NumberFormatException ex) {
throw new IOException(
"Command line 'df' did not return numeric data as expected " +
"for path '" + path + "'- check path is valid");
}
}
//-----------------------------------------------------------------------
/**
* Performs the os command.
*
* @param cmdAttribs the command line parameters
* @param max The maximum limit for the lines returned
* @return the parsed data
* @throws IOException if an error occurs
*/
List performCommand(String[] cmdAttribs, int max) throws IOException {
// this method does what it can to avoid the 'Too many open files' error
// based on trial and error and these links:
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027
// http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018
// however, its still not perfect as the JDK support is so poor
// (see commond-exec or ant for a better multi-threaded multi-os solution)
List lines = new ArrayList(20);
Process proc = null;
InputStream in = null;
OutputStream out = null;
InputStream err = null;
BufferedReader inr = null;
try {
proc = openProcess(cmdAttribs);
in = proc.getInputStream();
out = proc.getOutputStream();
err = proc.getErrorStream();
inr = new BufferedReader(new InputStreamReader(in));
String line = inr.readLine();
while (line != null && lines.size() < max) {
line = line.toLowerCase().trim();
lines.add(line);
line = inr.readLine();
}
proc.waitFor();
if (proc.exitValue() != 0) {
// os command problem, throw exception
throw new IOException(
"Command line returned OS error code '" + proc.exitValue() +
"' for command " + Arrays.asList(cmdAttribs));
}
if (lines.size() == 0) {
// unknown problem, throw exception
throw new IOException(
"Command line did not return any info " +
"for command " + Arrays.asList(cmdAttribs));
}
return lines;
} catch (InterruptedException ex) {
throw new IOException(
"Command line threw an InterruptedException '" + ex.getMessage() +
"' for command " + Arrays.asList(cmdAttribs));
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(out);
IOUtils.closeQuietly(err);
IOUtils.closeQuietly(inr);
if (proc != null) {
proc.destroy();
}
}
}
/**
* Opens the process to the operating system.
*
* @param cmdAttribs the command line parameters
* @return the process
* @throws IOException if an error occurs
*/
Process openProcess(String[] cmdAttribs) throws IOException {
return Runtime.getRuntime().exec(cmdAttribs);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.IOException;
import java.io.OutputStream;
/**
* Dumps data in hexadecimal format.
* <p>
* Provides a single function to take an array of bytes and display it
* in hexadecimal form.
* <p>
* Origin of code: POI.
*
* @author Scott Sanders
* @author Marc Johnson
* @version $Id: HexDump.java 596667 2007-11-20 13:50:14Z niallp $
*/
public class HexDump {
/**
* Instances should NOT be constructed in standard programming.
*/
public HexDump() {
super();
}
/**
* Dump an array of bytes to an OutputStream.
*
* @param data the byte array to be dumped
* @param offset its offset, whatever that might mean
* @param stream the OutputStream to which the data is to be
* written
* @param index initial index into the byte array
*
* @throws IOException is thrown if anything goes wrong writing
* the data to stream
* @throws ArrayIndexOutOfBoundsException if the index is
* outside the data array's bounds
* @throws IllegalArgumentException if the output stream is null
*/
public static void dump(byte[] data, long offset,
OutputStream stream, int index)
throws IOException, ArrayIndexOutOfBoundsException,
IllegalArgumentException {
if ((index < 0) || (index >= data.length)) {
throw new ArrayIndexOutOfBoundsException(
"illegal index: " + index + " into array of length "
+ data.length);
}
if (stream == null) {
throw new IllegalArgumentException("cannot write to nullstream");
}
long display_offset = offset + index;
StringBuffer buffer = new StringBuffer(74);
for (int j = index; j < data.length; j += 16) {
int chars_read = data.length - j;
if (chars_read > 16) {
chars_read = 16;
}
dump(buffer, display_offset).append(' ');
for (int k = 0; k < 16; k++) {
if (k < chars_read) {
dump(buffer, data[k + j]);
} else {
buffer.append(" ");
}
buffer.append(' ');
}
for (int k = 0; k < chars_read; k++) {
if ((data[k + j] >= ' ') && (data[k + j] < 127)) {
buffer.append((char) data[k + j]);
} else {
buffer.append('.');
}
}
buffer.append(EOL);
stream.write(buffer.toString().getBytes());
stream.flush();
buffer.setLength(0);
display_offset += chars_read;
}
}
/**
* The line-separator (initializes to "line.separator" system property.
*/
public static final String EOL =
System.getProperty("line.separator");
private static final char[] _hexcodes =
{
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'
};
private static final int[] _shifts =
{
28, 24, 20, 16, 12, 8, 4, 0
};
/**
* Dump a long value into a StringBuffer.
*
* @param _lbuffer the StringBuffer to dump the value in
* @param value the long value to be dumped
* @return StringBuffer containing the dumped value.
*/
private static StringBuffer dump(StringBuffer _lbuffer, long value) {
for (int j = 0; j < 8; j++) {
_lbuffer
.append(_hexcodes[((int) (value >> _shifts[j])) & 15]);
}
return _lbuffer;
}
/**
* Dump a byte value into a StringBuffer.
*
* @param _cbuffer the StringBuffer to dump the value in
* @param value the byte value to be dumped
* @return StringBuffer containing the dumped value.
*/
private static StringBuffer dump(StringBuffer _cbuffer, byte value) {
for (int j = 0; j < 2; j++) {
_cbuffer.append(_hexcodes[(value >> _shifts[j + 6]) & 15]);
}
return _cbuffer;
}
}

View File

@ -0,0 +1,238 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.Serializable;
/**
* Enumeration of IO case sensitivity.
* <p>
* Different filing systems have different rules for case-sensitivity.
* Windows is case-insensitive, Unix is case-sensitive.
* <p>
* This class captures that difference, providing an enumeration to
* control how filename comparisons should be performed. It also provides
* methods that use the enumeration to perform comparisons.
* <p>
* Wherever possible, you should use the <code>check</code> methods in this
* class to compare filenames.
*
* @author Stephen Colebourne
* @version $Id: IOCase.java 606345 2007-12-21 23:43:01Z ggregory $
* @since Commons IO 1.3
*/
public final class IOCase implements Serializable {
/**
* The constant for case sensitive regardless of operating system.
*/
public static final IOCase SENSITIVE = new IOCase("Sensitive", true);
/**
* The constant for case insensitive regardless of operating system.
*/
public static final IOCase INSENSITIVE = new IOCase("Insensitive", false);
/**
* The constant for case sensitivity determined by the current operating system.
* Windows is case-insensitive when comparing filenames, Unix is case-sensitive.
* <p>
* If you derialize this constant of Windows, and deserialize on Unix, or vice
* versa, then the value of the case-sensitivity flag will change.
*/
public static final IOCase SYSTEM = new IOCase("System", !FilenameUtils.isSystemWindows());
/** Serialization version. */
private static final long serialVersionUID = -6343169151696340687L;
/** The enumeration name. */
private final String name;
/** The sensitivity flag. */
private final transient boolean sensitive;
//-----------------------------------------------------------------------
/**
* Factory method to create an IOCase from a name.
*
* @param name the name to find
* @return the IOCase object
* @throws IllegalArgumentException if the name is invalid
*/
public static IOCase forName(String name) {
if (IOCase.SENSITIVE.name.equals(name)){
return IOCase.SENSITIVE;
}
if (IOCase.INSENSITIVE.name.equals(name)){
return IOCase.INSENSITIVE;
}
if (IOCase.SYSTEM.name.equals(name)){
return IOCase.SYSTEM;
}
throw new IllegalArgumentException("Invalid IOCase name: " + name);
}
//-----------------------------------------------------------------------
/**
* Private constructor.
*
* @param name the name
* @param sensitive the sensitivity
*/
private IOCase(String name, boolean sensitive) {
this.name = name;
this.sensitive = sensitive;
}
/**
* Replaces the enumeration from the stream with a real one.
* This ensures that the correct flag is set for SYSTEM.
*
* @return the resolved object
*/
private Object readResolve() {
return forName(name);
}
//-----------------------------------------------------------------------
/**
* Gets the name of the constant.
*
* @return the name of the constant
*/
public String getName() {
return name;
}
/**
* Does the object represent case sensitive comparison.
*
* @return true if case sensitive
*/
public boolean isCaseSensitive() {
return sensitive;
}
//-----------------------------------------------------------------------
/**
* Compares two strings using the case-sensitivity rule.
* <p>
* This method mimics {@link String#compareTo} but takes case-sensitivity
* into account.
*
* @param str1 the first string to compare, not null
* @param str2 the second string to compare, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
public int checkCompareTo(String str1, String str2) {
if (str1 == null || str2 == null) {
throw new NullPointerException("The strings must not be null");
}
return sensitive ? str1.compareTo(str2) : str1.compareToIgnoreCase(str2);
}
/**
* Compares two strings using the case-sensitivity rule.
* <p>
* This method mimics {@link String#equals} but takes case-sensitivity
* into account.
*
* @param str1 the first string to compare, not null
* @param str2 the second string to compare, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
public boolean checkEquals(String str1, String str2) {
if (str1 == null || str2 == null) {
throw new NullPointerException("The strings must not be null");
}
return sensitive ? str1.equals(str2) : str1.equalsIgnoreCase(str2);
}
/**
* Checks if one string starts with another using the case-sensitivity rule.
* <p>
* This method mimics {@link String#startsWith(String)} but takes case-sensitivity
* into account.
*
* @param str the string to check, not null
* @param start the start to compare against, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
public boolean checkStartsWith(String str, String start) {
return str.regionMatches(!sensitive, 0, start, 0, start.length());
}
/**
* Checks if one string ends with another using the case-sensitivity rule.
* <p>
* This method mimics {@link String#endsWith} but takes case-sensitivity
* into account.
*
* @param str the string to check, not null
* @param end the end to compare against, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
public boolean checkEndsWith(String str, String end) {
int endLen = end.length();
return str.regionMatches(!sensitive, str.length() - endLen, end, 0, endLen);
}
/**
* Checks if one string contains another at a specific index using the case-sensitivity rule.
* <p>
* This method mimics parts of {@link String#regionMatches(boolean, int, String, int, int)}
* but takes case-sensitivity into account.
*
* @param str the string to check, not null
* @param strStartIndex the index to start at in str
* @param search the start to search for, not null
* @return true if equal using the case rules
* @throws NullPointerException if either string is null
*/
public boolean checkRegionMatches(String str, int strStartIndex, String search) {
return str.regionMatches(!sensitive, strStartIndex, search, 0, search.length());
}
/**
* Converts the case of the input String to a standard format.
* Subsequent operations can then use standard String methods.
*
* @param str the string to convert, null returns null
* @return the lower-case version if case-insensitive
*/
String convertCase(String str) {
if (str == null) {
return null;
}
return sensitive ? str : str.toLowerCase();
}
//-----------------------------------------------------------------------
/**
* Gets a string describing the sensitivity.
*
* @return a string describing the sensitivity
*/
public String toString() {
return name;
}
}

View File

@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.IOException;
/**
* Subclasses IOException with the {@link Throwable} constructors missing before Java 6. If you are using Java 6,
* consider this class deprecated and use {@link IOException}.
*
* @author <a href="http://commons.apache.org/io/">Apache Commons IO</a>
* @version $Id$
* @since Commons IO 1.4
*/
public class IOExceptionWithCause extends IOException {
/**
* Defines the serial version UID.
*/
private static final long serialVersionUID = 1L;
/**
* Constructs a new instance with the given message and cause.
* <p>
* As specified in {@link Throwable}, the message in the given <code>cause</code> is not used in this instance's
* message.
* </p>
*
* @param message
* the message (see {@link #getMessage()})
* @param cause
* the cause (see {@link #getCause()}). A <code>null</code> value is allowed.
*/
public IOExceptionWithCause(String message, Throwable cause) {
super(message);
this.initCause(cause);
}
/**
* Constructs a new instance with the given cause.
* <p>
* The message is set to <code>cause==null ? null : cause.toString()</code>, which by default contains the class
* and message of <code>cause</code>. This constructor is useful for call sites that just wrap another throwable.
* </p>
*
* @param cause
* the cause (see {@link #getCause()}). A <code>null</code> value is allowed.
*/
public IOExceptionWithCause(Throwable cause) {
super(cause == null ? null : cause.toString());
this.initCause(cause);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* An Iterator over the lines in a <code>Reader</code>.
* <p>
* <code>LineIterator</code> holds a reference to an open <code>Reader</code>.
* When you have finished with the iterator you should close the reader
* to free internal resources. This can be done by closing the reader directly,
* or by calling the {@link #close()} or {@link #closeQuietly(LineIterator)}
* method on the iterator.
* <p>
* The recommended usage pattern is:
* <pre>
* LineIterator it = FileUtils.lineIterator(file, "UTF-8");
* try {
* while (it.hasNext()) {
* String line = it.nextLine();
* /// do something with line
* }
* } finally {
* LineIterator.closeQuietly(iterator);
* }
* </pre>
*
* @author Niall Pemberton
* @author Stephen Colebourne
* @author Sandy McArthur
* @version $Id: LineIterator.java 437567 2006-08-28 06:39:07Z bayard $
* @since Commons IO 1.2
*/
public class LineIterator implements Iterator {
/** The reader that is being read. */
private final BufferedReader bufferedReader;
/** The current line. */
private String cachedLine;
/** A flag indicating if the iterator has been fully read. */
private boolean finished = false;
/**
* Constructs an iterator of the lines for a <code>Reader</code>.
*
* @param reader the <code>Reader</code> to read from, not null
* @throws IllegalArgumentException if the reader is null
*/
public LineIterator(final Reader reader) throws IllegalArgumentException {
if (reader == null) {
throw new IllegalArgumentException("Reader must not be null");
}
if (reader instanceof BufferedReader) {
bufferedReader = (BufferedReader) reader;
} else {
bufferedReader = new BufferedReader(reader);
}
}
//-----------------------------------------------------------------------
/**
* Indicates whether the <code>Reader</code> has more lines.
* If there is an <code>IOException</code> then {@link #close()} will
* be called on this instance.
*
* @return <code>true</code> if the Reader has more lines
* @throws IllegalStateException if an IO exception occurs
*/
public boolean hasNext() {
if (cachedLine != null) {
return true;
} else if (finished) {
return false;
} else {
try {
while (true) {
String line = bufferedReader.readLine();
if (line == null) {
finished = true;
return false;
} else if (isValidLine(line)) {
cachedLine = line;
return true;
}
}
} catch(IOException ioe) {
close();
throw new IllegalStateException(ioe.toString());
}
}
}
/**
* Overridable method to validate each line that is returned.
*
* @param line the line that is to be validated
* @return true if valid, false to remove from the iterator
*/
protected boolean isValidLine(String line) {
return true;
}
/**
* Returns the next line in the wrapped <code>Reader</code>.
*
* @return the next line from the input
* @throws NoSuchElementException if there is no line to return
*/
public Object next() {
return nextLine();
}
/**
* Returns the next line in the wrapped <code>Reader</code>.
*
* @return the next line from the input
* @throws NoSuchElementException if there is no line to return
*/
public String nextLine() {
if (!hasNext()) {
throw new NoSuchElementException("No more lines");
}
String currentLine = cachedLine;
cachedLine = null;
return currentLine;
}
/**
* Closes the underlying <code>Reader</code> quietly.
* This method is useful if you only want to process the first few
* lines of a larger file. If you do not close the iterator
* then the <code>Reader</code> remains open.
* This method can safely be called multiple times.
*/
public void close() {
finished = true;
IOUtils.closeQuietly(bufferedReader);
cachedLine = null;
}
/**
* Unsupported.
*
* @throws UnsupportedOperationException always
*/
public void remove() {
throw new UnsupportedOperationException("Remove unsupported on LineIterator");
}
//-----------------------------------------------------------------------
/**
* Closes the iterator, handling null and ignoring exceptions.
*
* @param iterator the iterator to close
*/
public static void closeQuietly(LineIterator iterator) {
if (iterator != null) {
iterator.close();
}
}
}

View File

@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
/**
* Compare two files using the <b>default</b> {@link File#compareTo(File)} method.
* <p>
* This comparator can be used to sort lists or arrays of files
* by using the default file comparison.
* <p>
* Example of sorting a list of files using the
* {@link #DEFAULT_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, DefaultFileComparator.DEFAULT_COMPARATOR);
* </pre>
* <p>
* Example of doing a <i>reverse</i> sort of an array of files using the
* {@link #DEFAULT_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, DefaultFileComparator.DEFAULT_REVERSE);
* </pre>
* <p>
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class DefaultFileComparator implements Comparator, Serializable {
/** Singleton default comparator instance */
public static final Comparator DEFAULT_COMPARATOR = new DefaultFileComparator();
/** Singleton reverse default comparator instance */
public static final Comparator DEFAULT_REVERSE = new ReverseComparator(DEFAULT_COMPARATOR);
/**
* Compare the two files using the {@link File#compareTo(File)} method.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return the result of calling file1's
* {@link File#compareTo(File)} with file2 as the parameter.
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
return file1.compareTo(file2);
}
}

View File

@ -0,0 +1,112 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
/**
* Compare the file name <b>extensions</b> for order
* (see {@link FilenameUtils#getExtension(String)}).
* <p>
* This comparator can be used to sort lists or arrays of files
* by their file extension either in a case-sensitive, case-insensitive or
* system dependant case sensitive way. A number of singleton instances
* are provided for the various case sensitivity options (using {@link IOCase})
* and the reverse of those options.
* <p>
* Example of a <i>case-sensitive</i> file extension sort using the
* {@link #EXTENSION_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, ExtensionFileComparator.EXTENSION_COMPARATOR);
* </pre>
* <p>
* Example of a <i>reverse case-insensitive</i> file extension sort using the
* {@link #EXTENSION_INSENSITIVE_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, ExtensionFileComparator.EXTENSION_INSENSITIVE_REVERSE);
* </pre>
* <p>
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class ExtensionFileComparator implements Comparator, Serializable {
/** Case-sensitive extension comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator EXTENSION_COMPARATOR = new ExtensionFileComparator();
/** Reverse case-sensitive extension comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator EXTENSION_REVERSE = new ReverseComparator(EXTENSION_COMPARATOR);
/** Case-insensitive extension comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator EXTENSION_INSENSITIVE_COMPARATOR = new ExtensionFileComparator(IOCase.INSENSITIVE);
/** Reverse case-insensitive extension comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator EXTENSION_INSENSITIVE_REVERSE
= new ReverseComparator(EXTENSION_INSENSITIVE_COMPARATOR);
/** System sensitive extension comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator EXTENSION_SYSTEM_COMPARATOR = new ExtensionFileComparator(IOCase.SYSTEM);
/** Reverse system sensitive path comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator EXTENSION_SYSTEM_REVERSE = new ReverseComparator(EXTENSION_SYSTEM_COMPARATOR);
/** Whether the comparison is case sensitive. */
private final IOCase caseSensitivity;
/**
* Construct a case sensitive file extension comparator instance.
*/
public ExtensionFileComparator() {
this.caseSensitivity = IOCase.SENSITIVE;
}
/**
* Construct a file extension comparator instance with the specified case-sensitivity.
*
* @param caseSensitivity how to handle case sensitivity, null means case-sensitive
*/
public ExtensionFileComparator(IOCase caseSensitivity) {
this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
}
/**
* Compare the extensions of two files the specified case sensitivity.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return a negative value if the first file's extension
* is less than the second, zero if the extensions are the
* same and a positive value if the first files extension
* is greater than the second file.
*
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
String suffix1 = FilenameUtils.getExtension(file1.getName());
String suffix2 = FilenameUtils.getExtension(file2.getName());
return caseSensitivity.checkCompareTo(suffix1, suffix2);
}
}

View File

@ -0,0 +1,79 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
/**
* Compare the <b>last modified date/time</b> of two files for order
* (see {@link File#lastModified()}).
* <p>
* This comparator can be used to sort lists or arrays of files
* by their last modified date/time.
* <p>
* Example of sorting a list of files using the
* {@link #LASTMODIFIED_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
* </pre>
* <p>
* Example of doing a <i>reverse</i> sort of an array of files using the
* {@link #LASTMODIFIED_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, LastModifiedFileComparator.LASTMODIFIED_REVERSE);
* </pre>
* <p>
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class LastModifiedFileComparator implements Comparator, Serializable {
/** Last modified comparator instance */
public static final Comparator LASTMODIFIED_COMPARATOR = new LastModifiedFileComparator();
/** Reverse last modified comparator instance */
public static final Comparator LASTMODIFIED_REVERSE = new ReverseComparator(LASTMODIFIED_COMPARATOR);
/**
* Compare the last the last modified date/time of two files.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return a negative value if the first file's lastmodified date/time
* is less than the second, zero if the lastmodified date/time are the
* same and a positive value if the first files lastmodified date/time
* is greater than the second file.
*
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
long result = file1.lastModified() - file2.lastModified();
if (result < 0) {
return -1;
} else if (result > 0) {
return 1;
} else {
return 0;
}
}
}

View File

@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
import org.apache.commons.io.IOCase;
/**
* Compare the <b>names</b> of two files for order (see {@link File#getName()}).
* <p>
* This comparator can be used to sort lists or arrays of files
* by their name either in a case-sensitive, case-insensitive or
* system dependant case sensitive way. A number of singleton instances
* are provided for the various case sensitivity options (using {@link IOCase})
* and the reverse of those options.
* <p>
* Example of a <i>case-sensitive</i> file name sort using the
* {@link #NAME_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, NameFileComparator.NAME_COMPARATOR);
* </pre>
* <p>
* Example of a <i>reverse case-insensitive</i> file name sort using the
* {@link #NAME_INSENSITIVE_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, NameFileComparator.NAME_INSENSITIVE_REVERSE);
* </pre>
* <p>
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class NameFileComparator implements Comparator, Serializable {
/** Case-sensitive name comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator NAME_COMPARATOR = new NameFileComparator();
/** Reverse case-sensitive name comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator NAME_REVERSE = new ReverseComparator(NAME_COMPARATOR);
/** Case-insensitive name comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator NAME_INSENSITIVE_COMPARATOR = new NameFileComparator(IOCase.INSENSITIVE);
/** Reverse case-insensitive name comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator NAME_INSENSITIVE_REVERSE = new ReverseComparator(NAME_INSENSITIVE_COMPARATOR);
/** System sensitive name comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator NAME_SYSTEM_COMPARATOR = new NameFileComparator(IOCase.SYSTEM);
/** Reverse system sensitive name comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator NAME_SYSTEM_REVERSE = new ReverseComparator(NAME_SYSTEM_COMPARATOR);
/** Whether the comparison is case sensitive. */
private final IOCase caseSensitivity;
/**
* Construct a case sensitive file name comparator instance.
*/
public NameFileComparator() {
this.caseSensitivity = IOCase.SENSITIVE;
}
/**
* Construct a file name comparator instance with the specified case-sensitivity.
*
* @param caseSensitivity how to handle case sensitivity, null means case-sensitive
*/
public NameFileComparator(IOCase caseSensitivity) {
this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
}
/**
* Compare the names of two files with the specified case sensitivity.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return a negative value if the first file's name
* is less than the second, zero if the names are the
* same and a positive value if the first files name
* is greater than the second file.
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
return caseSensitivity.checkCompareTo(file1.getName(), file2.getName());
}
}

View File

@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
import org.apache.commons.io.IOCase;
/**
* Compare the <b>path</b> of two files for order (see {@link File#getPath()}).
* <p>
* This comparator can be used to sort lists or arrays of files
* by their path either in a case-sensitive, case-insensitive or
* system dependant case sensitive way. A number of singleton instances
* are provided for the various case sensitivity options (using {@link IOCase})
* and the reverse of those options.
* <p>
* Example of a <i>case-sensitive</i> file path sort using the
* {@link #PATH_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, PathFileComparator.PATH_COMPARATOR);
* </pre>
* <p>
* Example of a <i>reverse case-insensitive</i> file path sort using the
* {@link #PATH_INSENSITIVE_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, PathFileComparator.PATH_INSENSITIVE_REVERSE);
* </pre>
* <p>
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class PathFileComparator implements Comparator, Serializable {
/** Case-sensitive path comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator PATH_COMPARATOR = new PathFileComparator();
/** Reverse case-sensitive path comparator instance (see {@link IOCase#SENSITIVE}) */
public static final Comparator PATH_REVERSE = new ReverseComparator(PATH_COMPARATOR);
/** Case-insensitive path comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator PATH_INSENSITIVE_COMPARATOR = new PathFileComparator(IOCase.INSENSITIVE);
/** Reverse case-insensitive path comparator instance (see {@link IOCase#INSENSITIVE}) */
public static final Comparator PATH_INSENSITIVE_REVERSE = new ReverseComparator(PATH_INSENSITIVE_COMPARATOR);
/** System sensitive path comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator PATH_SYSTEM_COMPARATOR = new PathFileComparator(IOCase.SYSTEM);
/** Reverse system sensitive path comparator instance (see {@link IOCase#SYSTEM}) */
public static final Comparator PATH_SYSTEM_REVERSE = new ReverseComparator(PATH_SYSTEM_COMPARATOR);
/** Whether the comparison is case sensitive. */
private final IOCase caseSensitivity;
/**
* Construct a case sensitive file path comparator instance.
*/
public PathFileComparator() {
this.caseSensitivity = IOCase.SENSITIVE;
}
/**
* Construct a file path comparator instance with the specified case-sensitivity.
*
* @param caseSensitivity how to handle case sensitivity, null means case-sensitive
*/
public PathFileComparator(IOCase caseSensitivity) {
this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
}
/**
* Compare the paths of two files the specified case sensitivity.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return a negative value if the first file's path
* is less than the second, zero if the paths are the
* same and a positive value if the first files path
* is greater than the second file.
*
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
return caseSensitivity.checkCompareTo(file1.getPath(), file2.getPath());
}
}

View File

@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.Serializable;
import java.util.Comparator;
/**
* Reverses the result of comparing two objects using
* the delegate {@link Comparator}.
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
class ReverseComparator implements Comparator, Serializable {
private final Comparator delegate;
/**
* Construct an instance with the sepecified delegate {@link Comparator}.
*
* @param delegate The comparator to delegate to
*/
public ReverseComparator(Comparator delegate) {
if (delegate == null) {
throw new IllegalArgumentException("Delegate comparator is missing");
}
this.delegate = delegate;
}
/**
* Compare using the delegate Comparator, but reversing the result.
*
* @param obj1 The first object to compare
* @param obj2 The second object to compare
* @return the result from the delegate {@link Comparator#compare(Object, Object)}
* reversing the value (i.e. positive becomes negative and vice versa)
*/
public int compare(Object obj1, Object obj2) {
return delegate.compare(obj2, obj1); // parameters switched round
}
}

View File

@ -0,0 +1,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.comparator;
import java.io.File;
import java.io.Serializable;
import java.util.Comparator;
import org.apache.commons.io.FileUtils;
/**
* Compare the <b>length/size</b> of two files for order (see
* {@link File#length()} and {@link FileUtils#sizeOfDirectory(File)}).
* <p>
* This comparator can be used to sort lists or arrays of files
* by their length/size.
* <p>
* Example of sorting a list of files using the
* {@link #SIZE_COMPARATOR} singleton instance:
* <pre>
* List&lt;File&gt; list = ...
* Collections.sort(list, LengthFileComparator.LENGTH_COMPARATOR);
* </pre>
* <p>
* Example of doing a <i>reverse</i> sort of an array of files using the
* {@link #SIZE_REVERSE} singleton instance:
* <pre>
* File[] array = ...
* Arrays.sort(array, LengthFileComparator.LENGTH_REVERSE);
* </pre>
* <p>
* <strong>N.B.</strong> Directories are treated as <b>zero size</b> unless
* <code>sumDirectoryContents</code> is <code>true</code>.
*
* @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $
* @since Commons IO 1.4
*/
public class SizeFileComparator implements Comparator, Serializable {
/** Size comparator instance - directories are treated as zero size */
public static final Comparator SIZE_COMPARATOR = new SizeFileComparator();
/** Reverse size comparator instance - directories are treated as zero size */
public static final Comparator SIZE_REVERSE = new ReverseComparator(SIZE_COMPARATOR);
/**
* Size comparator instance which sums the size of a directory's contents
* using {@link FileUtils#sizeOfDirectory(File)}
*/
public static final Comparator SIZE_SUMDIR_COMPARATOR = new SizeFileComparator(true);
/**
* Reverse size comparator instance which sums the size of a directory's contents
* using {@link FileUtils#sizeOfDirectory(File)}
*/
public static final Comparator SIZE_SUMDIR_REVERSE = new ReverseComparator(SIZE_SUMDIR_COMPARATOR);
/** Whether the sum of the directory's contents should be calculated. */
private final boolean sumDirectoryContents;
/**
* Construct a file size comparator instance (directories treated as zero size).
*/
public SizeFileComparator() {
this.sumDirectoryContents = false;
}
/**
* Construct a file size comparator instance specifying whether the size of
* the directory contents should be aggregated.
* <p>
* If the <code>sumDirectoryContents</code> is <code>true</code> The size of
* directories is calculated using {@link FileUtils#sizeOfDirectory(File)}.
*
* @param sumDirectoryContents <code>true</code> if the sum of the directoryies contents
* should be calculated, otherwise <code>false</code> if directories should be treated
* as size zero (see {@link FileUtils#sizeOfDirectory(File)}).
*/
public SizeFileComparator(boolean sumDirectoryContents) {
this.sumDirectoryContents = sumDirectoryContents;
}
/**
* Compare the length of two files.
*
* @param obj1 The first file to compare
* @param obj2 The second file to compare
* @return a negative value if the first file's length
* is less than the second, zero if the lengths are the
* same and a positive value if the first files length
* is greater than the second file.
*
*/
public int compare(Object obj1, Object obj2) {
File file1 = (File)obj1;
File file2 = (File)obj2;
long size1 = 0;
if (file1.isDirectory()) {
size1 = sumDirectoryContents && file1.exists() ? FileUtils.sizeOfDirectory(file1) : 0;
} else {
size1 = file1.length();
}
long size2 = 0;
if (file2.isDirectory()) {
size2 = sumDirectoryContents && file2.exists() ? FileUtils.sizeOfDirectory(file2) : 0;
} else {
size2 = file2.length();
}
long result = size1 - size2;
if (result < 0) {
return -1;
} else if (result > 0) {
return 1;
} else {
return 0;
}
}
}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<body>
<p>This package provides various {@link java.util.Comparator} implementations
for {@link java.io.File}s.
</p>
</body>
</html>

View File

@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.filefilter;
import java.io.File;
/**
* An abstract class which implements the Java FileFilter and FilenameFilter
* interfaces via the IOFileFilter interface.
* <p>
* Note that a subclass <b>must</b> override one of the accept methods,
* otherwise your class will infinitely loop.
*
* @since Commons IO 1.0
* @version $Revision: 539231 $ $Date: 2007-05-18 04:10:33 +0100 (Fri, 18 May 2007) $
*
* @author Stephen Colebourne
*/
public abstract class AbstractFileFilter implements IOFileFilter {
/**
* Checks to see if the File should be accepted by this filter.
*
* @param file the File to check
* @return true if this file matches the test
*/
public boolean accept(File file) {
return accept(file.getParentFile(), file.getName());
}
/**
* Checks to see if the File should be accepted by this filter.
*
* @param dir the directory File to check
* @param name the filename within the directory to check
* @return true if this file matches the test
*/
public boolean accept(File dir, String name) {
return accept(new File(dir, name));
}
/**
* Provide a String representaion of this file filter.
*
* @return a String representaion
*/
public String toString() {
String name = getClass().getName();
int period = name.lastIndexOf('.');
return (period > 0 ? name.substring(period + 1) : name);
}
}

View File

@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.filefilter;
import java.io.File;
import java.io.Serializable;
import java.util.Date;
import org.apache.commons.io.FileUtils;
/**
* Filters files based on a cutoff time, can filter either newer
* files or files equal to or older.
* <p>
* For example, to print all files and directories in the
* current directory older than one day:
*
* <pre>
* File dir = new File(".");
* // We are interested in files older than one day
* long cutoff = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
* String[] files = dir.list( new AgeFileFilter(cutoff) );
* for ( int i = 0; i &lt; files.length; i++ ) {
* System.out.println(files[i]);
* }
* </pre>
*
* @author Rahul Akolkar
* @version $Id: AgeFileFilter.java 606381 2007-12-22 02:03:16Z ggregory $
* @since Commons IO 1.2
*/
public class AgeFileFilter extends AbstractFileFilter implements Serializable {
/** The cutoff time threshold. */
private final long cutoff;
/** Whether the files accepted will be older or newer. */
private final boolean acceptOlder;
/**
* Constructs a new age file filter for files equal to or older than
* a certain cutoff
*
* @param cutoff the threshold age of the files
*/
public AgeFileFilter(long cutoff) {
this(cutoff, true);
}
/**
* Constructs a new age file filter for files on any one side
* of a certain cutoff.
*
* @param cutoff the threshold age of the files
* @param acceptOlder if true, older files (at or before the cutoff)
* are accepted, else newer ones (after the cutoff).
*/
public AgeFileFilter(long cutoff, boolean acceptOlder) {
this.acceptOlder = acceptOlder;
this.cutoff = cutoff;
}
/**
* Constructs a new age file filter for files older than (at or before)
* a certain cutoff date.
*
* @param cutoffDate the threshold age of the files
*/
public AgeFileFilter(Date cutoffDate) {
this(cutoffDate, true);
}
/**
* Constructs a new age file filter for files on any one side
* of a certain cutoff date.
*
* @param cutoffDate the threshold age of the files
* @param acceptOlder if true, older files (at or before the cutoff)
* are accepted, else newer ones (after the cutoff).
*/
public AgeFileFilter(Date cutoffDate, boolean acceptOlder) {
this(cutoffDate.getTime(), acceptOlder);
}
/**
* Constructs a new age file filter for files older than (at or before)
* a certain File (whose last modification time will be used as reference).
*
* @param cutoffReference the file whose last modification
* time is usesd as the threshold age of the files
*/
public AgeFileFilter(File cutoffReference) {
this(cutoffReference, true);
}
/**
* Constructs a new age file filter for files on any one side
* of a certain File (whose last modification time will be used as
* reference).
*
* @param cutoffReference the file whose last modification
* time is usesd as the threshold age of the files
* @param acceptOlder if true, older files (at or before the cutoff)
* are accepted, else newer ones (after the cutoff).
*/
public AgeFileFilter(File cutoffReference, boolean acceptOlder) {
this(cutoffReference.lastModified(), acceptOlder);
}
//-----------------------------------------------------------------------
/**
* Checks to see if the last modification of the file matches cutoff
* favorably.
* <p>
* If last modification time equals cutoff and newer files are required,
* file <b>IS NOT</b> selected.
* If last modification time equals cutoff and older files are required,
* file <b>IS</b> selected.
*
* @param file the File to check
* @return true if the filename matches
*/
public boolean accept(File file) {
boolean newer = FileUtils.isFileNewer(file, cutoff);
return acceptOlder ? !newer : newer;
}
/**
* Provide a String representaion of this file filter.
*
* @return a String representaion
*/
public String toString() {
String condition = acceptOlder ? "<=" : ">";
return super.toString() + "(" + condition + cutoff + ")";
}
}

View File

@ -0,0 +1,167 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.filefilter;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* A {@link java.io.FileFilter} providing conditional AND logic across a list of
* file filters. This filter returns <code>true</code> if all filters in the
* list return <code>true</code>. Otherwise, it returns <code>false</code>.
* Checking of the file filter list stops when the first filter returns
* <code>false</code>.
*
* @since Commons IO 1.0
* @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $
*
* @author Steven Caswell
*/
public class AndFileFilter
extends AbstractFileFilter
implements ConditionalFileFilter, Serializable {
/** The list of file filters. */
private List fileFilters;
/**
* Constructs a new instance of <code>AndFileFilter</code>.
*
* @since Commons IO 1.1
*/
public AndFileFilter() {
this.fileFilters = new ArrayList();
}
/**
* Constructs a new instance of <code>AndFileFilter</code>
* with the specified list of filters.
*
* @param fileFilters a List of IOFileFilter instances, copied, null ignored
* @since Commons IO 1.1
*/
public AndFileFilter(final List fileFilters) {
if (fileFilters == null) {
this.fileFilters = new ArrayList();
} else {
this.fileFilters = new ArrayList(fileFilters);
}
}
/**
* Constructs a new file filter that ANDs the result of two other filters.
*
* @param filter1 the first filter, must not be null
* @param filter2 the second filter, must not be null
* @throws IllegalArgumentException if either filter is null
*/
public AndFileFilter(IOFileFilter filter1, IOFileFilter filter2) {
if (filter1 == null || filter2 == null) {
throw new IllegalArgumentException("The filters must not be null");
}
this.fileFilters = new ArrayList();
addFileFilter(filter1);
addFileFilter(filter2);
}
/**
* {@inheritDoc}
*/
public void addFileFilter(final IOFileFilter ioFileFilter) {
this.fileFilters.add(ioFileFilter);
}
/**
* {@inheritDoc}
*/
public List getFileFilters() {
return Collections.unmodifiableList(this.fileFilters);
}
/**
* {@inheritDoc}
*/
public boolean removeFileFilter(final IOFileFilter ioFileFilter) {
return this.fileFilters.remove(ioFileFilter);
}
/**
* {@inheritDoc}
*/
public void setFileFilters(final List fileFilters) {
this.fileFilters = new ArrayList(fileFilters);
}
/**
* {@inheritDoc}
*/
public boolean accept(final File file) {
if (this.fileFilters.size() == 0) {
return false;
}
for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) {
IOFileFilter fileFilter = (IOFileFilter) iter.next();
if (!fileFilter.accept(file)) {
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*/
public boolean accept(final File file, final String name) {
if (this.fileFilters.size() == 0) {
return false;
}
for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) {
IOFileFilter fileFilter = (IOFileFilter) iter.next();
if (!fileFilter.accept(file, name)) {
return false;
}
}
return true;
}
/**
* Provide a String representaion of this file filter.
*
* @return a String representaion
*/
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append(super.toString());
buffer.append("(");
if (fileFilters != null) {
for (int i = 0; i < fileFilters.size(); i++) {
if (i > 0) {
buffer.append(",");
}
Object filter = fileFilters.get(i);
buffer.append(filter == null ? "null" : filter.toString());
}
}
buffer.append(")");
return buffer.toString();
}
}

View File

@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.io.filefilter;
import java.io.File;
import java.io.Serializable;
/**
* This filter accepts <code>File</code>s that can be read.
* <p>
* Example, showing how to print out a list of the
* current directory's <i>readable</i> files:
*
* <pre>
* File dir = new File(".");
* String[] files = dir.list( CanReadFileFilter.CAN_READ );
* for ( int i = 0; i &lt; files.length; i++ ) {
* System.out.println(files[i]);
* }
* </pre>
*
* <p>
* Example, showing how to print out a list of the
* current directory's <i>un-readable</i> files:
*
* <pre>
* File dir = new File(".");
* String[] files = dir.list( CanReadFileFilter.CANNOT_READ );
* for ( int i = 0; i &lt; files.length; i++ ) {
* System.out.println(files[i]);
* }
* </pre>
*
* <p>
* Example, showing how to print out a list of the
* current directory's <i>read-only</i> files:
*
* <pre>
* File dir = new File(".");
* String[] files = dir.list( CanReadFileFilter.READ_ONLY );
* for ( int i = 0; i &lt; files.length; i++ ) {
* System.out.println(files[i]);
* }
* </pre>
*
* @since Commons IO 1.3
* @version $Revision: 587916 $
*/
public class CanReadFileFilter extends AbstractFileFilter implements Serializable {
/** Singleton instance of <i>readable</i> filter */
public static final IOFileFilter CAN_READ = new CanReadFileFilter();
/** Singleton instance of not <i>readable</i> filter */
public static final IOFileFilter CANNOT_READ = new NotFileFilter(CAN_READ);
/** Singleton instance of <i>read-only</i> filter */
public static final IOFileFilter READ_ONLY = new AndFileFilter(CAN_READ,
CanWriteFileFilter.CANNOT_WRITE);
/**
* Restrictive consructor.
*/
protected CanReadFileFilter() {
}
/**
* Checks to see if the file can be read.
*
* @param file the File to check.
* @return <code>true</code> if the file can be
* read, otherwise <code>false</code>.
*/
public boolean accept(File file) {
return file.canRead();
}
}

Some files were not shown because too many files have changed in this diff Show More