To print logs, we first need to provide a way to print logs externally, mainly providing adapters for various levels of printing and formats of printing logs:
/** * A proxy interface to enable additional operations. * Contains all possible Log message usages. */ public interface Printer { void addAdapter(@NonNull LogAdapter adapter); Printer t(@Nullable String tag); void d(@NonNull String message, @Nullable Object... args); void d(@Nullable Object object); /** * Formats the given json content and print it */ void json(@Nullable String json); /** * Formats the given xml content and print it */ void xml(@Nullable String xml); void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable throwable); void clearLogAdapters(); }Copy the code
As a Logger format adapter, LogAdapter can customize the format of printed logs. Implementation method:
class LoggerPrinter implements Printer { /** * It is used for json pretty print */ private static final int JSON_INDENT = 2; /** * Provides one-time used tag for the log message */ private final ThreadLocal<String> localTag = new ThreadLocal<>(); private final List<LogAdapter> logAdapters = new ArrayList<>(); @Override public Printer t(String tag) { if (tag ! = null) { localTag.set(tag); } return this; } @Override public void d(@NonNull String message, @Nullable Object... args) { log(DEBUG, null, message, args); } @Override public void d(@Nullable Object object) { log(DEBUG, null, Utils.toString(object)); } @Override public void json(@Nullable String json) { if (Utils.isEmpty(json)) { d("Empty/Null json content"); return; } try { json = json.trim(); if (json.startsWith("{")) { JSONObject jsonObject = new JSONObject(json); String message = jsonObject.toString(JSON_INDENT); d(message); return; } if (json.startsWith("[")) { JSONArray jsonArray = new JSONArray(json); String message = jsonArray.toString(JSON_INDENT); d(message); return; } e("Invalid Json"); } catch (JSONException e) { e("Invalid Json"); } } @Override public void xml(@Nullable String xml) { if (Utils.isEmpty(xml)) { d("Empty/Null xml content"); return; } try { Source xmlInput = new StreamSource(new StringReader(xml)); StreamResult xmlOutput = new StreamResult(new StringWriter()); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); transformer.transform(xmlInput, xmlOutput); d(xmlOutput.getWriter().toString().replaceFirst(">", ">\n")); } catch (TransformerException e) { e("Invalid xml"); } } @Override public synchronized void log(int priority, @Nullable String tag, @Nullable String message, @Nullable Throwable throwable) { if (throwable != null && message != null) { message += " : " + Utils.getStackTraceString(throwable); } if (throwable ! = null && message == null) { message = Utils.getStackTraceString(throwable); } if (Utils.isEmpty(message)) { message = "Empty/NULL log message"; } for (LogAdapter adapter : logAdapters) { if (adapter.isLoggable(priority, tag)) { adapter.log(priority, tag, message); } } } @Override public void clearLogAdapters() { logAdapters.clear(); } @Override public void addAdapter(@NonNull LogAdapter adapter) { logAdapters.add(checkNotNull(adapter)); } /** * This method is synchronized in order to avoid messy of logs' order. */ private synchronized void log(int priority, @Nullable Throwable throwable, @NonNull String msg, @Nullable Object... args) { checkNotNull(msg); String tag = getTag(); String message = createMessage(msg, args); log(priority, tag, message, throwable); } /** * @return the appropriate tag based on local or global */ @Nullable private String getTag() { String tag = localTag.get(); if (tag ! = null) { localTag.remove(); return tag; } return null; } @NonNull private String createMessage(@NonNull String message, @Nullable Object... args) { return args == null || args.length == 0 ? message : String.format(message, args); } } final class Utils { private Utils() { // Hidden constructor. } /** * Copied from "android.util.Log.getStackTraceString()" in order to avoid usage of Android stack * in unit tests. * * @return Stack trace in form of String */ static String getStackTraceString(Throwable tr) { if (tr == null) { return ""; } // This is to reduce the amount of log spew that apps do in the non-error // condition of the network being unavailable. Throwable t = tr; while (t ! = null) { if (t instanceof UnknownHostException) { return ""; } t = t.getCause(); } StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); pw.flush(); return sw.toString(); } static String logLevel(int value) { switch (value) { case VERBOSE: return "VERBOSE"; case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARN: return "WARN"; case ERROR: return "ERROR"; case ASSERT: return "ASSERT"; default: return "UNKNOWN"; } } @NonNull static <T> T checkNotNull(@Nullable final T obj) { if (obj == null) { throw new NullPointerException(); } return obj; } }Copy the code
Public void d(@nonnull String message, @nullable Object) public void d(@nonnull String message, @nullable Object… Args) prints logs through the List logAdapters = New ArrayList<>() collection in the class. That is, the logs will be printed as many times as the number of logAdapters configured.
public interface LogAdapter {
boolean isLoggable(int priority, @Nullable String tag);
void log(int priority, @Nullable String tag, @NonNull String message);
}
Copy the code
Set the Adapter at initialization time.
/** * @param application * @param tag Default value :"PRETTY_LOGGER" * @param isDebug Whether DEBUG * @param sdcard Whether saving logs to the memory card is enabled * @param saveDays The maximum duration for saving logs is 1000 x 60 x 60 * 24 * 7 * @param logCapacitySize Default log capacity 10 * 1024 * 1024 * @param minSaveLevel Minimum storage level Default logger. INFO */ public static void init(Application application, @Nullable String tag, final boolean isDebug, final boolean sdcard, final long saveDays, final long logCapacitySize, final int minSaveLevel) { LogHelper.isDebug = isDebug; Logger.addLogAdapter(new DiskLogAdapter(LogFormatStrategy.newBuilder().setEncodeType(sEncodeType) .showMillis(false).tag(tag).build( application.getPackageName(), application.getPackageManager().getApplicationLabel(application.getApplicationInfo()).toString().trim(), application, new DiskLogConditions(saveDays, logCapacitySize))) { @Override public boolean isLoggable(int priority, @Nullable String tag) { if(priority >= minSaveLevel) return sdcard; else return false; }}); LogReport.getInstance().init(application); }Copy the code
Add a print policy to the Adapter.
/**
* Draws borders around the given log message along with additional information such as
* </pre>
*
* <h3>Customize</h3>
* <pre><code>
* FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
* .showThreadInfo(false) // (Optional) Whether to show thread info or not. Default true
* .methodCount(0) // (Optional) How many method line to show. Default 2
* .methodOffset(7) // (Optional) Hides internal method calls up to offset. Default 5
* .logStrategy(customLog) // (Optional) Changes the log strategy to print out. Default LogCat
* .tag("My custom tag") // (Optional) Global tag for every log. Default PRETTY_LOGGER
* .build();
* </code></pre>
*/
public class PrettyFormatStrategy implements FormatStrategy {
/**
* Android's max limit for a log entry is ~4076 bytes,
* so 4000 bytes is used as chunk size since default charset
* is UTF-8
*/
private static final int CHUNK_SIZE = 4000;
/**
* The minimum stack trace index, starts at this class after two native calls.
*/
private static final int MIN_STACK_OFFSET = 5;
/**
* Drawing toolbox
*/
private static final char TOP_LEFT_CORNER = '┌';
private static final char BOTTOM_LEFT_CORNER = '└';
private static final char MIDDLE_CORNER = '├';
private static final char HORIZONTAL_LINE = '│';
private static final String DOUBLE_DIVIDER = "────────────────────────────────────────────────────────";
private static final String SINGLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄";
private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
private static final String MIDDLE_BORDER = MIDDLE_CORNER + SINGLE_DIVIDER + SINGLE_DIVIDER;
private final int methodCount;
private final int methodOffset;
private final boolean showThreadInfo;
@NonNull private final LogStrategy logStrategy;
@Nullable private final String tag;
private PrettyFormatStrategy(@NonNull Builder builder) {
checkNotNull(builder);
methodCount = builder.methodCount;
methodOffset = builder.methodOffset;
showThreadInfo = builder.showThreadInfo;
logStrategy = builder.logStrategy;
tag = builder.tag;
}
@NonNull public static Builder newBuilder() {
return new Builder();
}
@Override public void log(int priority, @Nullable String onceOnlyTag, @NonNull String message) {
checkNotNull(message);
String tag = formatTag(onceOnlyTag);
logTopBorder(priority, tag);
logHeaderContent(priority, tag, methodCount);
//get bytes of message with system's default charset (which is UTF-8 for Android)
byte[] bytes = message.getBytes();
int length = bytes.length;
if (length <= CHUNK_SIZE) {
if (methodCount > 0) {
logDivider(priority, tag);
}
logContent(priority, tag, message);
logBottomBorder(priority, tag);
return;
}
if (methodCount > 0) {
logDivider(priority, tag);
}
for (int i = 0; i < length; i += CHUNK_SIZE) {
int count = Math.min(length - i, CHUNK_SIZE);
//create a new String with system's default charset (which is UTF-8 for Android)
logContent(priority, tag, new String(bytes, i, count));
}
logBottomBorder(priority, tag);
}
private void logTopBorder(int logType, @Nullable String tag) {
logChunk(logType, tag, TOP_BORDER);
}
@SuppressWarnings("StringBufferReplaceableByString")
private void logHeaderContent(int logType, @Nullable String tag, int methodCount) {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
if (showThreadInfo) {
logChunk(logType, tag, HORIZONTAL_LINE + " Thread: " + Thread.currentThread().getName());
logDivider(logType, tag);
}
String level = "";
int stackOffset = getStackOffset(trace) + methodOffset;
//corresponding method count with the current stack may exceeds the stack trace. Trims the count
if (methodCount + stackOffset > trace.length) {
methodCount = trace.length - stackOffset - 1;
}
for (int i = methodCount; i > 0; i--) {
int stackIndex = i + stackOffset;
if (stackIndex >= trace.length) {
continue;
}
StringBuilder builder = new StringBuilder();
builder.append(HORIZONTAL_LINE)
.append(' ')
.append(level)
.append(getSimpleClassName(trace[stackIndex].getClassName()))
.append(".")
.append(trace[stackIndex].getMethodName())
.append(" ")
.append(" (")
.append(trace[stackIndex].getFileName())
.append(":")
.append(trace[stackIndex].getLineNumber())
.append(")");
level += " ";
logChunk(logType, tag, builder.toString());
}
}
private void logBottomBorder(int logType, @Nullable String tag) {
logChunk(logType, tag, BOTTOM_BORDER);
}
private void logDivider(int logType, @Nullable String tag) {
logChunk(logType, tag, MIDDLE_BORDER);
}
private void logContent(int logType, @Nullable String tag, @NonNull String chunk) {
checkNotNull(chunk);
String[] lines = chunk.split(System.getProperty("line.separator"));
for (String line : lines) {
logChunk(logType, tag, HORIZONTAL_LINE + " " + line);
}
}
private void logChunk(int priority, @Nullable String tag, @NonNull String chunk) {
checkNotNull(chunk);
logStrategy.log(priority, tag, chunk);
}
private String getSimpleClassName(@NonNull String name) {
checkNotNull(name);
int lastIndex = name.lastIndexOf(".");
return name.substring(lastIndex + 1);
}
/**
* Determines the starting index of the stack trace, after method calls made by this class.
*
* @param trace the stack trace
* @return the stack offset
*/
private int getStackOffset(@NonNull StackTraceElement[] trace) {
checkNotNull(trace);
for (int i = MIN_STACK_OFFSET; i < trace.length; i++) {
StackTraceElement e = trace[i];
String name = e.getClassName();
if (!name.equals(LoggerPrinter.class.getName()) && !name.equals(Logger.class.getName())) {
return --i;
}
}
return -1;
}
@Nullable private String formatTag(@Nullable String tag) {
if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
return this.tag + "-" + tag;
}
return this.tag;
}
public static class Builder {
int methodCount = 2;
int methodOffset = 0;
boolean showThreadInfo = true;
@Nullable LogStrategy logStrategy;
@Nullable String tag = "PRETTY_LOGGER";
private Builder() {
}
@NonNull public Builder methodCount(int val) {
methodCount = val;
return this;
}
@NonNull public Builder methodOffset(int val) {
methodOffset = val;
return this;
}
@NonNull public Builder showThreadInfo(boolean val) {
showThreadInfo = val;
return this;
}
@NonNull public Builder logStrategy(@Nullable LogStrategy val) {
logStrategy = val;
return this;
}
@NonNull public Builder tag(@Nullable String tag) {
this.tag = tag;
return this;
}
@NonNull public PrettyFormatStrategy build() {
if (logStrategy == null) {
logStrategy = new LogcatLogStrategy();
}
return new PrettyFormatStrategy(this);
}
}
}
public interface FormatStrategy {
void log(int priority, @Nullable String tag, @NonNull String message);
}
public class LogcatLogStrategy implements LogStrategy {
static final String DEFAULT_TAG = "NO_TAG";
@Override public void log(int priority, @Nullable String tag, @NonNull String message) {
checkNotNull(message);
if (tag == null) {
tag = DEFAULT_TAG;
}
Log.println(priority, tag, message);
}
}
public class DiskLogAdapter implements LogAdapter {
@NonNull private final FormatStrategy formatStrategy;
public DiskLogAdapter() {
formatStrategy = CsvFormatStrategy.newBuilder().build();
}
public DiskLogAdapter(@NonNull FormatStrategy formatStrategy) {
this.formatStrategy = checkNotNull(formatStrategy);
}
@Override public boolean isLoggable(int priority, @Nullable String tag) {
return true;
}
@Override public void log(int priority, @Nullable String tag, @NonNull String message) {
formatStrategy.log(priority, tag, message);
}
}
public class LogFormatStrategy implements FormatStrategy {
private static final String NEW_LINE = System.getProperty("line.separator");
private static final String SEPARATOR = " ";
private final Date date;
private final SimpleDateFormat dateFormat;
private final LogStrategy logStrategy;
private final IEncryption encryption;
private final String tag;
private final boolean showMillis;//是否显示毫秒值
private LogFormatStrategy(Builder builder) {
date = builder.date;
dateFormat = builder.dateFormat;
logStrategy = builder.logStrategy;
showMillis = builder.showMillis;
encryption = builder.encryption;
tag = builder.tag;
}
public static Builder newBuilder() {
return new Builder();
}
@Override
public void log(int priority, String onceOnlyTag, String message) {
String tag = formatTag(onceOnlyTag);
date.setTime(System.currentTimeMillis());
StringBuilder header = new StringBuilder();
// machine-readable date/time
if (showMillis){
header.append(date.getTime());
// human-readable date/time
header.append(SEPARATOR);
}
header.append(dateFormat.format(date));
// level
header.append(SEPARATOR);
header.append(logLevel(priority));
// tag
header.append("/");
header.append(tag);
header.append(SEPARATOR);
StringBuilder builder = new StringBuilder();
builder.append(header);
// message
if (message.contains(NEW_LINE)) {
message = message.replaceAll(NEW_LINE, NEW_LINE + header.toString());
}
builder.append(message);
// new line
builder.append(NEW_LINE);
String encodeString = null;//加密日志内容
try {
encodeString = encodeString(new String((builder.toString().trim()+"\n").getBytes(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// Log.d(LogFormatStrategy.class.getSimpleName(),"加密内容:"+encodeString);
if (encodeString == null){
logStrategy.log(priority, tag, builder.toString());
}else {
logStrategy.log(priority, tag, encodeString);
}
}
public synchronized String encodeString(String content) {
if (encryption != null) {
try {
return encryption.encrypt(content);
} catch (Exception e) {
e.printStackTrace();
return content;
}
}
return content;
}
private String formatTag(String tag) {
if (!Utils.isEmpty(tag) && !Utils.equals(this.tag, tag)) {
return this.tag + "-" + tag;
}
return this.tag;
}
public static final class Builder {
Date date;
SimpleDateFormat dateFormat;
LogStrategy logStrategy;
boolean showMillis = true;
IEncryption encryption;
String tag = "PRETTY_LOGGER";
private Builder() {
}
public Builder date(Date val) {
date = val;
return this;
}
public Builder dateFormat(SimpleDateFormat val) {
dateFormat = val;
return this;
}
public Builder logStrategy(LogStrategy val) {
logStrategy = val;
return this;
}
public Builder showMillis(boolean showMs) {
showMillis = showMs;
return this;
}
public Builder setEncodeType(IEncryption encodeType) {
encryption = encodeType;
return this;
}
public Builder tag(String tag) {
this.tag = tag;
return this;
}
/**
* @param pkgName 包名
* @param appName 应用名
* @return
*/
public LogFormatStrategy build(String pkgName, String appName, Application application, DiskLogConditions conditions) {
if (date == null) {
date = new Date();
}
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
}
if (logStrategy == null) {
String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String folder = diskPath + File.separatorChar + "Android" + File.separatorChar + "data" + File.separatorChar + pkgName + File.separatorChar + "log";
HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
ht.start();
Handler handler = new NewestDiskLogStrategy.WriteHandler(ht.getLooper(), folder, appName, application, conditions);
logStrategy = new NewestDiskLogStrategy(handler, conditions);
}
return new LogFormatStrategy(this);
}
}
static String logLevel(int value) {
switch (value) {
case VERBOSE:
return "V";
case DEBUG:
return "D";
case INFO:
return "I";
case WARN:
return "W";
case ERROR:
return "E";
case ASSERT:
return "A";
default:
return "UNKNOWN";
}
}
}
/**
* 最新的Disk日志存储策略
* 目前是按天记录为一个日志文件,结合记录天数(默认 最多7天) + 总的日志文件大小(默认 10M), 存储条件超过其中一条则需要清除最早的一次文件,
* 然后再进行新日志信息的保存
*/
public class NewestDiskLogStrategy implements LogStrategy {
private static final String TAG = "NewestDiskLogStrategy";
private final Handler handler;
private final DiskLogConditions conditions;
public NewestDiskLogStrategy(Handler handler, DiskLogConditions conditions) {
this.handler = handler;
this.conditions = conditions;
}
@Override
public void log(int level, @Nullable String tag, @NonNull String message) {
handler.sendMessage(handler.obtainMessage(level, message));
}
static class WriteHandler extends Handler {
private static final long SAVE_DAYS = 1000 * 60 * 60 * 24 * 7; // 保留7天的日志
private static final long SAVE_SIZES = 10 * 1024 * 1024; //10M
private static SimpleDateFormat dirFormat = new SimpleDateFormat("yyyy-MM-dd");// 日志文件夹格式
private SimpleDateFormat fileFormat = new SimpleDateFormat("yyyy-MM-dd");// 日志文件名格式
private final String folder;
private String appName = "Logger";
private DiskLogConditions conditions;
WriteHandler(Looper looper, String folder, String appName, @NonNull Application application, @NonNull DiskLogConditions conditions) {
super(looper);
// this.folder = folder + File.separatorChar + dirFormat.format(new Date());
this.folder = folder;
this.appName = appName;
this.conditions = conditions;
if (conditions.getSaveDays() == 0) {
conditions.setSaveDays(SAVE_DAYS);
}
if (conditions.getLogCapacitySize() == 0) {
conditions.setLogCapacitySize(SAVE_SIZES);
}
orderByDate(this.folder);
deleteLoggerFile(this.folder);
addFileDeviceInfoHeader(application);
}
private void addFileDeviceInfoHeader(Application application) {
File logFile = getLogFile(folder, appName);
try {
if (!logFile.getParentFile().exists()) {
logFile.getParentFile().mkdir();
}
if (!logFile.exists()) {
logFile.createNewFile();
writeFile(logFile, SystemInfoUtil.getSystemInfoHeader(application));
}
} catch (SecurityException e1) {
Log.e(TAG, "addFileDeviceInfoHeader() "+e1.getMessage());
} catch (IOException e2) {
Log.e(TAG, "addFileDeviceInfoHeader() "+e2.getMessage());
}
}
@SuppressWarnings("checkstyle:emptyblock")
@Override
public void handleMessage(Message msg) {
String content = (String) msg.obj;
File logFile = getLogFile(folder, appName);
writeFile(logFile, content);
}
private void writeFile(File file, String content) {
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(file, true);
writeLog(fileWriter, content);
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
if (fileWriter != null) {
try {
fileWriter.flush();
fileWriter.close();
} catch (IOException e1) { /* fail silently */ }
}
}
}
public static void orderByDate(String filePath) {
File file = new File(filePath);
File[] files = file.listFiles();
if(files == null || files.length == 0)
return;
Arrays.sort(files, new Comparator<File>() {
public int compare(File f1, File f2) {
long diff = f1.lastModified() - f2.lastModified();
if (diff > 0)
return 1;
else if (diff == 0)
return 0;
else
return -1;//如果 if 中修改为 返回-1 同时此处修改为返回 1 排序就会是递减
}
public boolean equals(Object obj) {
return true;
}
});
for (int i = 0; i < files.length; i++) {
Log.i(TAG, files[i].getName());
Log.i(TAG, "lastModified = " + new Date(files[i].lastModified()));
}
}
/**
* This is always called on a single background thread.
* Implementing classes must ONLY write to the fileWriter and nothing more.
* The abstract class takes care of everything else including close the stream and catching IOException
*
* @param fileWriter an instance of FileWriter already initialised to the correct file
*/
private void writeLog(FileWriter fileWriter, String content) throws IOException {
fileWriter.append(content);
}
private File getLogFile(String folderName, String fileName) {
File folder = new File(folderName);
if (!folder.exists()) {
//TODO: What if folder is not created, what happens then?
folder.mkdirs();
}
return new File(folder, String.format("%s_%s.log", fileName, fileFormat.format(new Date())));
}
/**
* 删除SAVE_DAYS天前的日志
*/
private synchronized void deleteLoggerFile(String path) {
File file = new File(path);
if (!file.exists()) {
return;
}
File[] files = file.listFiles();
for (File fil : files) {
// 删除最后修改日期早于七天前的日志
if (System.currentTimeMillis() - fil.lastModified() > conditions.getSaveDays()) {
fil.delete();
}
}
files = file.listFiles();
if (!file.exists()) {
return;
}
long logFolderSize = FileUtil.folderSize(file);
Log.i(TAG, "for before LogFolderSize = "+logFolderSize);
for(File fil : files) {
// 删除最后修改日期早于七天前的日志
logFolderSize = FileUtil.folderSize(file);
if(logFolderSize > conditions.getLogCapacitySize()) {
fil.delete();
}
Log.i(TAG, "LogFolderSize = "+logFolderSize);
}
}
// 删除指定文件目录下的所有文件
private synchronized void deleteDirWithFile(File dir) {
if (dir == null || !dir.exists() || !dir.isDirectory()) {
return;
}
for (File file : dir.listFiles()) {
if (file.isFile())
file.delete(); // 删除所有文件
else if (file.isDirectory())
deleteDirWithFile(file); // 递归的方式删除文件夹
}
dir.delete();// 删除目录本身
}
}
}
public class FileUtil {
private static String TAG = "FileUtil";
/**
* 递归删除目录下的所有文件及子目录下所有文件
*
* @param dir 将要删除的文件目录
* @return 删除成功返回true,否则返回false
*/
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
// 递归删除目录中的子目录下
for (String aChildren : children) {
boolean success = deleteDir(new File(dir, aChildren));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}
/**
* 读取File中的内容
*
* @param file 请务必保证file文件已经存在
* @return file中的内容
*/
public static String getText(File file) {
if (!file.exists()) {
return null;
}
StringBuilder text = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\n');
}
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return text.toString();
}
/**
* 遍历获取Log文件夹下的所有crash文件
*
* @param logdir 从哪个文件夹下找起
* @return 返回crash文件列表
*/
public static ArrayList<File> getCrashList(File logdir) {
ArrayList<File> crashFileList = new ArrayList<>();
findFiles(logdir.getAbsolutePath(), crashFileList);
return crashFileList;
}
/**
* 将指定文件夹中满足要求的文件存储到list集合中
*
* @param f
* @param list
*/
/**
* 递归查找文件
*
* @param baseDirName 查找的文件夹路径
* @param fileList 查找到的文件集合
*/
public static void findFiles(String baseDirName, List<File> fileList) {
File baseDir = new File(baseDirName); // 创建一个File对象
if (!baseDir.exists() || !baseDir.isDirectory()) { // 判断目录是否存在
LogUtil.e(TAG, "文件查找失败:" + baseDirName + "不是一个目录!");
}
String tempName;
//判断目录是否存在
File tempFile;
File[] files = baseDir.listFiles();
for (File file : files) {
tempFile = file;
if (tempFile.isDirectory()) {
findFiles(tempFile.getAbsolutePath(), fileList);
} else if (tempFile.isFile()) {
tempName = tempFile.getName();
if (tempName.contains("Crash")) {
// 匹配成功,将文件名添加到结果集
fileList.add(tempFile.getAbsoluteFile());
}
}
}
}
/**
* 获取文件夹的大小
*
* @param directory 需要测量大小的文件夹
* @return 返回文件夹大小,单位byte
*/
public static long folderSize(File directory) {
long length = 0;
try {
for (File file : directory.listFiles()) {
if (file.isFile())
length += file.length();
else
length += folderSize(file);
}
}catch (Exception e){
e.printStackTrace();
}
return length;
}
public static File createFile(File zipdir, File zipfile) {
if (!zipdir.exists()) {
boolean result = zipdir.mkdirs();
LogUtil.d("TAG", "zipDir.mkdirs() = " + result);
}
if (!zipfile.exists()) {
try {
boolean result = zipfile.createNewFile();
LogUtil.d("TAG", "zipDir.createNewFile() = " + result);
} catch (IOException e) {
e.printStackTrace();
Log.e("TAG", e.getMessage());
}
}
return zipfile;
}
/**
* @des 保存内容到文本
* @param filePath
* @param content
*/
public static boolean saveAsFileWriter(String filePath, String content) {
FileWriter fwriter = null;
try {
//true表示不覆盖原来的内容,而是加到文件的后面。若要覆盖原来的内容,直接省略这个参数就好
fwriter = new FileWriter(filePath, true);
fwriter.write(content);
} catch (IOException ex) {
ex.printStackTrace();
} finally {
try {
fwriter.flush();
fwriter.close();
return true;
} catch (IOException ex) {
ex.printStackTrace();
return false;
}
}
}
/**
* 获取 指定路径 下的 所有 后缀 的文件
*
* @param fileAbsolutePath 搜索的路径
* @param suffix 文件后缀
* @return
*/
public static Vector<File> getAllTypeFile(String fileAbsolutePath, String suffix) {
Vector<File> vecFile = new Vector<>();
File file = new File(fileAbsolutePath);
File[] subFile = file.listFiles();
for (int iFileLength = 0; iFileLength < subFile.length; iFileLength++) {
// 判断是否为文件夹
if (!subFile[iFileLength].isDirectory()) {
String filename = subFile[iFileLength].getName();
// 判断是否为suffix结尾
if (filename.trim().toLowerCase().endsWith(suffix)) {
vecFile.add(subFile[iFileLength]);
}
}
}
return vecFile;
}
/**
* 分割文件
* @param file 文件路径
* @param unitSize 每个文件大小
* @return 子文件路径
*/
public static ArrayList<File> partiFile(File file , long unitSize) {
if(!file.exists() || (!file.isFile()))
return null;
String fileName = file.getName();
String path = file.getParent();
long fileSize = file.length();
Log.i(TAG, "zip fileSize = "+fileSize);
long count = fileSize / unitSize;
if(fileSize % unitSize != 0) {
count ++;
}
String fn[] = fileName.split("\\.");
if(fn.length == 2 && (!TextUtils.isEmpty(fn[0]))) {
fileName = fn[0];
}
ArrayList<File> result = new ArrayList<File> ();
FileInputStream in;
FileOutputStream out = null;
File fileTemp;
String fileNameTemp;
int tempbyte;
if(count == 1) {
result.add(file);
return result;
}
try {
in = new FileInputStream(file);
for (int i = 1; i <= count; i++) {
fileNameTemp = fileName + "_part"+i+".data";
fileTemp = new File(path + "/"+fileNameTemp);
result.add(fileTemp);
if(!fileTemp.exists()) {
fileTemp.createNewFile();
}
out = new FileOutputStream(fileTemp);
long total = 0;
long x = fileSize % unitSize;
while ((tempbyte = in.read()) != -1) {
out.write(tempbyte);
total ++;
if(i != count && total == unitSize || (i == count && total == fileSize % unitSize))
break;//子文件到达指定大小就创建下一个文件;
}
}
out.close();
in.close();
}
catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
Copy the code
Obtain compressed logs.
/**
* 获取日志文件夹压缩包
* @param application
* @return
*/
public static File getLogZipFile(Application application) {
return LogReport.getInstance().getLogZipFile(application);
}
/**
* 获取日志文件夹压缩包分片列表
* @param application
* @param unitSize 每片大小
* @return
*/
public static ArrayList<File> getPartiFiles(Application application, long unitSize) {
return FileUtil.partiFile(getLogZipFile(application), unitSize);
}
/**
* 测试调试阶段打印或存储
* 支持msg的日志,例如:
* LogHelper.t("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void t(String msg, Object... args) {
if(LogHelper.isDebug)
Logger.v(msg, args);
}
/**
* 测试调试阶段打印或存储
* 支持msg的日志,例如:
* LogHelper.t("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void t(String tag, String msg, Object... args) {
if(LogHelper.isDebug)
Logger.t(tag).v(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.v("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void v(String msg, Object... args) {
Logger.v(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.v("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void v(String tag, String msg, Object... args) {
Logger.t(tag).v(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.i("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void i(String msg, Object... args) {
Logger.i(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.i("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void i(String tag, String msg, Object... args) {
Logger.t(tag).i(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.d("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void d(String msg, Object... args) {
Logger.d(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.d("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void d(String tag, String msg, Object... args) {
Logger.t(tag).d(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.w("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void w(String msg, Object... args) {
Logger.w(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.w("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void w(String tag, String msg, Object... args) {
Logger.t(tag).w(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.e("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void e(String msg, Object... args) {
Logger.e(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.e("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void e(String tag, String msg, Object... args) {
Logger.t(tag).e(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.wtf("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void wtf(String msg, Object... args) {
Logger.wtf(msg, args);
}
/**
* 支持msg的日志,例如:
* LogHelper.wtf("name=%s,age=%d", "zhangsan", 18);
*
* @param msg
* @param args
*/
public static void wtf(String tag, String msg, Object... args) {
Logger.t(tag).wtf(msg, args);
}
/**
* @param throwable
* @param msg
* @param args
*/
public static void e(Throwable throwable, String msg, Object... args) {
Logger.e(throwable, msg, args);
}
/**
* @des 打印json
* @param json
*/
public static void json(String json) {
Logger.json(json);
}
/**
* @des 打印json
* @param json
*/
public static void json(String tag, String json) {
Logger.t(tag).json(json);
}
/**
* @des xml
* @param xml
*/
public static void xml(String xml) {
Logger.xml(xml);
}
/**
* @des xml
* @param xml
*/
public static void xml(String tag, String xml) {
Logger.t(tag).xml(xml);
}
public static IEncryption getEncodeType() {
return sEncodeType;
}
/**
* @des 设置加密方式
* @param encodeType
*/
public static void setEncodeType(IEncryption encodeType) {
sEncodeType = encodeType;
}
/**
* 日志崩溃管理框架
*/
public final class LogReport {
private static LogReport mLogReport;
/**
* 设置缓存文件夹的大小,默认是10MB
*/
private long mCacheSize = 10 * 1024 * 1024;
/**
* 设置日志保存的路径
*/
private String mROOT;
/**
* 设置加密方式
*/
private IEncryption mEncryption;
/**
* 设置日志的保存方式
*/
private ISave mLogSaver;
/**
* 设置在哪种网络状态下上传,true为只在wifi模式下上传,false是wifi和移动网络都上传
*/
private boolean mWifiOnly = true;
/**
* 压缩文件时间格式
*/
private static final SimpleDateFormat ZIP_FOLDER_TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS", Locale.getDefault());
private File mLogFolder;
private File mZipFolder;
private LogReport() {
}
public static LogReport getInstance() {
if (mLogReport == null) {
synchronized (LogReport.class) {
if (mLogReport == null) {
mLogReport = new LogReport();
}
}
}
return mLogReport;
}
public LogReport setEncryption(IEncryption encryption) {
this.mEncryption = encryption;
return this;
}
public LogReport setWifiOnly(boolean wifiOnly) {
mWifiOnly = wifiOnly;
return this;
}
public LogReport setLogDir(Context context, String logDir) {
if (TextUtils.isEmpty(logDir)) {
//如果SD不可用,则存储在沙盒中
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
mROOT = context.getExternalCacheDir().getAbsolutePath();
} else {
mROOT = context.getCacheDir().getAbsolutePath();
}
} else {
mROOT = logDir;
}
return this;
}
public LogReport setLogSaver(ISave logSaver) {
this.mLogSaver = logSaver;
return this;
}
public String getROOT() {
return mROOT;
}
public void init(Context context) {
if (TextUtils.isEmpty(mROOT)) {
//如果SD不可用,则存储在沙盒中
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
mROOT = context.getExternalCacheDir().getAbsolutePath();
} else {
mROOT = context.getCacheDir().getAbsolutePath();
}
}
if (mEncryption != null) {
mLogSaver.setEncodeType(mEncryption);
}
CrashHandler.getInstance().init(mLogSaver);
LogWriter.getInstance().init(mLogSaver);
}
public long getCacheSize() {
return mCacheSize;
}
public LogReport setCacheSize(long cacheSize) {
this.mCacheSize = cacheSize;
return this;
}
/**
* @param application
* @return
* @des 获取原始加密压缩文件, 取回日志之后再做解密处理
*/
public File getLogZipFile(Application application) {
String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String folder = diskPath
+ File.separatorChar
+ "Android"
+ File.separatorChar
+ "data"
+ File.separatorChar
+ application.getPackageName()
+ File.separatorChar
+ "log/";
mLogFolder = new File(folder);
// 如果Log文件夹都不存在,说明不存在崩溃日志,检查缓存是否超出大小后退出
if (!mLogFolder.exists() || mLogFolder.listFiles().length == 0) {
LogHelper.d("Log文件夹都不存在,无需上传");
return null;
}
mZipFolder = new File(mLogFolder + "alreadyUpload/");
File zipFile = new File(mZipFolder,
"UploadOn" + ZIP_FOLDER_TIME_FORMAT.format(System.currentTimeMillis()) + ".zip");
// File logFile = new File(mLogFolder, String.format("%s.log", appName));//原始加密文件
// File decodeFile = new File(mLogFolder, String.format("%s_%s.log", appName, "decode"));//解密后的文件
// FileUtil.saveAsFileWriter(decodeFile.getPath(), decodeString(FileUtil.getText(logFile)));
//创建文件,如果父路径缺少,创建父路径
zipFile = FileUtil.createFile(mZipFolder, zipFile);
if (CompressUtil.zipFileAtPath(mLogFolder.getAbsolutePath(),
zipFile.getAbsolutePath())) { //压缩加密文件
LogHelper.d("日志文件压缩==========>成功===>大小:", FileUtil.folderSize(mZipFolder));
return zipFile;
} else {
LogHelper.d("日志文件压缩==========>失败");
}
return null;
}
/**
* @des 删除原日志文件
*/
public boolean deleteLog() {
if (mLogFolder == null) {
return false;
}
return FileUtil.deleteDir(mLogFolder);
}
/**
* @des 删除压缩日志
*/
public boolean deleteZipLog() {
if (mZipFolder == null) {
return false;
}
return FileUtil.deleteDir(mZipFolder);
}
/**
* @des 解密日志
*/
private String decodeString(String content) {
if (LogHelper.getEncodeType() != null) {
try {
return LogHelper.getEncodeType().decrypt(content);
} catch (Exception e) {
LogHelper.e(e.toString());
e.printStackTrace();
return content;
}
}
return content;
}
}
Copy the code
Upload log information:
class RealWarnReport private constructor() {
private val TAG = "RealWarnReport"
private val URL_UPLOAD_APP_LOG = "/cloud/v1/log/uploadAppLog"
private var mJobjBaseParam: JSONObject? = null
private var mBaseUrl: String? = null
private var application: Application? = null
companion object {
val instance = RealWarnReportHolder.holder
val SP_FILE = "LogHelper"
val SP_CONTENT = "content"
}
private object RealWarnReportHolder {
val holder = RealWarnReport()
}
fun init(provider: NetBodyProvider, application: Application) {
this.application = application
mJobjBaseParam = provider.getNetBody()
mBaseUrl = provider.getServerUrl()
PreferencesUtils.init(application)
checkInitParam()
if(!TextUtils.isEmpty(mJobjBaseParam!!.optString("token")) || (!TextUtils.isEmpty(mJobjBaseParam!!.optString("accountID")))) {
checkUploadFailureAndUplpad()
}
}
/**
* 登录后把用户信息注入
*/
fun setAccountInfo(token: String?, accountID: String?) {
if (mJobjBaseParam != null) {
try {
mJobjBaseParam!!.put("token", token)
mJobjBaseParam!!.put("accountID", accountID)
checkUploadFailureAndUplpad()
} catch (e: JSONException) {
e.printStackTrace()
}
}
}
fun accountLogout() {
setAccountInfo("", "")
}
private fun checkUploadFailureAndUplpad() {
if(TextUtils.isEmpty(mJobjBaseParam!!.optString("token")) || TextUtils.isEmpty(mJobjBaseParam!!.optString("accountID"))) {
return
}
if (NetUtil.isConnected(this.application!!)) {
val content = PreferencesUtils.from(SP_FILE)
.getString(SP_CONTENT, null)
if (!TextUtils.isEmpty(content)) {
val jarr: JSONArray
val parm: JSONObject
jarr = try {
JSONArray(content)
} catch (e: JSONException) {
e.printStackTrace()
return
}
try {
parm = JSONObject(mJobjBaseParam.toString())
parm.put("appTimestamp", System.currentTimeMillis().toString() + "")
parm.put("traceId", System.currentTimeMillis().toString() + "")
parm.put("method", "uploadAppLog")
parm.put("type", "abnormal")
parm.put("logs", jarr)
} catch (e: JSONException) {
e.printStackTrace()
return
}
Log.i(
TAG,
"checkUploadFailureAndUplpad() content = $content"
)
//delete SP_CONTENT
PreferencesUtils.from(SP_FILE)
.remove(SP_CONTENT)
val intent = Intent(
application,
UploadRealWarnService::class.java
)
intent.putExtra(UploadRealWarnService.PARAM_BODY, parm.toString())
intent.putExtra(UploadRealWarnService.PARAM_DATA, content)
intent.putExtra(UploadRealWarnService.PARAM_RESEND, true)
intent.putExtra(
UploadRealWarnService.PARAM_URL,
mBaseUrl + URL_UPLOAD_APP_LOG
)
this.application!!.startService(intent)
} else {
Log.i(
TAG,
"checkUploadFailureAndUplpad() content null"
)
}
}
}
/**
* 检查参数
*/
private fun checkInitParam() {
if (mJobjBaseParam == null || mJobjBaseParam!!.length() == 0) {
throw RuntimeException("RealWarnReport mJobjBaseParam 参数错误")
}
if (TextUtils.isEmpty(mBaseUrl)) {
throw RuntimeException("RealWarnReport mBaseUrl参数错误")
}
}
/**
* 上传关键日志到服务器,同时记录到详细日志中
* @param applicationContext
* @param msg
*/
fun upload(applicationContext: Context, msg: String?) {
//上传的关键日志也要记录到详细日志中
LogHelper.e(TAG, msg)
if(TextUtils.isEmpty(mJobjBaseParam!!.optString("token")) || TextUtils.isEmpty(mJobjBaseParam!!.optString("accountID"))) {
return
}
checkInitParam()
//如果网络可用,而且是移动网络,但是用户设置了只在wifi下上传,返回
if (!NetUtil.isConnected(applicationContext)) {
writeLocalFile(msg)
return
}
val parm: JSONObject
val data: JSONObject
val jarr: JSONArray
try {
parm = JSONObject(mJobjBaseParam.toString())
data = JSONObject()
jarr = JSONArray()
data.put("content", msg)
data.put("time", System.currentTimeMillis())
jarr.put(data)
parm.put("appTimestamp", System.currentTimeMillis().toString() + "")
parm.put("traceId", System.currentTimeMillis().toString() + "")
parm.put("method", "uploadAppLog")
parm.put("type", "abnormal")
parm.put("logs", jarr)
} catch (e: JSONException) {
e.printStackTrace()
return
}
val intent = Intent(
applicationContext,
UploadRealWarnService::class.java
)
intent.putExtra(UploadRealWarnService.PARAM_BODY, parm.toString())
intent.putExtra(
UploadRealWarnService.PARAM_URL,
mBaseUrl + URL_UPLOAD_APP_LOG
)
applicationContext.startService(intent)
}
/**
* 记录需要上传的关键日志到本地文件
*/
fun writeLocalFile(msg: String?) {
if (TextUtils.isEmpty(msg)) {
return
}
val content = PreferencesUtils.from(SP_FILE)
.getString(SP_CONTENT, null)
Log.i(
TAG,
"writeUploadFailure() content = $content"
)
val jarr: JSONArray
val job: JSONObject
jarr = if (TextUtils.isEmpty(content)) {
JSONArray()
} else {
try {
JSONArray(content)
} catch (e: JSONException) {
e.printStackTrace()
JSONArray()
}
}
job = JSONObject()
try {
job.put("content", msg)
job.put("time", System.currentTimeMillis())
jarr.put(job)
} catch (e: JSONException) {
e.printStackTrace()
}
Log.d(
TAG,
"writeUploadFailure() jarr = $jarr"
)
PreferencesUtils.from(SP_FILE)
.putString(SP_CONTENT, jarr.toString())
.apply()
}
interface NetBodyProvider {
fun getNetBody(): JSONObject
fun getServerUrl(): String
}
}
class UploadRealWarnService :
IntentService(TAG) {
override fun onCreate() {
super.onCreate()
PreferencesUtils.init(this.applicationContext)
}
/**
* 同一时间只会有一个耗时任务被执行,其他的请求还要在后面排队,
* onHandleIntent()方法不会多线程并发执行,所有无需考虑同步问题
*
* @param intent
*/
override fun onHandleIntent(intent: Intent?) {
val bodyAndParam =
intent!!.getStringExtra(PARAM_BODY)
val url = intent.getStringExtra(PARAM_URL)
val isReSend =
intent.getBooleanExtra(PARAM_RESEND, false)
val data = intent.getStringExtra(PARAM_DATA)
val report =
HttpReporter(applicationContext)
report.setUrl(url)
report.sendReport(bodyAndParam, object : ILogUpload.OnUploadFinishedListener {
override fun onSuceess() {}
override fun onError(error: String?) {
if (isReSend && TextUtils.isEmpty(data)) {
PreferencesUtils.from(RealWarnReport.SP_FILE)
.putString(RealWarnReport.SP_CONTENT, data)
.apply()
}
}
})
}
companion object {
const val TAG = "UploadRealWarnService"
const val PARAM_BODY = "body_param"
const val PARAM_DATA = "body_data"
const val PARAM_RESEND = "body_resend"
const val PARAM_URL = "url_param"
}
}
Copy the code