Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f41de2a4 authored by Jesse Wilson's avatar Jesse Wilson
Browse files

Adding support for LoggingPrintStream.write(byte[]) and friends.

By default, Android's System.out and System.err are implemented by
the AndroidPrintStream subclass of LoggingPrintStream. Until now,
that class has silently discarded the raw bytes it has received.
This causes two problems:

Applications may be accidentally wasting CPU+memory writing to
System.out. By making this output visible, the developers of such
applications can silence the problem at the source.

Application developers may be purposefully writing to these streams
and perplexed by the data's disappearance. For example, the core
library's own java.util.logging.ConsoleHandler sends its log data
into this black hole. By making the data visible, we save the data
and remove an unnecessary sharp edge from our API.
parent ff3e4c83
Loading
Loading
Loading
Loading
+68 −13
Original line number Diff line number Diff line
@@ -16,11 +16,17 @@

package com.android.internal.os;

import java.io.PrintStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.Locale;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.Formatter;
import java.util.Locale;

/**
 * A print stream which logs output line by line.
@@ -31,6 +37,27 @@ abstract class LoggingPrintStream extends PrintStream {

    private final StringBuilder builder = new StringBuilder();

    /**
     * A buffer that is initialized when raw bytes are first written to this
     * stream. It may contain the leading bytes of multi-byte characters.
     * Between writes this buffer is always ready to receive data; ie. the
     * position is at the first unassigned byte and the limit is the capacity.
     */
    private ByteBuffer encodedBytes;

    /**
     * A buffer that is initialized when raw bytes are first written to this
     * stream. Between writes this buffer is always clear; ie. the position is
     * zero and the limit is the capacity.
     */
    private CharBuffer decodedChars;

    /**
     * Decodes bytes to characters using the system default charset. Initialized
     * when raw bytes are first written to this stream.
     */
    private CharsetDecoder decoder;

    protected LoggingPrintStream() {
        super(new OutputStream() {
            public void write(int oneByte) throws IOException {
@@ -80,20 +107,48 @@ abstract class LoggingPrintStream extends PrintStream {
        }
    }

    /*
     * We have no idea of how these bytes are encoded, so just ignore them.
     */

    /** Ignored. */
    public void write(int oneByte) {}
    public void write(int oneByte) {
        write(new byte[] { (byte) oneByte }, 0, 1);
    }

    /** Ignored. */
    @Override
    public void write(byte buffer[]) {}
    public void write(byte[] buffer) {
        write(buffer, 0, buffer.length);
    }

    /** Ignored. */
    @Override
    public void write(byte bytes[], int start, int count) {}
    public synchronized void write(byte bytes[], int start, int count) {
        if (decoder == null) {
            encodedBytes = ByteBuffer.allocate(80);
            decodedChars = CharBuffer.allocate(80);
            decoder = Charset.defaultCharset().newDecoder()
                    .onMalformedInput(CodingErrorAction.REPLACE)
                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
        }

        int end = start + count;
        while (start < end) {
            // copy some bytes from the array to the long-lived buffer. This
            // way, if we end with a partial character we don't lose it.
            int numBytes = Math.min(encodedBytes.remaining(), end - start);
            encodedBytes.put(bytes, start, numBytes);
            start += numBytes;

            encodedBytes.flip();
            CoderResult coderResult;
            do {
                // decode bytes from the byte buffer into the char buffer
                coderResult = decoder.decode(encodedBytes, decodedChars, false);

                // copy chars from the char buffer into our string builder
                decodedChars.flip();
                builder.append(decodedChars);
                decodedChars.clear();
            } while (coderResult.isOverflow());
            encodedBytes.compact();
        }
        flush(false);
    }

    /** Always returns false. */
    @Override
+56 −5
Original line number Diff line number Diff line
@@ -18,12 +18,12 @@ package com.android.internal.os;

import junit.framework.TestCase;

import java.util.Arrays;
import java.util.List;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.List;

public class LoggingPrintStreamTest extends TestCase {

@@ -121,6 +121,58 @@ public class LoggingPrintStreamTest extends TestCase {
        assertEquals(Arrays.asList("Foo", "4", "a"), out.lines);
    }

    public void testMultiByteCharactersSpanningBuffers() throws Exception {
        // assume 3*1000 bytes won't fit in LoggingPrintStream's internal buffer
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            builder.append("\u20AC"); // a Euro character; 3 bytes in UTF-8
        }
        String expected = builder.toString();

        out.write(expected.getBytes("UTF-8"));
        out.flush();
        assertEquals(Arrays.asList(expected), out.lines);
    }

    public void testWriteOneByteAtATimeMultibyteCharacters() throws Exception {
        String expected = " \u20AC  \u20AC   \u20AC    \u20AC     ";
        for (byte b : expected.getBytes()) {
            out.write(b);
        }
        out.flush();
        assertEquals(Arrays.asList(expected), out.lines);
    }

    public void testWriteByteArrayAtATimeMultibyteCharacters() throws Exception {
        String expected = " \u20AC  \u20AC   \u20AC    \u20AC     ";
        out.write(expected.getBytes());
        out.flush();
        assertEquals(Arrays.asList(expected), out.lines);
    }

    public void testWriteWithOffsetsMultibyteCharacters() throws Exception {
        String expected = " \u20AC  \u20AC   \u20AC    \u20AC     ";
        byte[] bytes = expected.getBytes();
        int i = 0;
        while (i < bytes.length - 5) {
            out.write(bytes, i, 5);
            i += 5;
        }
        out.write(bytes, i, bytes.length - i);
        out.flush();
        assertEquals(Arrays.asList(expected), out.lines);
    }

    public void testWriteFlushesOnNewlines() throws Exception {
        String a = " \u20AC  \u20AC ";
        String b = "  \u20AC    \u20AC  ";
        String c = "   ";
        String toWrite = a + "\n" + b + "\n" + c;
        out.write(toWrite.getBytes());
        out.flush();
        assertEquals(Arrays.asList(a, b, c), out.lines);
    }

    static class TestPrintStream extends LoggingPrintStream {

        final List<String> lines = new ArrayList<String>();
@@ -129,5 +181,4 @@ public class LoggingPrintStreamTest extends TestCase {
            lines.add(line);
        }
    }

}