mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Create a core-util module with some common utilities.
This commit is contained in:
6
core-util/src/main/AndroidManifest.xml
Normal file
6
core-util/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.signal.core.util">
|
||||
|
||||
</manifest>
|
||||
180
core-util/src/main/java/org/signal/core/util/Conversions.java
Normal file
180
core-util/src/main/java/org/signal/core/util/Conversions.java
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.signal.core.util;
|
||||
|
||||
public class Conversions {
|
||||
|
||||
public static byte intsToByteHighAndLow(int highValue, int lowValue) {
|
||||
return (byte)((highValue << 4 | lowValue) & 0xFF);
|
||||
}
|
||||
|
||||
public static int highBitsToInt(byte value) {
|
||||
return (value & 0xFF) >> 4;
|
||||
}
|
||||
|
||||
public static int lowBitsToInt(byte value) {
|
||||
return (value & 0xF);
|
||||
}
|
||||
|
||||
public static int highBitsToMedium(int value) {
|
||||
return (value >> 12);
|
||||
}
|
||||
|
||||
public static int lowBitsToMedium(int value) {
|
||||
return (value & 0xFFF);
|
||||
}
|
||||
|
||||
public static byte[] shortToByteArray(int value) {
|
||||
byte[] bytes = new byte[2];
|
||||
shortToByteArray(bytes, 0, value);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static int shortToByteArray(byte[] bytes, int offset, int value) {
|
||||
bytes[offset+1] = (byte)value;
|
||||
bytes[offset] = (byte)(value >> 8);
|
||||
return 2;
|
||||
}
|
||||
|
||||
public static int shortToLittleEndianByteArray(byte[] bytes, int offset, int value) {
|
||||
bytes[offset] = (byte)value;
|
||||
bytes[offset+1] = (byte)(value >> 8);
|
||||
return 2;
|
||||
}
|
||||
|
||||
public static byte[] mediumToByteArray(int value) {
|
||||
byte[] bytes = new byte[3];
|
||||
mediumToByteArray(bytes, 0, value);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static int mediumToByteArray(byte[] bytes, int offset, int value) {
|
||||
bytes[offset + 2] = (byte)value;
|
||||
bytes[offset + 1] = (byte)(value >> 8);
|
||||
bytes[offset] = (byte)(value >> 16);
|
||||
return 3;
|
||||
}
|
||||
|
||||
public static byte[] intToByteArray(int value) {
|
||||
byte[] bytes = new byte[4];
|
||||
intToByteArray(bytes, 0, value);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static int intToByteArray(byte[] bytes, int offset, int value) {
|
||||
bytes[offset + 3] = (byte)value;
|
||||
bytes[offset + 2] = (byte)(value >> 8);
|
||||
bytes[offset + 1] = (byte)(value >> 16);
|
||||
bytes[offset] = (byte)(value >> 24);
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static int intToLittleEndianByteArray(byte[] bytes, int offset, int value) {
|
||||
bytes[offset] = (byte)value;
|
||||
bytes[offset+1] = (byte)(value >> 8);
|
||||
bytes[offset+2] = (byte)(value >> 16);
|
||||
bytes[offset+3] = (byte)(value >> 24);
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static byte[] longToByteArray(long l) {
|
||||
byte[] bytes = new byte[8];
|
||||
longToByteArray(bytes, 0, l);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static int longToByteArray(byte[] bytes, int offset, long value) {
|
||||
bytes[offset + 7] = (byte)value;
|
||||
bytes[offset + 6] = (byte)(value >> 8);
|
||||
bytes[offset + 5] = (byte)(value >> 16);
|
||||
bytes[offset + 4] = (byte)(value >> 24);
|
||||
bytes[offset + 3] = (byte)(value >> 32);
|
||||
bytes[offset + 2] = (byte)(value >> 40);
|
||||
bytes[offset + 1] = (byte)(value >> 48);
|
||||
bytes[offset] = (byte)(value >> 56);
|
||||
return 8;
|
||||
}
|
||||
|
||||
public static int longTo4ByteArray(byte[] bytes, int offset, long value) {
|
||||
bytes[offset + 3] = (byte)value;
|
||||
bytes[offset + 2] = (byte)(value >> 8);
|
||||
bytes[offset + 1] = (byte)(value >> 16);
|
||||
bytes[offset + 0] = (byte)(value >> 24);
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static int byteArrayToShort(byte[] bytes) {
|
||||
return byteArrayToShort(bytes, 0);
|
||||
}
|
||||
|
||||
public static int byteArrayToShort(byte[] bytes, int offset) {
|
||||
return
|
||||
(bytes[offset] & 0xff) << 8 | (bytes[offset + 1] & 0xff);
|
||||
}
|
||||
|
||||
// The SSL patented 3-byte Value.
|
||||
public static int byteArrayToMedium(byte[] bytes, int offset) {
|
||||
return
|
||||
(bytes[offset] & 0xff) << 16 |
|
||||
(bytes[offset + 1] & 0xff) << 8 |
|
||||
(bytes[offset + 2] & 0xff);
|
||||
}
|
||||
|
||||
public static int byteArrayToInt(byte[] bytes) {
|
||||
return byteArrayToInt(bytes, 0);
|
||||
}
|
||||
|
||||
public static int byteArrayToInt(byte[] bytes, int offset) {
|
||||
return
|
||||
(bytes[offset] & 0xff) << 24 |
|
||||
(bytes[offset + 1] & 0xff) << 16 |
|
||||
(bytes[offset + 2] & 0xff) << 8 |
|
||||
(bytes[offset + 3] & 0xff);
|
||||
}
|
||||
|
||||
public static int byteArrayToIntLittleEndian(byte[] bytes, int offset) {
|
||||
return
|
||||
(bytes[offset + 3] & 0xff) << 24 |
|
||||
(bytes[offset + 2] & 0xff) << 16 |
|
||||
(bytes[offset + 1] & 0xff) << 8 |
|
||||
(bytes[offset] & 0xff);
|
||||
}
|
||||
|
||||
public static long byteArrayToLong(byte[] bytes) {
|
||||
return byteArrayToLong(bytes, 0);
|
||||
}
|
||||
|
||||
public static long byteArray4ToLong(byte[] bytes, int offset) {
|
||||
return
|
||||
((bytes[offset + 0] & 0xffL) << 24) |
|
||||
((bytes[offset + 1] & 0xffL) << 16) |
|
||||
((bytes[offset + 2] & 0xffL) << 8) |
|
||||
((bytes[offset + 3] & 0xffL));
|
||||
}
|
||||
|
||||
public static long byteArrayToLong(byte[] bytes, int offset) {
|
||||
return
|
||||
((bytes[offset] & 0xffL) << 56) |
|
||||
((bytes[offset + 1] & 0xffL) << 48) |
|
||||
((bytes[offset + 2] & 0xffL) << 40) |
|
||||
((bytes[offset + 3] & 0xffL) << 32) |
|
||||
((bytes[offset + 4] & 0xffL) << 24) |
|
||||
((bytes[offset + 5] & 0xffL) << 16) |
|
||||
((bytes[offset + 6] & 0xffL) << 8) |
|
||||
((bytes[offset + 7] & 0xffL));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
public class LinkedBlockingLifoQueue<E> extends LinkedBlockingDeque<E> {
|
||||
@Override
|
||||
public void put(E runnable) throws InterruptedException {
|
||||
super.putFirst(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(E runnable) {
|
||||
super.addFirst(runnable);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offer(E runnable) {
|
||||
super.addFirst(runnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
95
core-util/src/main/java/org/signal/core/util/StreamUtil.java
Normal file
95
core-util/src/main/java/org/signal/core/util/StreamUtil.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Utility methods for input and output streams.
|
||||
*/
|
||||
public final class StreamUtil {
|
||||
|
||||
private static final String TAG = Log.tag(StreamUtil.class);
|
||||
|
||||
private StreamUtil() {}
|
||||
|
||||
public static void close(@Nullable Closeable closeable) {
|
||||
if (closeable == null) return;
|
||||
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getStreamLength(InputStream in) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int totalSize = 0;
|
||||
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
totalSize += read;
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
readFully(in, buffer, buffer.length);
|
||||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer, int len) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = in.read(buffer, offset, len - offset);
|
||||
if (read == -1) throw new EOFException("Stream ended early");
|
||||
|
||||
if (read + offset < len) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readFully(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
bout.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
in.close();
|
||||
|
||||
return bout.toByteArray();
|
||||
}
|
||||
|
||||
public static String readFullyAsString(InputStream in) throws IOException {
|
||||
return new String(readFully(in));
|
||||
}
|
||||
|
||||
public static long copy(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
long total = 0;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
total += read;
|
||||
}
|
||||
|
||||
in.close();
|
||||
out.close();
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.signal.core.util.concurrent;
|
||||
|
||||
import android.os.HandlerThread;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.LinkedBlockingLifoQueue;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class SignalExecutors {
|
||||
|
||||
public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded"));
|
||||
public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(getIdealThreadCount(), new NumberedThreadFactory("signal-bounded"));
|
||||
public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial"));
|
||||
|
||||
private SignalExecutors() {}
|
||||
|
||||
public static ExecutorService newCachedSingleThreadExecutor(final String name) {
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 15, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), r -> new Thread(r, name));
|
||||
executor.allowCoreThreadTimeOut(true);
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* ThreadPoolExecutor will only create a new thread if the provided queue returns false from
|
||||
* offer(). That means if you give it an unbounded queue, it'll only ever create 1 thread, no
|
||||
* matter how long the queue gets.
|
||||
*
|
||||
* But if you bound the queue and submit more runnables than there are threads, your task is
|
||||
* rejected and throws an exception.
|
||||
*
|
||||
* So we make a queue that will always return false if it's non-empty to ensure new threads get
|
||||
* created. Then, if a task gets rejected, we simply add it to the queue.
|
||||
*/
|
||||
public static ExecutorService newCachedBoundedExecutor(final String name, int minThreads, int maxThreads) {
|
||||
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(minThreads,
|
||||
maxThreads,
|
||||
30,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<Runnable>() {
|
||||
@Override
|
||||
public boolean offer(Runnable runnable) {
|
||||
if (isEmpty()) {
|
||||
return super.offer(runnable);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}, new NumberedThreadFactory(name));
|
||||
|
||||
threadPool.setRejectedExecutionHandler((runnable, executor) -> {
|
||||
try {
|
||||
executor.getQueue().put(runnable);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
return threadPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an executor that prioritizes newer work. This is the opposite of a traditional executor,
|
||||
* which processor work in FIFO order.
|
||||
*/
|
||||
public static ExecutorService newFixedLifoThreadExecutor(String name, int minThreads, int maxThreads) {
|
||||
return new ThreadPoolExecutor(minThreads, maxThreads, 0, TimeUnit.MILLISECONDS, new LinkedBlockingLifoQueue<>(), new NumberedThreadFactory(name));
|
||||
}
|
||||
|
||||
public static HandlerThread getAndStartHandlerThread(@NonNull String name) {
|
||||
HandlerThread handlerThread = new HandlerThread(name);
|
||||
handlerThread.start();
|
||||
return handlerThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an 'ideal' thread count based on the number of available processors.
|
||||
*/
|
||||
public static int getIdealThreadCount() {
|
||||
return Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4));
|
||||
}
|
||||
|
||||
private static class NumberedThreadFactory implements ThreadFactory {
|
||||
|
||||
private final String baseName;
|
||||
private final AtomicInteger counter;
|
||||
|
||||
NumberedThreadFactory(@NonNull String baseName) {
|
||||
this.baseName = baseName;
|
||||
this.counter = new AtomicInteger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(@NonNull Runnable r) {
|
||||
return new Thread(r, baseName + "-" + counter.getAndIncrement());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@SuppressLint("LogNotSignal")
|
||||
public final class AndroidLogger extends Log.Logger {
|
||||
|
||||
@Override
|
||||
public void v(String tag, String message, Throwable t) {
|
||||
android.util.Log.v(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(String tag, String message, Throwable t) {
|
||||
android.util.Log.d(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(String tag, String message, Throwable t) {
|
||||
android.util.Log.i(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(String tag, String message, Throwable t) {
|
||||
android.util.Log.w(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(String tag, String message, Throwable t) {
|
||||
android.util.Log.e(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wtf(String tag, String message, Throwable t) {
|
||||
android.util.Log.wtf(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockUntilAllWritesFinished() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
public class GrowingBuffer {
|
||||
|
||||
private byte[] buffer;
|
||||
|
||||
public byte[] get(int minLength) {
|
||||
if (buffer == null || buffer.length < minLength) {
|
||||
buffer = new byte[minLength];
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
150
core-util/src/main/java/org/signal/core/util/logging/Log.java
Normal file
150
core-util/src/main/java/org/signal/core/util/logging/Log.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
|
||||
@SuppressLint("LogNotSignal")
|
||||
public final class Log {
|
||||
|
||||
private static Logger[] loggers;
|
||||
|
||||
@MainThread
|
||||
public static void initialize(Logger... loggers) {
|
||||
Log.loggers = loggers;
|
||||
}
|
||||
|
||||
public static void v(String tag, String message) {
|
||||
v(tag, message, null);
|
||||
}
|
||||
|
||||
public static void d(String tag, String message) {
|
||||
d(tag, message, null);
|
||||
}
|
||||
|
||||
public static void i(String tag, String message) {
|
||||
i(tag, message, null);
|
||||
}
|
||||
|
||||
public static void w(String tag, String message) {
|
||||
w(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(String tag, String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void wtf(String tag, String message) {
|
||||
wtf(tag, message, null);
|
||||
}
|
||||
|
||||
public static void v(String tag, Throwable t) {
|
||||
v(tag, null, t);
|
||||
}
|
||||
|
||||
public static void d(String tag, Throwable t) {
|
||||
d(tag, null, t);
|
||||
}
|
||||
|
||||
public static void i(String tag, Throwable t) {
|
||||
i(tag, null, t);
|
||||
}
|
||||
|
||||
public static void w(String tag, Throwable t) {
|
||||
w(tag, null, t);
|
||||
}
|
||||
|
||||
public static void e(String tag, Throwable t) {
|
||||
e(tag, null, t);
|
||||
}
|
||||
|
||||
public static void wtf(String tag, Throwable t) {
|
||||
wtf(tag, null, t);
|
||||
}
|
||||
|
||||
public static void v(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.v(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.v(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.d(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.i(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.i(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.w(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.e(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.e(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void wtf(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.wtf(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.wtf(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static String tag(Class<?> clazz) {
|
||||
String simpleName = clazz.getSimpleName();
|
||||
if (simpleName.length() > 23) {
|
||||
return simpleName.substring(0, 23);
|
||||
}
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
public static void blockUntilAllWritesFinished() {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.blockUntilAllWritesFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Logger {
|
||||
public abstract void v(String tag, String message, Throwable t);
|
||||
public abstract void d(String tag, String message, Throwable t);
|
||||
public abstract void i(String tag, String message, Throwable t);
|
||||
public abstract void w(String tag, String message, Throwable t);
|
||||
public abstract void e(String tag, String message, Throwable t);
|
||||
public abstract void wtf(String tag, String message, Throwable t);
|
||||
public abstract void blockUntilAllWritesFinished();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
class LogFile {
|
||||
|
||||
public static class Writer {
|
||||
|
||||
private final byte[] ivBuffer = new byte[16];
|
||||
private final GrowingBuffer ciphertextBuffer = new GrowingBuffer();
|
||||
|
||||
private final byte[] secret;
|
||||
private final File file;
|
||||
private final Cipher cipher;
|
||||
private final BufferedOutputStream outputStream;
|
||||
|
||||
Writer(@NonNull byte[] secret, @NonNull File file) throws IOException {
|
||||
this.secret = secret;
|
||||
this.file = file;
|
||||
this.outputStream = new BufferedOutputStream(new FileOutputStream(file, true));
|
||||
|
||||
try {
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void writeEntry(@NonNull String entry) throws IOException {
|
||||
new SecureRandom().nextBytes(ivBuffer);
|
||||
|
||||
byte[] plaintext = entry.getBytes();
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
|
||||
|
||||
int cipherLength = cipher.getOutputSize(plaintext.length);
|
||||
byte[] ciphertext = ciphertextBuffer.get(cipherLength);
|
||||
cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
|
||||
|
||||
outputStream.write(ivBuffer);
|
||||
outputStream.write(Conversions.intToByteArray(cipherLength));
|
||||
outputStream.write(ciphertext, 0, cipherLength);
|
||||
|
||||
outputStream.flush();
|
||||
} catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
long getLogSize() {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
void close() {
|
||||
StreamUtil.close(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
static class Reader {
|
||||
|
||||
private final byte[] ivBuffer = new byte[16];
|
||||
private final byte[] intBuffer = new byte[4];
|
||||
private final GrowingBuffer ciphertextBuffer = new GrowingBuffer();
|
||||
|
||||
private final byte[] secret;
|
||||
private final Cipher cipher;
|
||||
private final BufferedInputStream inputStream;
|
||||
|
||||
Reader(@NonNull byte[] secret, @NonNull File file) throws IOException {
|
||||
this.secret = secret;
|
||||
this.inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
|
||||
try {
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
String readAll() throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
String entry;
|
||||
while ((entry = readEntry()) != null) {
|
||||
builder.append(entry).append('\n');
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String readEntry() throws IOException {
|
||||
try {
|
||||
StreamUtil.readFully(inputStream, ivBuffer);
|
||||
StreamUtil.readFully(inputStream, intBuffer);
|
||||
|
||||
int length = Conversions.byteArrayToInt(intBuffer);
|
||||
byte[] ciphertext = ciphertextBuffer.get(length);
|
||||
|
||||
StreamUtil.readFully(inputStream, ciphertext, length);
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
|
||||
byte[] plaintext = cipher.doFinal(ciphertext, 0, length);
|
||||
|
||||
return new String(plaintext);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@SuppressLint("LogNotSignal")
|
||||
public final class PersistentLogger extends Log.Logger {
|
||||
|
||||
private static final String TAG = PersistentLogger.class.getSimpleName();
|
||||
|
||||
private static final String LOG_V = "V";
|
||||
private static final String LOG_D = "D";
|
||||
private static final String LOG_I = "I";
|
||||
private static final String LOG_W = "W";
|
||||
private static final String LOG_E = "E";
|
||||
private static final String LOG_WTF = "A";
|
||||
|
||||
private static final String LOG_DIRECTORY = "log";
|
||||
private static final String FILENAME_PREFIX = "log-";
|
||||
private static final int MAX_LOG_FILES = 7;
|
||||
private static final int MAX_LOG_SIZE = 300 * 1024;
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz");
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
private final byte[] secret;
|
||||
private final String logTag;
|
||||
|
||||
private LogFile.Writer writer;
|
||||
|
||||
public PersistentLogger(@NonNull Context context, @NonNull byte[] secret, @NonNull String logTag) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.secret = secret;
|
||||
this.logTag = logTag;
|
||||
this.executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread thread = new Thread(r, "signal-PersistentLogger");
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
|
||||
executor.execute(this::initializeWriter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void v(String tag, String message, Throwable t) {
|
||||
write(LOG_V, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(String tag, String message, Throwable t) {
|
||||
write(LOG_D, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(String tag, String message, Throwable t) {
|
||||
write(LOG_I, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(String tag, String message, Throwable t) {
|
||||
write(LOG_W, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(String tag, String message, Throwable t) {
|
||||
write(LOG_E, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wtf(String tag, String message, Throwable t) {
|
||||
write(LOG_WTF, tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockUntilAllWritesFinished() {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
executor.execute(latch::countDown);
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
android.util.Log.w(TAG, "Failed to wait for all writes.");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @Nullable CharSequence getLogs() {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<CharSequence> logs = new AtomicReference<>();
|
||||
|
||||
executor.execute(() -> {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
try {
|
||||
File[] logFiles = getSortedLogFiles();
|
||||
for (int i = logFiles.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
LogFile.Reader reader = new LogFile.Reader(secret, logFiles[i]);
|
||||
builder.append(reader.readAll());
|
||||
} catch (IOException e) {
|
||||
android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference.");
|
||||
logFiles[i].delete();
|
||||
}
|
||||
}
|
||||
|
||||
logs.set(builder);
|
||||
} catch (IOException e) {
|
||||
logs.set(null);
|
||||
}
|
||||
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
return logs.get();
|
||||
} catch (InterruptedException e) {
|
||||
android.util.Log.w(TAG, "Failed to wait for logs to be retrieved.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeWriter() {
|
||||
try {
|
||||
writer = new LogFile.Writer(secret, getOrCreateActiveLogFile());
|
||||
} catch (IOException e) {
|
||||
android.util.Log.e(TAG, "Failed to initialize writer.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private void write(String level, String tag, String message, Throwable t) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
if (writer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (writer.getLogSize() >= MAX_LOG_SIZE) {
|
||||
writer.close();
|
||||
writer = new LogFile.Writer(secret, createNewLogFile());
|
||||
trimLogFilesOverMax();
|
||||
}
|
||||
|
||||
for (String entry : buildLogEntries(level, tag, message, t)) {
|
||||
writer.writeEntry(entry);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
android.util.Log.w(TAG, "Failed to write line. Deleting all logs and starting over.");
|
||||
deleteAllLogs();
|
||||
initializeWriter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void trimLogFilesOverMax() throws IOException {
|
||||
File[] logs = getSortedLogFiles();
|
||||
if (logs.length > MAX_LOG_FILES) {
|
||||
for (int i = MAX_LOG_FILES; i < logs.length; i++) {
|
||||
logs[i].delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteAllLogs() {
|
||||
try {
|
||||
File[] logs = getSortedLogFiles();
|
||||
for (File log : logs) {
|
||||
log.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
android.util.Log.w(TAG, "Was unable to delete logs.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getOrCreateActiveLogFile() throws IOException {
|
||||
File[] logs = getSortedLogFiles();
|
||||
if (logs.length > 0) {
|
||||
return logs[0];
|
||||
}
|
||||
|
||||
return createNewLogFile();
|
||||
}
|
||||
|
||||
private File createNewLogFile() throws IOException {
|
||||
return new File(getOrCreateLogDirectory(), FILENAME_PREFIX + System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private File[] getSortedLogFiles() throws IOException {
|
||||
File[] logs = getOrCreateLogDirectory().listFiles();
|
||||
if (logs != null) {
|
||||
Arrays.sort(logs, (o1, o2) -> o2.getName().compareTo(o1.getName()));
|
||||
return logs;
|
||||
}
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
private File getOrCreateLogDirectory() throws IOException {
|
||||
File logDir = new File(context.getCacheDir(), LOG_DIRECTORY);
|
||||
if (!logDir.exists() && !logDir.mkdir()) {
|
||||
throw new IOException("Unable to create log directory.");
|
||||
}
|
||||
|
||||
return logDir;
|
||||
}
|
||||
|
||||
private List<String> buildLogEntries(String level, String tag, String message, Throwable t) {
|
||||
List<String> entries = new LinkedList<>();
|
||||
Date date = new Date();
|
||||
|
||||
entries.add(buildEntry(level, tag, message, date));
|
||||
|
||||
if (t != null) {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
t.printStackTrace(new PrintStream(outputStream));
|
||||
|
||||
String trace = new String(outputStream.toByteArray());
|
||||
String[] lines = trace.split("\\n");
|
||||
|
||||
for (String line : lines) {
|
||||
entries.add(buildEntry(level, tag, line, date));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private String buildEntry(String level, String tag, String message, Date date) {
|
||||
return logTag + ' ' + DATE_FORMAT.format(date) + ' ' + level + ' ' + tag + ": " + message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.signal.core.util.logging;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class LogTest {
|
||||
|
||||
@Test
|
||||
public void tag_short_class_name() {
|
||||
assertEquals("MyClass", Log.tag(MyClass.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tag_23_character_class_name() {
|
||||
String tag = Log.tag(TwentyThreeCharacters23.class);
|
||||
assertEquals("TwentyThreeCharacters23", tag);
|
||||
assertEquals(23, tag.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tag_24_character_class_name() {
|
||||
assertEquals(24, TwentyFour24Characters24.class.getSimpleName().length());
|
||||
String tag = Log.tag(TwentyFour24Characters24.class);
|
||||
assertEquals("TwentyFour24Characters2", tag);
|
||||
assertEquals(23, Log.tag(TwentyThreeCharacters23.class).length());
|
||||
}
|
||||
|
||||
private class MyClass {
|
||||
}
|
||||
|
||||
private class TwentyThreeCharacters23 {
|
||||
}
|
||||
|
||||
private class TwentyFour24Characters24 {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user