/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.android; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; /** * An output stream that contains a thread to write data after it has been * written to an internal buffer, so that any write * will return immediately and will not block. * * Uses a circular buffer to process data. Data may overrun, and oh well, that's * the way it is */ public class ThreadedBufferedOutputStream extends OutputStream { private OutputStream os; private int dataEndIndex, wroteIndex; private byte [] data; protected IOException exception; private int minDataToWrite; private boolean flushing; /** * @param minDataToWrite will not schedule a write unless the amount of data to write is equal to or above * the minDataToWrite (or flushing) * @param os */ public ThreadedBufferedOutputStream(int size, int minDataToWrite, OutputStream os) { this.data = new byte[size]; this.os = os; this.intThread.setDaemon(true); this.minDataToWrite = minDataToWrite; intThread.start(); } @Override public void close() throws IOException { flush(); os.close(); } @Override public void flush() throws IOException { try { synchronized(intThread) { flushing = true; intThread.notify(); while(dataEndIndex != wroteIndex) { intThread.wait(); } os.flush(); flushing = false; } } catch(InterruptedException e) { throw new IllegalStateException(e); } } /** * Will never block. Always write to memory */ @Override public void write(byte[] buffer, int offset, int count) throws IOException { synchronized(intThread) { int avail = (wroteIndex - dataEndIndex + data.length) % data.length; if(avail == 0) avail = data.length; if(avail < count) { throw new IOException("Buffer overrun, have "+avail+" bytes available and want to write "+count); } //if we have to split into two copies if(count > data.length - dataEndIndex) { int firstPart = data.length - dataEndIndex; int secondPart = count - firstPart; System.arraycopy(buffer, offset, data, dataEndIndex, firstPart); System.arraycopy(buffer, offset + firstPart, data, 0, secondPart); dataEndIndex = secondPart; } else { System.arraycopy(buffer, offset, data, dataEndIndex, count); dataEndIndex += count; } //notify that we changed the position so the thread can start //writing it intThread.notify(); } } private static byte [] writeOne = new byte[1]; /** * Will never block. Always write to memory */ @Override public void write(int oneByte) throws IOException { writeOne[0] = (byte)oneByte; write(writeOne, 0, 1); } public Thread intThread = new Thread() { public void run() { try { while(true) { int writeEnd; synchronized(this) { //if we don't have enough data that we care to try to write it. If we are flushing, we will write //any amount of data, even one byte while((dataEndIndex - wroteIndex + data.length) % data.length < (flushing ? 1 : minDataToWrite) ) { wait(); } writeEnd = dataEndIndex; if(writeEnd < wroteIndex) writeEnd = data.length; } os.write(data, wroteIndex, writeEnd - wroteIndex); synchronized(this) { wroteIndex = writeEnd; if(wroteIndex == data.length) wroteIndex = 0; //notify that we updated the wrote index (incase we are flushing) notify(); } } } catch(InterruptedException e) { throw new IllegalStateException(e); } catch (IOException e) { exception = e; } } }; //TEST public static void main(String [] argv) throws IOException, InterruptedException { ThreadedBufferedOutputStream os = new ThreadedBufferedOutputStream(1000, 300, new FileOutputStream("/tmp/foo")); for(int i = 0; i < 4096; i++) { os.write(("i is "+i+"\n").getBytes()); System.out.println("i is "+i); // if(i % 100 == 0) // os.flush(); } os.close(); System.out.println("done with test 1"); os = new ThreadedBufferedOutputStream(1024, 512, new FileOutputStream("/tmp/foo2")); while(true) { os.write(("writing to overrun").getBytes()); } } }