();
+ private Uri mUri;
+ private RandomAccessFile mMiniThumbFile;
+ private FileChannel mChannel;
+ private ByteBuffer mBuffer;
+
+ public MiniThumbFile(Uri uri) {
+ mUri = uri;
+ mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
+ }
+
+ protected static synchronized void reset() {
+ for (MiniThumbFile file : sThumbFiles.values())
+ file.deactivate();
+ sThumbFiles.clear();
+ }
+
+ protected static synchronized MiniThumbFile instance(Uri uri) {
+ String type = uri.getPathSegments().get(0);
+ MiniThumbFile file = sThumbFiles.get(type);
+ if (file == null) {
+ file = new MiniThumbFile(Uri.parse(MediaStore.CONTENT_AUTHORITY_SLASH + type + "/media"));
+ sThumbFiles.put(type, file);
+ }
+
+ return file;
+ }
+
+ private String randomAccessFilePath(int version) {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/" + Video.Thumbnails.THUMBNAILS_DIRECTORY;
+ return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
+ }
+
+ private void removeOldFile() {
+ String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
+ File oldFile = new File(oldPath);
+ if (oldFile.exists()) {
+ try {
+ oldFile.delete();
+ } catch (SecurityException ex) {
+ }
+ }
+ }
+
+ private RandomAccessFile miniThumbDataFile() {
+ if (mMiniThumbFile == null) {
+ removeOldFile();
+ String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
+ File directory = new File(path).getParentFile();
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs())
+ Log.e("Unable to create .thumbnails directory %s", directory.toString());
+ }
+ File f = new File(path);
+ try {
+ mMiniThumbFile = new RandomAccessFile(f, "rw");
+ } catch (IOException ex) {
+ try {
+ mMiniThumbFile = new RandomAccessFile(f, "r");
+ } catch (IOException ex2) {
+ }
+ }
+
+ if (mMiniThumbFile != null)
+ mChannel = mMiniThumbFile.getChannel();
+ }
+ return mMiniThumbFile;
+ }
+
+ protected synchronized void deactivate() {
+ if (mMiniThumbFile != null) {
+ try {
+ mMiniThumbFile.close();
+ mMiniThumbFile = null;
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ protected synchronized long getMagic(long id) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r != null) {
+ long pos = id * BYTES_PER_MINTHUMB;
+ FileLock lock = null;
+ try {
+ mBuffer.clear();
+ mBuffer.limit(1 + 8);
+
+ lock = mChannel.lock(pos, 1 + 8, true);
+ if (mChannel.read(mBuffer, pos) == 9) {
+ mBuffer.position(0);
+ if (mBuffer.get() == 1)
+ return mBuffer.getLong();
+ }
+ } catch (IOException ex) {
+ Log.e("Got exception checking file magic: ", ex);
+ } catch (RuntimeException ex) {
+ Log.e("Got exception when reading magic, id = %d, disk full or mount read-only? %s", id, ex.getClass().toString());
+ } finally {
+ try {
+ if (lock != null)
+ lock.release();
+ } catch (IOException ex) {
+ }
+ }
+ }
+ return 0;
+ }
+
+ protected synchronized void saveMiniThumbToFile(byte[] data, long id, long magic) throws IOException {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return;
+
+ long pos = id * BYTES_PER_MINTHUMB;
+ FileLock lock = null;
+ try {
+ if (data != null) {
+ if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE)
+ return;
+
+ mBuffer.clear();
+ mBuffer.put((byte) 1);
+ mBuffer.putLong(magic);
+ mBuffer.putInt(data.length);
+ mBuffer.put(data);
+ mBuffer.flip();
+
+ lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
+ mChannel.write(mBuffer, pos);
+ }
+ } catch (IOException ex) {
+ Log.e("couldn't save mini thumbnail data for %d; %s", id, ex.getMessage());
+ throw ex;
+ } catch (RuntimeException ex) {
+ Log.e("couldn't save mini thumbnail data for %d, disk full or mount read-only? %s", id, ex.getClass().toString());
+ } finally {
+ try {
+ if (lock != null)
+ lock.release();
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ protected synchronized byte[] getMiniThumbFromFile(long id, byte[] data) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return null;
+
+ long pos = id * BYTES_PER_MINTHUMB;
+ FileLock lock = null;
+ try {
+ mBuffer.clear();
+ lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);
+ int size = mChannel.read(mBuffer, pos);
+ if (size > 1 + 8 + 4) {
+ mBuffer.position(9);
+ int length = mBuffer.getInt();
+
+ if (size >= 1 + 8 + 4 + length && data.length >= length) {
+ mBuffer.get(data, 0, length);
+ return data;
+ }
+ }
+ } catch (IOException ex) {
+ Log.e("got exception when reading thumbnail id = %d, exception: %s", id, ex.getMessage());
+ } catch (RuntimeException ex) {
+ Log.e("Got exception when reading thumbnail, id = %d, disk full or mount read-only? %s", id, ex.getClass().toString());
+ } finally {
+ try {
+ if (lock != null)
+ lock.release();
+ } catch (IOException ex) {
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/vitamio/src/io/vov/vitamio/utils/Base64.java b/vitamio/src/io/vov/vitamio/utils/Base64.java
new file mode 100644
index 0000000..53ad670
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/Base64.java
@@ -0,0 +1,739 @@
+/*
+ * Copyright (C) 2010 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 io.vov.vitamio.utils;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs 2045 and 3548.
+ */
+public class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ /**
+ * Flag to pass to {@link Base64OutputStream} to indicate that it
+ * should not close the output stream it is wrapping when it
+ * itself is closed.
+ */
+ public static final int NO_CLOSE = 16;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ *
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(str.getBytes(), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len*3/4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -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, -2, -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, -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, -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, -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, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -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, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -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, 63,
+ -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, -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, -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, -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, -1, -1, -1, -1,
+ };
+
+ /** Non-data values in the DECODE arrays. */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 3/4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p+4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p+1] & 0xff] << 12) |
+ (alphabet[input[p+2] & 0xff] << 6) |
+ (alphabet[input[p+3] & 0xff]))) >= 0) {
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op+1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0: break;
+ case 1: output_len += 2; break;
+ case 2: output_len += 3; break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ assert encoder.op == output_len;
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * RFC 2045).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ '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', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ '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', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 8/5 + 10;
+ }
+
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+
+ case 1:
+ if (p+2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ };
+ break;
+
+ case 2:
+ if (p+1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p+3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p+1] & 0xff) << 8) |
+ (input[p+2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op+1] = alphabet[(v >> 12) & 0x3f];
+ output[op+2] = alphabet[(v >> 6) & 0x3f];
+ output[op+3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p-tailLen == len-1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p-tailLen == len-2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ assert tailLen == 0;
+ assert p == len;
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len-1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len-2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p+1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ private Base64() { } // don't instantiate
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/CPU.java b/vitamio/src/io/vov/vitamio/utils/CPU.java
new file mode 100644
index 0000000..3fd557d
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/CPU.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import android.os.Build;
+import android.text.TextUtils;
+
+public class CPU {
+ private static final Map cpuinfo = new HashMap();
+ private static int cachedFeature = -1;
+ private static String cachedFeatureString = null;
+ public static final int FEATURE_ARM_V5TE = 1 << 0;
+ public static final int FEATURE_ARM_V6 = 1 << 1;
+ public static final int FEATURE_ARM_VFP = 1 << 2;
+ public static final int FEATURE_ARM_V7A = 1 << 3;
+ public static final int FEATURE_ARM_VFPV3 = 1 << 4;
+ public static final int FEATURE_ARM_NEON = 1 << 5;
+ public static final int FEATURE_X86 = 1 << 6;
+ public static final int FEATURE_MIPS = 1 << 7;
+
+ public static String getFeatureString() {
+ getFeature();
+ return cachedFeatureString;
+ }
+
+ public static int getFeature() {
+ if (cachedFeature > 0)
+ return getCachedFeature();
+
+ cachedFeature = FEATURE_ARM_V5TE;
+
+ if (cpuinfo.isEmpty()) {
+ BufferedReader bis = null;
+ try {
+ bis = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
+ String line;
+ String[] pairs;
+ while ((line = bis.readLine()) != null) {
+ if (!line.trim().equals("")) {
+ pairs = line.split(":");
+ if (pairs.length > 1)
+ cpuinfo.put(pairs[0].trim(), pairs[1].trim());
+ }
+ }
+ } catch (Exception e) {
+ Log.e("getCPUFeature", e);
+ } finally {
+ try {
+ if (bis != null)
+ bis.close();
+ } catch (IOException e) {
+ Log.e("getCPUFeature", e);
+ }
+ }
+ }
+
+ if (!cpuinfo.isEmpty()) {
+ for (String key : cpuinfo.keySet())
+ Log.d("%s:%s", key, cpuinfo.get(key));
+
+ boolean hasARMv6 = false;
+ boolean hasARMv7 = false;
+
+ String val = cpuinfo.get("CPU architecture");
+ if (!TextUtils.isEmpty(val)) {
+ try {
+ int i = StringUtils.convertToInt(val);
+ Log.d("CPU architecture: %s", i);
+ if (i >= 7) {
+ hasARMv6 = true;
+ hasARMv7 = true;
+ } else if (i >= 6) {
+ hasARMv6 = true;
+ hasARMv7 = false;
+ }
+ } catch (NumberFormatException ex) {
+ Log.e("getCPUFeature", ex);
+ }
+
+ val = cpuinfo.get("Processor");
+ if (TextUtils.isEmpty(val)) {
+ val = cpuinfo.get("model name");
+ }
+ if (val != null && (val.contains("(v7l)") || val.contains("ARMv7"))) {
+ hasARMv6 = true;
+ hasARMv7 = true;
+ }
+ if (val != null && (val.contains("(v6l)") || val.contains("ARMv6"))) {
+ hasARMv6 = true;
+ hasARMv7 = false;
+ }
+
+ if (hasARMv6)
+ cachedFeature |= FEATURE_ARM_V6;
+ if (hasARMv7)
+ cachedFeature |= FEATURE_ARM_V7A;
+
+ val = cpuinfo.get("Features");
+ if (val != null) {
+ if (val.contains("neon"))
+ cachedFeature |= FEATURE_ARM_VFP | FEATURE_ARM_VFPV3 | FEATURE_ARM_NEON;
+ else if (val.contains("vfpv3"))
+ cachedFeature |= FEATURE_ARM_VFP | FEATURE_ARM_VFPV3;
+ else if (val.contains("vfp"))
+ cachedFeature |= FEATURE_ARM_VFP;
+ }
+ } else {
+ String vendor_id = cpuinfo.get("vendor_id");
+ String mips = cpuinfo.get("cpu model");
+ if (!TextUtils.isEmpty(vendor_id) && vendor_id.contains("GenuineIntel")) {
+ cachedFeature |= FEATURE_X86;
+ } else if (!TextUtils.isEmpty(mips) && mips.contains("MIPS")) {
+ cachedFeature |= FEATURE_MIPS;
+ }
+ }
+ }
+
+ return getCachedFeature();
+ }
+
+ private static int getCachedFeature() {
+ if (cachedFeatureString == null) {
+ StringBuffer sb = new StringBuffer();
+ if ((cachedFeature & FEATURE_ARM_V5TE) > 0)
+ sb.append("V5TE ");
+ if ((cachedFeature & FEATURE_ARM_V6) > 0)
+ sb.append("V6 ");
+ if ((cachedFeature & FEATURE_ARM_VFP) > 0)
+ sb.append("VFP ");
+ if ((cachedFeature & FEATURE_ARM_V7A) > 0)
+ sb.append("V7A ");
+ if ((cachedFeature & FEATURE_ARM_VFPV3) > 0)
+ sb.append("VFPV3 ");
+ if ((cachedFeature & FEATURE_ARM_NEON) > 0)
+ sb.append("NEON ");
+ if ((cachedFeature & FEATURE_X86) > 0)
+ sb.append("X86 ");
+ if ((cachedFeature & FEATURE_MIPS) > 0)
+ sb.append("MIPS ");
+ cachedFeatureString = sb.toString();
+ }
+ Log.d("GET CPU FATURE: %s", cachedFeatureString);
+ return cachedFeature;
+ }
+
+ public static boolean isDroidXDroid2() {
+ return (Build.MODEL.trim().equalsIgnoreCase("DROIDX") || Build.MODEL.trim().equalsIgnoreCase("DROID2") || Build.FINGERPRINT.toLowerCase().contains("shadow") || Build.FINGERPRINT.toLowerCase().contains("droid2"));
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/ContextUtils.java b/vitamio/src/io/vov/vitamio/utils/ContextUtils.java
new file mode 100644
index 0000000..57557a2
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/ContextUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+
+public class ContextUtils {
+ public static int getVersionCode(Context ctx) {
+ int version = 0;
+ try {
+ version = ctx.getPackageManager().getPackageInfo(ctx.getApplicationInfo().packageName, 0).versionCode;
+ } catch (Exception e) {
+ Log.e("getVersionInt", e);
+ }
+ return version;
+ }
+
+ public static String getDataDir(Context ctx) {
+ ApplicationInfo ai = ctx.getApplicationInfo();
+ if (ai.dataDir != null)
+ return fixLastSlash(ai.dataDir);
+ else
+ return "/data/data/" + ai.packageName + "/";
+ }
+
+ public static String fixLastSlash(String str) {
+ String res = str == null ? "/" : str.trim() + "/";
+ if (res.length() > 2 && res.charAt(res.length() - 2) == '/')
+ res = res.substring(0, res.length() - 1);
+ return res;
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/Crypto.java b/vitamio/src/io/vov/vitamio/utils/Crypto.java
new file mode 100644
index 0000000..228cb27
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/Crypto.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.spec.AlgorithmParameterSpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class Crypto {
+ private Cipher ecipher;
+
+ public Crypto(String key) {
+ try {
+ SecretKeySpec skey = new SecretKeySpec(generateKey(key), "AES");
+ setupCrypto(skey);
+ } catch (Exception e) {
+ Log.e("Crypto", e);
+ }
+ }
+
+ private void setupCrypto(SecretKey key) {
+ byte[] iv = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };
+ AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);
+ try {
+ ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ ecipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
+ } catch (Exception e) {
+ ecipher = null;
+ Log.e("setupCrypto", e);
+ }
+ }
+
+ public String encrypt(String plaintext) {
+ if (ecipher == null)
+ return "";
+
+ try {
+ byte[] ciphertext = ecipher.doFinal(plaintext.getBytes("UTF-8"));
+ return Base64.encodeToString(ciphertext, Base64.NO_WRAP);
+ } catch (Exception e) {
+ Log.e("encryp", e);
+ return "";
+ }
+ }
+
+ public static String md5(String plain) {
+ try {
+ MessageDigest m = MessageDigest.getInstance("MD5");
+ m.update(plain.getBytes());
+ byte[] digest = m.digest();
+ BigInteger bigInt = new BigInteger(1, digest);
+ String hashtext = bigInt.toString(16);
+ while (hashtext.length() < 32) {
+ hashtext = "0" + hashtext;
+ }
+ return hashtext;
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ private static byte[] generateKey(String input) {
+ try {
+ byte[] bytesOfMessage = input.getBytes("UTF-8");
+ MessageDigest md = MessageDigest.getInstance("SHA256");
+ return md.digest(bytesOfMessage);
+ } catch (Exception e) {
+ Log.e("generateKey", e);
+ return null;
+ }
+ }
+
+ private PublicKey readKeyFromStream(InputStream keyStream) throws IOException {
+ ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(keyStream));
+ try {
+ PublicKey pubKey = (PublicKey) oin.readObject();
+ return pubKey;
+ } catch (Exception e) {
+ Log.e("readKeyFromStream", e);
+ return null;
+ } finally {
+ oin.close();
+ }
+ }
+
+ public String rsaEncrypt(InputStream keyStream, String data) {
+ try {
+ return rsaEncrypt(keyStream, data.getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ return "";
+ }
+ }
+
+ public String rsaEncrypt(InputStream keyStream, byte[] data) {
+ try {
+ PublicKey pubKey = readKeyFromStream(keyStream);
+ Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, pubKey);
+ byte[] cipherData = cipher.doFinal(data);
+ return Base64.encodeToString(cipherData, Base64.NO_WRAP);
+ } catch (Exception e) {
+ Log.e("rsaEncrypt", e);
+ return "";
+ }
+ }
+
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/Device.java b/vitamio/src/io/vov/vitamio/utils/Device.java
new file mode 100644
index 0000000..2fed347
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/Device.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.os.Build;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.util.DisplayMetrics;
+
+import java.util.Locale;
+
+public class Device {
+ public static String getLocale() {
+ Locale locale = Locale.getDefault();
+ if (locale != null) {
+ String lo = locale.getLanguage();
+ Log.i("getLocale " + lo);
+ if (lo != null) {
+ return lo.toLowerCase();
+ }
+ }
+ return "en";
+ }
+
+ public static String getDeviceFeatures(Context ctx) {
+ return getIdentifiers(ctx) + getSystemFeatures() + getScreenFeatures(ctx);
+ }
+
+ @SuppressLint("NewApi")
+ public static String getIdentifiers(Context ctx) {
+ StringBuilder sb = new StringBuilder();
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO)
+ sb.append(getPair("serial", Build.SERIAL));
+ else
+ sb.append(getPair("serial", "No Serial"));
+ sb.append(getPair("android_id", Settings.Secure.getString(ctx.getContentResolver(), Settings.Secure.ANDROID_ID)));
+ TelephonyManager tel = (TelephonyManager) ctx.getSystemService(Context.TELEPHONY_SERVICE);
+ sb.append(getPair("sim_country_iso", tel.getSimCountryIso()));
+ sb.append(getPair("network_operator_name", tel.getNetworkOperatorName()));
+ sb.append(getPair("unique_id", Crypto.md5(sb.toString())));
+ ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
+ sb.append(getPair("network_type", cm.getActiveNetworkInfo() == null ? "-1" : String.valueOf(cm.getActiveNetworkInfo().getType())));
+ return sb.toString();
+ }
+
+ public static String getSystemFeatures() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getPair("android_release", Build.VERSION.RELEASE));
+ sb.append(getPair("android_sdk_int", "" + Build.VERSION.SDK_INT));
+ sb.append(getPair("device_cpu_abi", Build.CPU_ABI));
+ sb.append(getPair("device_model", Build.MODEL));
+ sb.append(getPair("device_manufacturer", Build.MANUFACTURER));
+ sb.append(getPair("device_board", Build.BOARD));
+ sb.append(getPair("device_fingerprint", Build.FINGERPRINT));
+ sb.append(getPair("device_cpu_feature", CPU.getFeatureString()));
+ return sb.toString();
+ }
+
+ public static String getScreenFeatures(Context ctx) {
+ StringBuilder sb = new StringBuilder();
+ DisplayMetrics disp = ctx.getResources().getDisplayMetrics();
+ sb.append(getPair("screen_density", "" + disp.density));
+ sb.append(getPair("screen_density_dpi", "" + disp.densityDpi));
+ sb.append(getPair("screen_height_pixels", "" + disp.heightPixels));
+ sb.append(getPair("screen_width_pixels", "" + disp.widthPixels));
+ sb.append(getPair("screen_scaled_density", "" + disp.scaledDensity));
+ sb.append(getPair("screen_xdpi", "" + disp.xdpi));
+ sb.append(getPair("screen_ydpi", "" + disp.ydpi));
+ return sb.toString();
+ }
+
+ private static String getPair(String key, String value) {
+ key = key == null ? "" : key.trim();
+ value = value == null ? "" : value.trim();
+ return "&" + key + "=" + value;
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/FileUtils.java b/vitamio/src/io/vov/vitamio/utils/FileUtils.java
new file mode 100644
index 0000000..2932052
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/FileUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileUtils {
+ private static final String FILE_NAME_RESERVED = "|\\?*<\":>+[]/'";
+
+ public static String getUniqueFileName(String name, String id) {
+ StringBuilder sb = new StringBuilder();
+ for (Character c : name.toCharArray()) {
+ if (FILE_NAME_RESERVED.indexOf(c) == -1) {
+ sb.append(c);
+ }
+ }
+ name = sb.toString();
+ if (name.length() > 16) {
+ name = name.substring(0, 16);
+ }
+ id = Crypto.md5(id);
+ name += id;
+ try {
+ File f = File.createTempFile(name, null);
+ if (f.exists()) {
+ f.delete();
+ return name;
+ }
+ } catch (IOException e) {
+ }
+ return id;
+ }
+
+ public static String getCanonical(File f) {
+ if (f == null)
+ return null;
+
+ try {
+ return f.getCanonicalPath();
+ } catch (IOException e) {
+ return f.getAbsolutePath();
+ }
+ }
+
+ /**
+ * Get the path for the file:/// only
+ *
+ * @param uri
+ * @return
+ */
+ public static String getPath(String uri) {
+ Log.i("FileUtils#getPath(%s)", uri);
+ if (TextUtils.isEmpty(uri))
+ return null;
+ if (uri.startsWith("file://") && uri.length() > 7)
+ return Uri.decode(uri.substring(7));
+ return Uri.decode(uri);
+ }
+
+ public static String getName(String uri) {
+ String path = getPath(uri);
+ if (path != null)
+ return new File(path).getName();
+ return null;
+ }
+
+ public static void deleteDir(File f) {
+ if (f.exists() && f.isDirectory()) {
+ for (File file : f.listFiles()) {
+ if (file.isDirectory())
+ deleteDir(file);
+ file.delete();
+ }
+ f.delete();
+ }
+ }
+}
\ No newline at end of file
diff --git a/vitamio/src/io/vov/vitamio/utils/IOUtils.java b/vitamio/src/io/vov/vitamio/utils/IOUtils.java
new file mode 100644
index 0000000..11eb8a7
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/IOUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import android.database.Cursor;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.Closeable;
+
+public class IOUtils {
+
+ private static final String TAG = "IOUtils";
+
+ public static void closeSilently(Closeable c) {
+ if (c == null)
+ return;
+ try {
+ c.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static void closeSilently(ParcelFileDescriptor c) {
+ if (c == null)
+ return;
+ try {
+ c.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static void closeSilently(Cursor cursor) {
+ try {
+ if (cursor != null) cursor.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/Log.java b/vitamio/src/io/vov/vitamio/utils/Log.java
new file mode 100644
index 0000000..f27db17
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/Log.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import io.vov.vitamio.BuildConfig;
+
+import java.util.MissingFormatArgumentException;
+
+public class Log {
+ public static final String TAG = "Vitamio[Player]";
+
+ public static void i(String msg, Object... args) {
+ try {
+ if (BuildConfig.DEBUG)
+ android.util.Log.i(TAG, String.format(msg, args));
+ } catch (MissingFormatArgumentException e) {
+ android.util.Log.e(TAG, "vitamio.Log", e);
+ android.util.Log.i(TAG, msg);
+ }
+ }
+
+ public static void d(String msg, Object... args) {
+ try {
+ if (BuildConfig.DEBUG)
+ android.util.Log.d(TAG, String.format(msg, args));
+ } catch (MissingFormatArgumentException e) {
+ android.util.Log.e(TAG, "vitamio.Log", e);
+ android.util.Log.d(TAG, msg);
+ }
+ }
+
+ public static void e(String msg, Object... args) {
+ try {
+ android.util.Log.e(TAG, String.format(msg, args));
+ } catch (MissingFormatArgumentException e) {
+ android.util.Log.e(TAG, "vitamio.Log", e);
+ android.util.Log.e(TAG, msg);
+ }
+ }
+
+ public static void e(String msg, Throwable t) {
+ android.util.Log.e(TAG, msg, t);
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/ScreenResolution.java b/vitamio/src/io/vov/vitamio/utils/ScreenResolution.java
new file mode 100644
index 0000000..bf5b0f9
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/ScreenResolution.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (C) 2013 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.view.Display;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+
+/**
+ * Class to get the real screen resolution includes the system status bar.
+ * We can get the value by calling the getRealMetrics method if API >= 17
+ * Reflection needed on old devices..
+ * */
+public class ScreenResolution {
+ /**
+ * Gets the resolution,
+ * @return a pair to return the width and height
+ * */
+ public static Pair getResolution(Context ctx){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return getRealResolution(ctx);
+ }
+ else {
+ return getRealResolutionOnOldDevice(ctx);
+ }
+ }
+
+ /**
+ * Gets resolution on old devices.
+ * Tries the reflection to get the real resolution first.
+ * Fall back to getDisplayMetrics if the above method failed.
+ * */
+ private static Pair getRealResolutionOnOldDevice(Context ctx) {
+ try{
+ WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Method mGetRawWidth = Display.class.getMethod("getRawWidth");
+ Method mGetRawHeight = Display.class.getMethod("getRawHeight");
+ Integer realWidth = (Integer) mGetRawWidth.invoke(display);
+ Integer realHeight = (Integer) mGetRawHeight.invoke(display);
+ return new Pair(realWidth, realHeight);
+ }
+ catch (Exception e) {
+ DisplayMetrics disp = ctx.getResources().getDisplayMetrics();
+ return new Pair(disp.widthPixels, disp.heightPixels);
+ }
+ }
+
+ /**
+ * Gets real resolution via the new getRealMetrics API.
+ * */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ private static Pair getRealResolution(Context ctx) {
+ WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ DisplayMetrics metrics = new DisplayMetrics();
+ display.getRealMetrics(metrics);
+ return new Pair(metrics.widthPixels, metrics.heightPixels);
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/utils/StringUtils.java b/vitamio/src/io/vov/vitamio/utils/StringUtils.java
new file mode 100644
index 0000000..9313b8e
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/utils/StringUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.utils;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+public class StringUtils {
+ public static String join(Object[] elements, CharSequence separator) {
+ return join(Arrays.asList(elements), separator);
+ }
+
+ public static String join(Iterable extends Object> elements, CharSequence separator) {
+ StringBuilder builder = new StringBuilder();
+
+ if (elements != null) {
+ Iterator extends Object> iter = elements.iterator();
+ if (iter.hasNext()) {
+ builder.append(String.valueOf(iter.next()));
+ while (iter.hasNext()) {
+ builder.append(separator).append(String.valueOf(iter.next()));
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ public static String fixLastSlash(String str) {
+ String res = str == null ? "/" : str.trim() + "/";
+ if (res.length() > 2 && res.charAt(res.length() - 2) == '/')
+ res = res.substring(0, res.length() - 1);
+ return res;
+ }
+
+ public static int convertToInt(String str) throws NumberFormatException {
+ int s, e;
+ for (s = 0; s < str.length(); s++)
+ if (Character.isDigit(str.charAt(s)))
+ break;
+ for (e = str.length(); e > 0; e--)
+ if (Character.isDigit(str.charAt(e - 1)))
+ break;
+ if (e > s) {
+ try {
+ return Integer.parseInt(str.substring(s, e));
+ } catch (NumberFormatException ex) {
+ Log.e("convertToInt", ex);
+ throw new NumberFormatException();
+ }
+ } else {
+ throw new NumberFormatException();
+ }
+ }
+
+ public static String generateTime(long time) {
+ int totalSeconds = (int) (time / 1000);
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ return hours > 0 ? String.format("%02d:%02d:%02d", hours, minutes, seconds) : String.format("%02d:%02d", minutes, seconds);
+ }
+
+}
diff --git a/vitamio/src/io/vov/vitamio/widget/CenterLayout.java b/vitamio/src/io/vov/vitamio/widget/CenterLayout.java
new file mode 100644
index 0000000..c01826f
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/widget/CenterLayout.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2013 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+@RemoteView
+public class CenterLayout extends ViewGroup {
+ private int mPaddingLeft = 0;
+ private int mPaddingRight = 0;
+ private int mPaddingTop = 0;
+ private int mPaddingBottom = 0;
+ private int mWidth, mHeight;
+
+ public CenterLayout(Context context) {
+ super(context);
+ }
+
+ public CenterLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CenterLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ int childRight;
+ int childBottom;
+
+ CenterLayout.LayoutParams lp = (CenterLayout.LayoutParams) child.getLayoutParams();
+
+ childRight = lp.x + child.getMeasuredWidth();
+ childBottom = lp.y + child.getMeasuredHeight();
+
+ maxWidth = Math.max(maxWidth, childRight);
+ maxHeight = Math.max(maxHeight, childBottom);
+ }
+ }
+
+ maxWidth += mPaddingLeft + mPaddingRight;
+ maxHeight += mPaddingTop + mPaddingBottom;
+
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), resolveSize(maxHeight, heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int count = getChildCount();
+ mWidth = getMeasuredWidth();
+ mHeight = getMeasuredHeight();
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ CenterLayout.LayoutParams lp = (CenterLayout.LayoutParams) child.getLayoutParams();
+ int childLeft = mPaddingLeft + lp.x;
+ if (lp.width > 0)
+ childLeft += (int) ((mWidth - lp.width) / 2.0);
+ else
+ childLeft += (int) ((mWidth - child.getMeasuredWidth()) / 2.0);
+ int childTop = mPaddingTop + lp.y;
+ if (lp.height > 0)
+ childTop += (int) ((mHeight - lp.height) / 2.0);
+ else
+ childTop += (int) ((mHeight - child.getMeasuredHeight()) / 2.0);
+ child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
+ }
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof CenterLayout.LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ public int x;
+ public int y;
+
+ public LayoutParams(int width, int height, int x, int y) {
+ super(width, height);
+ this.x = x;
+ this.y = y;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+}
diff --git a/vitamio/src/io/vov/vitamio/widget/MediaController.java b/vitamio/src/io/vov/vitamio/widget/MediaController.java
new file mode 100644
index 0000000..7441d65
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/widget/MediaController.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (C) 2013 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.widget;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.PopupWindow;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import java.lang.reflect.Method;
+
+import io.vov.vitamio.utils.Log;
+import io.vov.vitamio.utils.StringUtils;
+
+/**
+ * A view containing controls for a MediaPlayer. Typically contains the buttons
+ * like "Play/Pause" and a progress slider. It takes care of synchronizing the
+ * controls with the state of the MediaPlayer.
+ *
+ * The way to use this class is to a) instantiate it programatically or b)
+ * create it in your xml layout.
+ *
+ * a) The MediaController will create a default set of controls and put them in
+ * a window floating above your application. Specifically, the controls will
+ * float above the view specified with setAnchorView(). By default, the window
+ * will disappear if left idle for three seconds and reappear when the user
+ * touches the anchor view. To customize the MediaController's style, layout and
+ * controls you should extend MediaController and override the {#link
+ * {@link #makeControllerView()} method.
+ *
+ * b) The MediaController is a FrameLayout, you can put it in your layout xml
+ * and get it through {@link #findViewById(int)}.
+ *
+ * NOTES: In each way, if you want customize the MediaController, the SeekBar's
+ * id must be mediacontroller_progress, the Play/Pause's must be
+ * mediacontroller_pause, current time's must be mediacontroller_time_current,
+ * total time's must be mediacontroller_time_total, file name's must be
+ * mediacontroller_file_name. And your resources must have a pause_button
+ * drawable and a play_button drawable.
+ *
+ * Functions like show() and hide() have no effect when MediaController is
+ * created in an xml layout.
+ */
+public class MediaController extends FrameLayout {
+ private static final int sDefaultTimeout = 3000;
+ private static final int FADE_OUT = 1;
+ private static final int SHOW_PROGRESS = 2;
+ private MediaPlayerControl mPlayer;
+ private Context mContext;
+ private PopupWindow mWindow;
+ private int mAnimStyle;
+ private View mAnchor;
+ private View mRoot;
+ private SeekBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private TextView mFileName;
+ private OutlineTextView mInfoView;
+ private String mTitle;
+ private long mDuration;
+ private boolean mShowing;
+ private boolean mDragging;
+ private boolean mInstantSeeking = false;
+ private boolean mFromXml = false;
+ private ImageButton mPauseButton;
+ private AudioManager mAM;
+ private OnShownListener mShownListener;
+ private OnHiddenListener mHiddenListener;
+ @SuppressLint("HandlerLeak")
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ long pos;
+ switch (msg.what) {
+ case FADE_OUT:
+ hide();
+ break;
+ case SHOW_PROGRESS:
+ pos = setProgress();
+ if (!mDragging && mShowing) {
+ msg = obtainMessage(SHOW_PROGRESS);
+ sendMessageDelayed(msg, 1000 - (pos % 1000));
+ updatePausePlay();
+ }
+ break;
+ }
+ }
+ };
+ private View.OnClickListener mPauseListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ }
+ };
+ private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ public void onStartTrackingTouch(SeekBar bar) {
+ mDragging = true;
+ show(3600000);
+ mHandler.removeMessages(SHOW_PROGRESS);
+ if (mInstantSeeking)
+ mAM.setStreamMute(AudioManager.STREAM_MUSIC, true);
+ if (mInfoView != null) {
+ mInfoView.setText("");
+ mInfoView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
+ if (!fromuser)
+ return;
+
+ long newposition = (mDuration * progress) / 1000;
+ String time = StringUtils.generateTime(newposition);
+ if (mInstantSeeking)
+ mPlayer.seekTo(newposition);
+ if (mInfoView != null)
+ mInfoView.setText(time);
+ if (mCurrentTime != null)
+ mCurrentTime.setText(time);
+ }
+
+ public void onStopTrackingTouch(SeekBar bar) {
+ if (!mInstantSeeking)
+ mPlayer.seekTo((mDuration * bar.getProgress()) / 1000);
+ if (mInfoView != null) {
+ mInfoView.setText("");
+ mInfoView.setVisibility(View.GONE);
+ }
+ show(sDefaultTimeout);
+ mHandler.removeMessages(SHOW_PROGRESS);
+ mAM.setStreamMute(AudioManager.STREAM_MUSIC, false);
+ mDragging = false;
+ mHandler.sendEmptyMessageDelayed(SHOW_PROGRESS, 1000);
+ }
+ };
+
+ public MediaController(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRoot = this;
+ mFromXml = true;
+ initController(context);
+ }
+
+ public MediaController(Context context) {
+ super(context);
+ if (!mFromXml && initController(context))
+ initFloatingWindow();
+ }
+
+ private boolean initController(Context context) {
+ mContext = context;
+ mAM = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ return true;
+ }
+
+ @Override
+ public void onFinishInflate() {
+ if (mRoot != null)
+ initControllerView(mRoot);
+ }
+
+ private void initFloatingWindow() {
+ mWindow = new PopupWindow(mContext);
+ mWindow.setFocusable(false);
+ mWindow.setBackgroundDrawable(null);
+ mWindow.setOutsideTouchable(true);
+ mAnimStyle = android.R.style.Animation;
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public void setWindowLayoutType() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ try {
+ mAnchor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+ Method setWindowLayoutType = PopupWindow.class.getMethod("setWindowLayoutType", new Class[] { int.class });
+ setWindowLayoutType.invoke(mWindow, WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG);
+ } catch (Exception e) {
+ Log.e("setWindowLayoutType", e);
+ }
+ }
+ }
+
+ /**
+ * Set the view that acts as the anchor for the control view. This can for
+ * example be a VideoView, or your Activity's main view.
+ *
+ * @param view The view to which to anchor the controller when it is visible.
+ */
+ public void setAnchorView(View view) {
+ mAnchor = view;
+ if (!mFromXml) {
+ removeAllViews();
+ mRoot = makeControllerView();
+ mWindow.setContentView(mRoot);
+ mWindow.setWidth(LayoutParams.MATCH_PARENT);
+ mWindow.setHeight(LayoutParams.WRAP_CONTENT);
+ }
+ initControllerView(mRoot);
+ }
+
+ /**
+ * Create the view that holds the widgets that control playback. Derived
+ * classes can override this to create their own.
+ *
+ * @return The controller view.
+ */
+ protected View makeControllerView() {
+ return ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(getResources().getIdentifier("mediacontroller", "layout", mContext.getPackageName()), this);
+ }
+
+ private void initControllerView(View v) {
+ mPauseButton = (ImageButton) v.findViewById(getResources().getIdentifier("mediacontroller_play_pause", "id", mContext.getPackageName()));
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ mPauseButton.setOnClickListener(mPauseListener);
+ }
+
+ mProgress = (SeekBar) v.findViewById(getResources().getIdentifier("mediacontroller_seekbar", "id", mContext.getPackageName()));
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(mSeekListener);
+ }
+ mProgress.setMax(1000);
+ }
+
+ mEndTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_total", "id", mContext.getPackageName()));
+ mCurrentTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_current", "id", mContext.getPackageName()));
+ mFileName = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_file_name", "id", mContext.getPackageName()));
+ if (mFileName != null)
+ mFileName.setText(mTitle);
+ }
+
+ public void setMediaPlayer(MediaPlayerControl player) {
+ mPlayer = player;
+ updatePausePlay();
+ }
+
+ /**
+ * Control the action when the seekbar dragged by user
+ *
+ * @param seekWhenDragging True the media will seek periodically
+ */
+ public void setInstantSeeking(boolean seekWhenDragging) {
+ mInstantSeeking = seekWhenDragging;
+ }
+
+ public void show() {
+ show(sDefaultTimeout);
+ }
+
+ /**
+ * Set the content of the file_name TextView
+ *
+ * @param name
+ */
+ public void setFileName(String name) {
+ mTitle = name;
+ if (mFileName != null)
+ mFileName.setText(mTitle);
+ }
+
+ /**
+ * Set the View to hold some information when interact with the
+ * MediaController
+ *
+ * @param v
+ */
+ public void setInfoView(OutlineTextView v) {
+ mInfoView = v;
+ }
+
+ /**
+ *
+ * Change the animation style resource for this controller.
+ *
+ *
+ *
+ * If the controller is showing, calling this method will take effect only the
+ * next time the controller is shown.
+ *
+ *
+ * @param animationStyle animation style to use when the controller appears
+ * and disappears. Set to -1 for the default animation, 0 for no animation, or
+ * a resource identifier for an explicit animation.
+ */
+ public void setAnimationStyle(int animationStyle) {
+ mAnimStyle = animationStyle;
+ }
+
+ /**
+ * Show the controller on screen. It will go away automatically after
+ * 'timeout' milliseconds of inactivity.
+ *
+ * @param timeout The timeout in milliseconds. Use 0 to show the controller
+ * until hide() is called.
+ */
+ public void show(int timeout) {
+ if (!mShowing && mAnchor != null && mAnchor.getWindowToken() != null) {
+ if (mPauseButton != null)
+ mPauseButton.requestFocus();
+
+ if (mFromXml) {
+ setVisibility(View.VISIBLE);
+ } else {
+ int[] location = new int[2];
+
+ mAnchor.getLocationOnScreen(location);
+ Rect anchorRect = new Rect(location[0], location[1], location[0] + mAnchor.getWidth(), location[1] + mAnchor.getHeight());
+
+ mWindow.setAnimationStyle(mAnimStyle);
+ setWindowLayoutType();
+ mWindow.showAtLocation(mAnchor, Gravity.NO_GRAVITY, anchorRect.left, anchorRect.bottom);
+ }
+ mShowing = true;
+ if (mShownListener != null)
+ mShownListener.onShown();
+ }
+ updatePausePlay();
+ mHandler.sendEmptyMessage(SHOW_PROGRESS);
+
+ if (timeout != 0) {
+ mHandler.removeMessages(FADE_OUT);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(FADE_OUT), timeout);
+ }
+ }
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ public void hide() {
+ if (mAnchor == null)
+ return;
+
+ if (mShowing) {
+ try {
+ mHandler.removeMessages(SHOW_PROGRESS);
+ if (mFromXml)
+ setVisibility(View.GONE);
+ else
+ mWindow.dismiss();
+ } catch (IllegalArgumentException ex) {
+ Log.d("MediaController already removed");
+ }
+ mShowing = false;
+ if (mHiddenListener != null)
+ mHiddenListener.onHidden();
+ }
+ }
+
+ public void setOnShownListener(OnShownListener l) {
+ mShownListener = l;
+ }
+
+ public void setOnHiddenListener(OnHiddenListener l) {
+ mHiddenListener = l;
+ }
+
+ private long setProgress() {
+ if (mPlayer == null || mDragging)
+ return 0;
+
+ long position = mPlayer.getCurrentPosition();
+ long duration = mPlayer.getDuration();
+ if (mProgress != null) {
+ if (duration > 0) {
+ long pos = 1000L * position / duration;
+ mProgress.setProgress((int) pos);
+ }
+ int percent = mPlayer.getBufferPercentage();
+ mProgress.setSecondaryProgress(percent * 10);
+ }
+
+ mDuration = duration;
+
+ if (mEndTime != null)
+ mEndTime.setText(StringUtils.generateTime(mDuration));
+ if (mCurrentTime != null)
+ mCurrentTime.setText(StringUtils.generateTime(position));
+
+ return position;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ show(sDefaultTimeout);
+ return true;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ show(sDefaultTimeout);
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (event.getRepeatCount() == 0 && (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_SPACE)) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ if (mPauseButton != null)
+ mPauseButton.requestFocus();
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) {
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ updatePausePlay();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+ hide();
+ return true;
+ } else {
+ show(sDefaultTimeout);
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ private void updatePausePlay() {
+ if (mRoot == null || mPauseButton == null)
+ return;
+
+ if (mPlayer.isPlaying())
+ mPauseButton.setImageResource(getResources().getIdentifier("mediacontroller_pause", "drawable", mContext.getPackageName()));
+ else
+ mPauseButton.setImageResource(getResources().getIdentifier("mediacontroller_play", "drawable", mContext.getPackageName()));
+ }
+
+ private void doPauseResume() {
+ if (mPlayer.isPlaying())
+ mPlayer.pause();
+ else
+ mPlayer.start();
+ updatePausePlay();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (mPauseButton != null)
+ mPauseButton.setEnabled(enabled);
+ if (mProgress != null)
+ mProgress.setEnabled(enabled);
+ super.setEnabled(enabled);
+ }
+
+ public interface OnShownListener {
+ public void onShown();
+ }
+
+ public interface OnHiddenListener {
+ public void onHidden();
+ }
+
+ public interface MediaPlayerControl {
+ void start();
+
+ void pause();
+
+ long getDuration();
+
+ long getCurrentPosition();
+
+ void seekTo(long pos);
+
+ boolean isPlaying();
+
+ int getBufferPercentage();
+ }
+
+}
diff --git a/vitamio/src/io/vov/vitamio/widget/OutlineTextView.java b/vitamio/src/io/vov/vitamio/widget/OutlineTextView.java
new file mode 100644
index 0000000..807ae80
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/widget/OutlineTextView.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2013 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * Display text with border, use the same XML attrs as
+ * {@link android.widget.TextView}, except that {@link OutlineTextView} will
+ * transform the shadow to border
+ */
+public class OutlineTextView extends TextView {
+ private TextPaint mTextPaint;
+ private TextPaint mTextPaintOutline;
+ private String mText = "";
+ private int mAscent = 0;
+ private float mBorderSize;
+ private int mBorderColor;
+ private int mColor;
+ private float mSpacingMult = 1.0f;
+ private float mSpacingAdd = 0;
+ private boolean mIncludePad = true;
+
+ public OutlineTextView(Context context) {
+ super(context);
+ initPaint();
+ }
+
+ public OutlineTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initPaint();
+ }
+
+ public OutlineTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initPaint();
+ }
+
+ private void initPaint() {
+ mTextPaint = new TextPaint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setTextSize(getTextSize());
+ mTextPaint.setColor(mColor);
+ mTextPaint.setStyle(Paint.Style.FILL);
+ mTextPaint.setTypeface(getTypeface());
+
+ mTextPaintOutline = new TextPaint();
+ mTextPaintOutline.setAntiAlias(true);
+ mTextPaintOutline.setTextSize(getTextSize());
+ mTextPaintOutline.setColor(mBorderColor);
+ mTextPaintOutline.setStyle(Paint.Style.STROKE);
+ mTextPaintOutline.setTypeface(getTypeface());
+ mTextPaintOutline.setStrokeWidth(mBorderSize);
+ }
+
+ public void setText(String text) {
+ super.setText(text);
+ mText = text.toString();
+ requestLayout();
+ invalidate();
+ }
+
+ public void setTextSize(float size) {
+ super.setTextSize(size);
+ requestLayout();
+ invalidate();
+ initPaint();
+ }
+
+ public void setTextColor(int color) {
+ super.setTextColor(color);
+ mColor = color;
+ invalidate();
+ initPaint();
+ }
+
+ public void setShadowLayer(float radius, float dx, float dy, int color) {
+ super.setShadowLayer(radius, dx, dy, color);
+ mBorderSize = radius;
+ mBorderColor = color;
+ requestLayout();
+ invalidate();
+ initPaint();
+ }
+
+ public void setTypeface(Typeface tf, int style) {
+ super.setTypeface(tf, style);
+ requestLayout();
+ invalidate();
+ initPaint();
+ }
+
+ public void setTypeface(Typeface tf) {
+ super.setTypeface(tf);
+ requestLayout();
+ invalidate();
+ initPaint();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ Layout layout = new StaticLayout(getText(), mTextPaintOutline, getWidth(), Layout.Alignment.ALIGN_CENTER, mSpacingMult, mSpacingAdd, mIncludePad);
+ layout.draw(canvas);
+ layout = new StaticLayout(getText(), mTextPaint, getWidth(), Layout.Alignment.ALIGN_CENTER, mSpacingMult, mSpacingAdd, mIncludePad);
+ layout.draw(canvas);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Layout layout = new StaticLayout(getText(), mTextPaintOutline, measureWidth(widthMeasureSpec), Layout.Alignment.ALIGN_CENTER, mSpacingMult, mSpacingAdd, mIncludePad);
+ int ex = (int) (mBorderSize * 2 + 1);
+ setMeasuredDimension(measureWidth(widthMeasureSpec) + ex, measureHeight(heightMeasureSpec) * layout.getLineCount() + ex);
+ }
+
+ private int measureWidth(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY) {
+ result = specSize;
+ } else {
+ result = (int) mTextPaintOutline.measureText(mText) + getPaddingLeft() + getPaddingRight();
+ if (specMode == MeasureSpec.AT_MOST) {
+ result = Math.min(result, specSize);
+ }
+ }
+
+ return result;
+ }
+
+ private int measureHeight(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ mAscent = (int) mTextPaintOutline.ascent();
+ if (specMode == MeasureSpec.EXACTLY) {
+ result = specSize;
+ } else {
+ result = (int) (-mAscent + mTextPaintOutline.descent()) + getPaddingTop() + getPaddingBottom();
+ if (specMode == MeasureSpec.AT_MOST) {
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/vitamio/src/io/vov/vitamio/widget/VideoView.java b/vitamio/src/io/vov/vitamio/widget/VideoView.java
new file mode 100644
index 0000000..5647368
--- /dev/null
+++ b/vitamio/src/io/vov/vitamio/widget/VideoView.java
@@ -0,0 +1,772 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ * Copyright (C) 2013 YIXIA.COM
+ *
+ * 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 io.vov.vitamio.widget;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+
+import io.vov.vitamio.MediaFormat;
+import io.vov.vitamio.MediaPlayer;
+import io.vov.vitamio.MediaPlayer.OnBufferingUpdateListener;
+import io.vov.vitamio.MediaPlayer.OnCompletionListener;
+import io.vov.vitamio.MediaPlayer.OnErrorListener;
+import io.vov.vitamio.MediaPlayer.OnInfoListener;
+import io.vov.vitamio.MediaPlayer.OnPreparedListener;
+import io.vov.vitamio.MediaPlayer.OnSeekCompleteListener;
+import io.vov.vitamio.MediaPlayer.OnTimedTextListener;
+import io.vov.vitamio.MediaPlayer.OnVideoSizeChangedListener;
+import io.vov.vitamio.MediaPlayer.TrackInfo;
+import io.vov.vitamio.Vitamio;
+import io.vov.vitamio.utils.Log;
+import io.vov.vitamio.utils.ScreenResolution;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Displays a video file. The VideoView class can load images from various
+ * sources (such as resources or content providers), takes care of computing its
+ * measurement from the video so that it can be used in any layout manager, and
+ * provides various display options such as scaling and tinting.
+ *
+ * VideoView also provide many wrapper methods for
+ * {@link io.vov.vitamio.MediaPlayer}, such as {@link #getVideoWidth()},
+ * {@link #setTimedTextShown(boolean)}
+ */
+public class VideoView extends SurfaceView implements MediaController.MediaPlayerControl {
+ public static final int VIDEO_LAYOUT_ORIGIN = 0;
+ public static final int VIDEO_LAYOUT_SCALE = 1;
+ public static final int VIDEO_LAYOUT_STRETCH = 2;
+ public static final int VIDEO_LAYOUT_ZOOM = 3;
+ public static final int VIDEO_LAYOUT_FIT_PARENT = 4;
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+ private static final int STATE_SUSPEND = 6;
+ private static final int STATE_RESUME = 7;
+ private static final int STATE_SUSPEND_UNSUPPORTED = 8;
+ OnVideoSizeChangedListener mSizeChangedListener = new OnVideoSizeChangedListener() {
+ public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+ Log.d("onVideoSizeChanged: (%dx%d)", width, height);
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ mVideoAspectRatio = mp.getVideoAspectRatio();
+ if (mVideoWidth != 0 && mVideoHeight != 0)
+ setVideoLayout(mVideoLayout, mAspectRatio);
+ }
+ };
+ OnPreparedListener mPreparedListener = new OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ Log.d("onPrepared");
+ mCurrentState = STATE_PREPARED;
+ // mTargetState = STATE_PLAYING;
+
+ // Get the capabilities of the player for this stream
+ //TODO mCanPause
+
+ if (mOnPreparedListener != null)
+ mOnPreparedListener.onPrepared(mMediaPlayer);
+ if (mMediaController != null)
+ mMediaController.setEnabled(true);
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ mVideoAspectRatio = mp.getVideoAspectRatio();
+
+ long seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0)
+ seekTo(seekToPosition);
+
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ setVideoLayout(mVideoLayout, mAspectRatio);
+ if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
+ if (mTargetState == STATE_PLAYING) {
+ start();
+ if (mMediaController != null)
+ mMediaController.show();
+ } else if (!isPlaying() && (seekToPosition != 0 || getCurrentPosition() > 0)) {
+ if (mMediaController != null)
+ mMediaController.show(0);
+ }
+ }
+ } else if (mTargetState == STATE_PLAYING) {
+ start();
+ }
+ }
+ };
+ SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() {
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ mSurfaceWidth = w;
+ mSurfaceHeight = h;
+ boolean isValidState = (mTargetState == STATE_PLAYING);
+ boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
+ if (mMediaPlayer != null && isValidState && hasValidSize) {
+ if (mSeekWhenPrepared != 0)
+ seekTo(mSeekWhenPrepared);
+ start();
+ if (mMediaController != null) {
+ if (mMediaController.isShowing())
+ mMediaController.hide();
+ mMediaController.show();
+ }
+ }
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceHolder = holder;
+ if (mMediaPlayer != null && mCurrentState == STATE_SUSPEND && mTargetState == STATE_RESUME) {
+ mMediaPlayer.setDisplay(mSurfaceHolder);
+ resume();
+ } else {
+ openVideo();
+ }
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurfaceHolder = null;
+ if (mMediaController != null)
+ mMediaController.hide();
+ release(true);
+ }
+ };
+ private Uri mUri;
+ private long mDuration;
+ private int mCurrentState = STATE_IDLE;
+ private int mTargetState = STATE_IDLE;
+ private float mAspectRatio = 0;
+ private int mVideoLayout = VIDEO_LAYOUT_SCALE;
+ private SurfaceHolder mSurfaceHolder = null;
+ private MediaPlayer mMediaPlayer = null;
+ private int mVideoWidth;
+ private int mVideoHeight;
+ private float mVideoAspectRatio;
+ private int mVideoChroma = MediaPlayer.VIDEOCHROMA_RGBA;
+ private boolean mHardwareDecoder = false;
+ private int mSurfaceWidth;
+ private int mSurfaceHeight;
+ private MediaController mMediaController;
+ private View mMediaBufferingIndicator;
+ private OnCompletionListener mOnCompletionListener;
+ private OnPreparedListener mOnPreparedListener;
+ private OnErrorListener mOnErrorListener;
+ private OnSeekCompleteListener mOnSeekCompleteListener;
+ private OnTimedTextListener mOnTimedTextListener;
+ private OnInfoListener mOnInfoListener;
+ private OnBufferingUpdateListener mOnBufferingUpdateListener;
+ private int mCurrentBufferPercentage;
+ private long mSeekWhenPrepared; // recording the seek position while preparing
+ private Context mContext;
+ private Map mHeaders;
+ private int mBufSize;
+ private OnCompletionListener mCompletionListener = new OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ Log.d("onCompletion");
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ if (mMediaController != null)
+ mMediaController.hide();
+ if (mOnCompletionListener != null)
+ mOnCompletionListener.onCompletion(mMediaPlayer);
+ }
+ };
+ private OnErrorListener mErrorListener = new OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
+ Log.d("Error: %d, %d", framework_err, impl_err);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ if (mMediaController != null)
+ mMediaController.hide();
+
+ if (mOnErrorListener != null) {
+ if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err))
+ return true;
+ }
+
+ if (getWindowToken() != null) {
+ int message = framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK ? getResources().getIdentifier("VideoView_error_text_invalid_progressive_playback", "string", mContext.getPackageName()): getResources().getIdentifier("VideoView_error_text_unknown", "string", mContext.getPackageName());
+
+ new AlertDialog.Builder(mContext).setTitle(getResources().getIdentifier("VideoView_error_title", "string", mContext.getPackageName())).setMessage(message).setPositiveButton(getResources().getIdentifier("VideoView_error_button", "string", mContext.getPackageName()), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (mOnCompletionListener != null)
+ mOnCompletionListener.onCompletion(mMediaPlayer);
+ }
+ }).setCancelable(false).show();
+ }
+ return true;
+ }
+ };
+ private OnBufferingUpdateListener mBufferingUpdateListener = new OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ if (mOnBufferingUpdateListener != null)
+ mOnBufferingUpdateListener.onBufferingUpdate(mp, percent);
+ }
+ };
+ private OnInfoListener mInfoListener = new OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ Log.d("onInfo: (%d, %d)", what, extra);
+
+ if(MediaPlayer.MEDIA_INFO_UNKNOW_TYPE == what){
+ Log.e(" VITAMIO--TYPE_CHECK stype not include onInfo mediaplayer unknow type ");
+ }
+
+ if(MediaPlayer.MEDIA_INFO_FILE_OPEN_OK == what){
+ long buffersize=mMediaPlayer.audioTrackInit();
+ mMediaPlayer.audioInitedOk(buffersize);
+ }
+
+ Log.d("onInfo: (%d, %d)", what, extra);
+
+ if (mOnInfoListener != null) {
+ mOnInfoListener.onInfo(mp, what, extra);
+ } else if (mMediaPlayer != null) {
+ if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
+ mMediaPlayer.pause();
+ if (mMediaBufferingIndicator != null)
+ mMediaBufferingIndicator.setVisibility(View.VISIBLE);
+ } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
+ mMediaPlayer.start();
+ if (mMediaBufferingIndicator != null)
+ mMediaBufferingIndicator.setVisibility(View.GONE);
+ }
+ }
+ return true;
+ }
+ };
+ private OnSeekCompleteListener mSeekCompleteListener = new OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ Log.d("onSeekComplete");
+ if (mOnSeekCompleteListener != null)
+ mOnSeekCompleteListener.onSeekComplete(mp);
+ }
+ };
+ private OnTimedTextListener mTimedTextListener = new OnTimedTextListener() {
+ @Override
+ public void onTimedTextUpdate(byte[] pixels, int width, int height) {
+ Log.i("onSubtitleUpdate: bitmap subtitle, %dx%d", width, height);
+ if (mOnTimedTextListener != null)
+ mOnTimedTextListener.onTimedTextUpdate(pixels, width, height);
+ }
+
+ @Override
+ public void onTimedText(String text) {
+ Log.i("onSubtitleUpdate: %s", text);
+ if (mOnTimedTextListener != null)
+ mOnTimedTextListener.onTimedText(text);
+ }
+ };
+
+ public VideoView(Context context) {
+ super(context);
+ initVideoView(context);
+ }
+
+ public VideoView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ initVideoView(context);
+ }
+
+ public VideoView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initVideoView(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
+ int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * Set the display options
+ *
+ * @param layout
+ * - {@link #VIDEO_LAYOUT_ORIGIN}
+ *
- {@link #VIDEO_LAYOUT_SCALE}
+ *
- {@link #VIDEO_LAYOUT_STRETCH}
+ *
- {@link #VIDEO_LAYOUT_FIT_PARENT}
+ *
- {@link #VIDEO_LAYOUT_ZOOM}
+ *
+ * @param aspectRatio video aspect ratio, will audo detect if 0.
+ */
+ public void setVideoLayout(int layout, float aspectRatio) {
+ LayoutParams lp = getLayoutParams();
+ Pair res = ScreenResolution.getResolution(mContext);
+ int windowWidth = res.first.intValue(), windowHeight = res.second.intValue();
+ float windowRatio = windowWidth / (float) windowHeight;
+ float videoRatio = aspectRatio <= 0.01f ? mVideoAspectRatio : aspectRatio;
+ mSurfaceHeight = mVideoHeight;
+ mSurfaceWidth = mVideoWidth;
+ if (VIDEO_LAYOUT_ORIGIN == layout && mSurfaceWidth < windowWidth && mSurfaceHeight < windowHeight) {
+ lp.width = (int) (mSurfaceHeight * videoRatio);
+ lp.height = mSurfaceHeight;
+ } else if (layout == VIDEO_LAYOUT_ZOOM) {
+ lp.width = windowRatio > videoRatio ? windowWidth : (int) (videoRatio * windowHeight);
+ lp.height = windowRatio < videoRatio ? windowHeight : (int) (windowWidth / videoRatio);
+ } else if (layout == VIDEO_LAYOUT_FIT_PARENT) {
+ ViewGroup parent = (ViewGroup) getParent();
+ float parentRatio = ((float) parent.getWidth()) / ((float) parent.getHeight());
+ lp.width = (parentRatio < videoRatio) ? parent.getWidth() : Math.round(((float) parent.getHeight()) * videoRatio);
+ lp.height = (parentRatio > videoRatio) ? parent.getHeight() : Math.round(((float) parent.getWidth()) / videoRatio);
+ } else {
+ boolean full = layout == VIDEO_LAYOUT_STRETCH;
+ lp.width = (full || windowRatio < videoRatio) ? windowWidth : (int) (videoRatio * windowHeight);
+ lp.height = (full || windowRatio > videoRatio) ? windowHeight : (int) (windowWidth / videoRatio);
+ }
+ setLayoutParams(lp);
+ getHolder().setFixedSize(mSurfaceWidth, mSurfaceHeight);
+ Log.d("VIDEO: %dx%dx%f, Surface: %dx%d, LP: %dx%d, Window: %dx%dx%f", mVideoWidth, mVideoHeight, mVideoAspectRatio, mSurfaceWidth, mSurfaceHeight, lp.width, lp.height, windowWidth, windowHeight, windowRatio);
+ mVideoLayout = layout;
+ mAspectRatio = aspectRatio;
+ }
+
+ @SuppressWarnings("deprecation")
+ private void initVideoView(Context ctx) {
+ mContext = ctx;
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ getHolder().setFormat(PixelFormat.RGBA_8888); // PixelFormat.RGB_565
+ getHolder().addCallback(mSHCallback);
+ // this value only use Hardware decoder before Android 2.3
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && mHardwareDecoder) {
+ getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ }
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (ctx instanceof Activity)
+ ((Activity) ctx).setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
+ public boolean isValid() {
+ return (mSurfaceHolder != null && mSurfaceHolder.getSurface().isValid());
+ }
+
+ public void setVideoPath(String path) {
+ setVideoURI(Uri.parse(path));
+ }
+
+ public void setVideoURI(Uri uri) {
+ setVideoURI(uri, null);
+ }
+
+ public void setVideoURI(Uri uri, Map headers) {
+ mUri = uri;
+ mHeaders = headers;
+ mSeekWhenPrepared = 0;
+ openVideo();
+ requestLayout();
+ invalidate();
+ }
+
+ public void stopPlayback() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ }
+ }
+
+ private void openVideo() {
+// if (mUri == null || mSurfaceHolder == null || !Vitamio.isInitialized(mContext))
+// return;
+ if (mUri == null || mSurfaceHolder == null )
+ return;
+ Intent i = new Intent("com.android.music.musicservicecommand");
+ i.putExtra("command", "pause");
+ mContext.sendBroadcast(i);
+
+ release(false);
+ try {
+ mDuration = -1;
+ mCurrentBufferPercentage = 0;
+ mMediaPlayer = new MediaPlayer(mContext, mHardwareDecoder);
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
+ mMediaPlayer.setOnTimedTextListener(mTimedTextListener);
+ mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
+
+ mMediaPlayer.setDisplay(mSurfaceHolder);
+ mMediaPlayer.setBufferSize(mBufSize);
+ mMediaPlayer.setVideoChroma(mVideoChroma == MediaPlayer.VIDEOCHROMA_RGB565 ? MediaPlayer.VIDEOCHROMA_RGB565 : MediaPlayer.VIDEOCHROMA_RGBA);
+ mMediaPlayer.setScreenOnWhilePlaying(true);
+ mMediaPlayer.prepareAsync();
+ mCurrentState = STATE_PREPARING;
+ attachMediaController();
+ } catch (IOException ex) {
+ Log.e("Unable to open content: " + mUri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ return;
+ } catch (IllegalArgumentException ex) {
+ Log.e("Unable to open content: " + mUri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+ return;
+ }
+ }
+
+ public void setMediaController(MediaController controller) {
+ if (mMediaController != null)
+ mMediaController.hide();
+ mMediaController = controller;
+ attachMediaController();
+ }
+
+ public void setMediaBufferingIndicator(View mediaBufferingIndicator) {
+ if (mMediaBufferingIndicator != null)
+ mMediaBufferingIndicator.setVisibility(View.GONE);
+ mMediaBufferingIndicator = mediaBufferingIndicator;
+ }
+
+ private void attachMediaController() {
+ if (mMediaPlayer != null && mMediaController != null) {
+ mMediaController.setMediaPlayer(this);
+ View anchorView = this.getParent() instanceof View ? (View) this.getParent() : this;
+ mMediaController.setAnchorView(anchorView);
+ mMediaController.setEnabled(isInPlaybackState());
+
+ if (mUri != null) {
+ List paths = mUri.getPathSegments();
+ String name = paths == null || paths.isEmpty() ? "null" : paths.get(paths.size() - 1);
+ mMediaController.setFileName(name);
+ }
+ }
+ }
+
+ public void setOnPreparedListener(OnPreparedListener l) {
+ mOnPreparedListener = l;
+ }
+
+ public void setOnCompletionListener(OnCompletionListener l) {
+ mOnCompletionListener = l;
+ }
+
+ public void setOnErrorListener(OnErrorListener l) {
+ mOnErrorListener = l;
+ }
+
+ public void setOnBufferingUpdateListener(OnBufferingUpdateListener l) {
+ mOnBufferingUpdateListener = l;
+ }
+
+ public void setOnSeekCompleteListener(OnSeekCompleteListener l) {
+ mOnSeekCompleteListener = l;
+ }
+
+ public void setOnTimedTextListener(OnTimedTextListener l) {
+ mOnTimedTextListener = l;
+ }
+
+ public void setOnInfoListener(OnInfoListener l) {
+ mOnInfoListener = l;
+ }
+
+ private void release(boolean cleartargetstate) {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mCurrentState = STATE_IDLE;
+ if (cleartargetstate)
+ mTargetState = STATE_IDLE;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (isInPlaybackState() && mMediaController != null)
+ toggleMediaControlsVisiblity();
+ return false;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ if (isInPlaybackState() && mMediaController != null)
+ toggleMediaControlsVisiblity();
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_CALL && keyCode != KeyEvent.KEYCODE_ENDCALL;
+ if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_SPACE) {
+ if (mMediaPlayer.isPlaying()) {
+ pause();
+ mMediaController.show();
+ } else {
+ start();
+ mMediaController.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (!mMediaPlayer.isPlaying()) {
+ start();
+ mMediaController.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ pause();
+ mMediaController.show();
+ }
+ return true;
+ } else {
+ toggleMediaControlsVisiblity();
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void toggleMediaControlsVisiblity() {
+ if (mMediaController.isShowing()) {
+ mMediaController.hide();
+ } else {
+ mMediaController.show();
+ }
+ }
+
+ public void start() {
+ if (isInPlaybackState()) {
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ }
+ mTargetState = STATE_PLAYING;
+ }
+
+ public void pause() {
+ if (isInPlaybackState()) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ }
+
+ public void suspend() {
+ if (isInPlaybackState()) {
+ release(false);
+ mCurrentState = STATE_SUSPEND_UNSUPPORTED;
+ Log.d("Unable to suspend video. Release MediaPlayer.");
+ }
+ }
+
+ public void resume() {
+ if (mSurfaceHolder == null && mCurrentState == STATE_SUSPEND) {
+ mTargetState = STATE_RESUME;
+ } else if (mCurrentState == STATE_SUSPEND_UNSUPPORTED) {
+ openVideo();
+ }
+ }
+
+ public long getDuration() {
+ if (isInPlaybackState()) {
+ if (mDuration > 0)
+ return mDuration;
+ mDuration = mMediaPlayer.getDuration();
+ return mDuration;
+ }
+ mDuration = -1;
+ return mDuration;
+ }
+
+ public long getCurrentPosition() {
+ if (isInPlaybackState())
+ return mMediaPlayer.getCurrentPosition();
+ return 0;
+ }
+
+ public void seekTo(long msec) {
+ if (isInPlaybackState()) {
+ mMediaPlayer.seekTo(msec);
+ mSeekWhenPrepared = 0;
+ } else {
+ mSeekWhenPrepared = msec;
+ }
+ }
+
+ public boolean isPlaying() {
+ return isInPlaybackState() && mMediaPlayer.isPlaying();
+ }
+
+ public int getBufferPercentage() {
+ if (mMediaPlayer != null)
+ return mCurrentBufferPercentage;
+ return 0;
+ }
+
+ public void setVolume(float leftVolume, float rightVolume) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.setVolume(leftVolume, rightVolume);
+ }
+
+ public int getVideoWidth() {
+ return mVideoWidth;
+ }
+
+ public int getVideoHeight() {
+ return mVideoHeight;
+ }
+
+ public float getVideoAspectRatio() {
+ return mVideoAspectRatio;
+ }
+
+ /**
+ * Must set before {@link #setVideoURI}
+ * @param chroma
+ */
+ public void setVideoChroma(int chroma) {
+ getHolder().setFormat(chroma == MediaPlayer.VIDEOCHROMA_RGB565 ? PixelFormat.RGB_565 : PixelFormat.RGBA_8888); // PixelFormat.RGB_565
+ mVideoChroma = chroma;
+ }
+
+ public void setHardwareDecoder(boolean hardware) {
+ mHardwareDecoder= hardware;
+ }
+
+ public void setVideoQuality(int quality) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.setVideoQuality(quality);
+ }
+
+ public void setBufferSize(int bufSize) {
+ mBufSize = bufSize;
+ }
+
+ public boolean isBuffering() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.isBuffering();
+ return false;
+ }
+
+ public String getMetaEncoding() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.getMetaEncoding();
+ return null;
+ }
+
+ public void setMetaEncoding(String encoding) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.setMetaEncoding(encoding);
+ }
+
+ public SparseArray getAudioTrackMap(String encoding) {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.findTrackFromTrackInfo(TrackInfo.MEDIA_TRACK_TYPE_AUDIO, mMediaPlayer.getTrackInfo(encoding));
+ return null;
+ }
+
+ public int getAudioTrack() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.getAudioTrack();
+ return -1;
+ }
+
+ public void setAudioTrack(int audioIndex) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.selectTrack(audioIndex);
+ }
+
+ public void setTimedTextShown(boolean shown) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.setTimedTextShown(shown);
+ }
+
+ public void setTimedTextEncoding(String encoding) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.setTimedTextEncoding(encoding);
+ }
+
+ public int getTimedTextLocation() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.getTimedTextLocation();
+ return -1;
+ }
+
+ public void addTimedTextSource(String subPath) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.addTimedTextSource(subPath);
+ }
+
+ public String getTimedTextPath() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.getTimedTextPath();
+ return null;
+ }
+
+ public void setSubTrack(int trackId) {
+ if (mMediaPlayer != null)
+ mMediaPlayer.selectTrack(trackId);
+ }
+
+ public int getTimedTextTrack() {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.getTimedTextTrack();
+ return -1;
+ }
+
+ public SparseArray getSubTrackMap(String encoding) {
+ if (mMediaPlayer != null)
+ return mMediaPlayer.findTrackFromTrackInfo(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT, mMediaPlayer.getTrackInfo(encoding));
+ return null;
+ }
+
+ protected boolean isInPlaybackState() {
+ return (mMediaPlayer != null && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE && mCurrentState != STATE_PREPARING);
+ }
+}
\ No newline at end of file