Procházet zdrojové kódy

1,增加日志封装库

yinxueqin před 3 roky
rodič
revize
61d84870b7
75 změnil soubory, kde provedl 4053 přidání a 0 odebrání
  1. 1 0
      .gitignore
  2. 1 0
      app/.gitignore
  3. 46 0
      app/build.gradle
  4. 21 0
      app/proguard-rules.pro
  5. 27 0
      app/src/androidTest/java/cn/hgits/loglibrary/ExampleInstrumentedTest.java
  6. 23 0
      app/src/main/AndroidManifest.xml
  7. 302 0
      app/src/main/java/cn/hgits/loglibrary/MainActivity.java
  8. 30 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  9. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  10. 82 0
      app/src/main/res/layout/activity_main.xml
  11. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  12. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  13. binární
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  14. binární
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  15. binární
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  16. binární
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  17. binární
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  18. binární
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  19. binární
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  20. binární
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  21. binární
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  22. binární
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  23. 6 0
      app/src/main/res/values/colors.xml
  24. 3 0
      app/src/main/res/values/strings.xml
  25. 11 0
      app/src/main/res/values/styles.xml
  26. 17 0
      app/src/test/java/cn/hgits/loglibrary/ExampleUnitTest.java
  27. 33 0
      build.gradle
  28. 1 0
      debuglogshow/.gitignore
  29. 62 0
      debuglogshow/build.gradle
  30. 32 0
      debuglogshow/consumer-rules.pro
  31. 53 0
      debuglogshow/proguard-rules.pro
  32. 27 0
      debuglogshow/src/androidTest/java/com/hgsoft/debuglogshow/ExampleInstrumentedTest.java
  33. 14 0
      debuglogshow/src/main/AndroidManifest.xml
  34. 15 0
      debuglogshow/src/main/aidl/com/hgsoft/debuglogshow/IDebugLogService.aidl
  35. 12 0
      debuglogshow/src/main/aidl/com/hgsoft/debuglogshow/IDebugLogServiceCallback.aidl
  36. 232 0
      debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugLogActivity.java
  37. 383 0
      debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugLogService.java
  38. 43 0
      debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugUtil.java
  39. 147 0
      debuglogshow/src/main/java/com/hgsoft/debuglogshow/Logcat.java
  40. binární
      debuglogshow/src/main/res/drawable/ic_stat_vlc.png
  41. 80 0
      debuglogshow/src/main/res/layout/debug_log.xml
  42. 5 0
      debuglogshow/src/main/res/layout/debug_log_item.xml
  43. 15 0
      debuglogshow/src/main/res/values/strings.xml
  44. 17 0
      debuglogshow/src/test/java/com/hgsoft/debuglogshow/ExampleUnitTest.java
  45. 208 0
      doc/decode_mars_crypt_log_file.py
  46. 169 0
      doc/decode_mars_nocrypt_log_file.py
  47. 19 0
      doc/gen_key.py
  48. binární
      doc/logOperator.zip
  49. binární
      doc/logOperator/AdbWinApi.dll
  50. binární
      doc/logOperator/AdbWinUsbApi.dll
  51. binární
      doc/logOperator/XlogDecode.exe
  52. binární
      doc/logOperator/adb.exe
  53. 4 0
      doc/logOperator/导出日志.bat
  54. binární
      doc/logOperator/日志操作教程/日志操作第1步.png
  55. binární
      doc/logOperator/日志操作教程/日志操作第2步.png
  56. binární
      doc/logOperator/日志操作教程/日志操作第3步.png
  57. 0 0
      doc/logOperator/此目录最好不要放入有中文的文件夹中.txt
  58. 5 0
      doc/日志解密密钥.txt
  59. 52 0
      doc/解密说明.txt
  60. 20 0
      gradle.properties
  61. 6 0
      gradle/wrapper/gradle-wrapper.properties
  62. 172 0
      gradlew
  63. 84 0
      gradlew.bat
  64. 1 0
      hgsoftloglibrary/.gitignore
  65. 149 0
      hgsoftloglibrary/build.gradle
  66. 34 0
      hgsoftloglibrary/consumer-rules.pro
  67. 55 0
      hgsoftloglibrary/proguard-rules.pro
  68. 27 0
      hgsoftloglibrary/src/androidTest/java/com/hgsoft/log/ExampleInstrumentedTest.java
  69. 2 0
      hgsoftloglibrary/src/main/AndroidManifest.xml
  70. 18 0
      hgsoftloglibrary/src/main/java/com/hgsoft/log/LogInfo.java
  71. 22 0
      hgsoftloglibrary/src/main/java/com/hgsoft/log/LogLevel.java
  72. 865 0
      hgsoftloglibrary/src/main/java/com/hgsoft/log/LogUtil.java
  73. 199 0
      hgsoftloglibrary/src/main/java/com/hgsoft/log/MarsXLogInit.java
  74. 17 0
      hgsoftloglibrary/src/test/java/com/hgsoft/log/ExampleUnitTest.java
  75. 4 0
      settings.gradle

+ 1 - 0
.gitignore

@@ -104,3 +104,4 @@ com_crashlytics_export_strings.xml
 crashlytics.properties
 crashlytics-build.properties
 
+/.svn

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 46 - 0
app/build.gradle

@@ -0,0 +1,46 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        applicationId "cn.hgits.loglibrary"
+        minSdkVersion 19
+        targetSdkVersion 30
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    implementation project(path: ':hgsoftloglibrary')
+    implementation project(path: ':debuglogshow')
+
+    //implementation 'com.hgsoft.app:loglibrary:1.2201.1'
+    //implementation 'com.hgsoft.app:debulogshow:1.2000.0'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'androidx.appcompat:appcompat:1.3.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+}

+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 27 - 0
app/src/androidTest/java/cn/hgits/loglibrary/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package cn.hgits.loglibrary;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("cn.hgits.loglibrary", appContext.getPackageName());
+    }
+}

+ 23 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="cn.hgits.loglibrary">
+
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- normal -->
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

+ 302 - 0
app/src/main/java/cn/hgits/loglibrary/MainActivity.java

@@ -0,0 +1,302 @@
+package cn.hgits.loglibrary;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.AppCompatButton;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import com.hgsoft.debuglogshow.DebugLogActivity;
+import com.hgsoft.log.LogUtil;
+import com.hgsoft.log.MarsXLogInit;
+import net.lingala.zip4j.exception.ZipException;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class MainActivity extends AppCompatActivity {
+
+    private static final String TAG = "MainActivity";
+
+    AppCompatButton btnInitLog;
+    AppCompatButton btnInitTwelveLog;
+    AppCompatButton btnCloseLog;
+    AppCompatButton btnPrint;
+    AppCompatButton btnCreateFourteenLog;
+    AppCompatButton btnSaveDiffLogFile;
+    AppCompatButton btnZipCurrentLog;
+    AppCompatButton btnZipAllLog;
+    AppCompatButton btnShowLog;
+
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        btnInitLog = findViewById(R.id.btn_init_log);
+        btnInitTwelveLog = findViewById(R.id.btn_init_twelve_log);
+        btnCloseLog = findViewById(R.id.btn_close_log);
+        btnPrint = findViewById(R.id.btn_print_log);
+        btnCreateFourteenLog = findViewById(R.id.btn_create_fourteen_log);
+        btnSaveDiffLogFile = findViewById(R.id.btn_save_diff_log_file);
+        btnZipCurrentLog = findViewById(R.id.btn_zip_current_log);
+        btnZipAllLog = findViewById(R.id.btn_zip_all_log);
+        btnShowLog = findViewById(R.id.btn_show_log);
+
+        btnInitLog.setOnClickListener(onClickListener);
+        btnInitTwelveLog.setOnClickListener(onClickListener);
+        btnCloseLog.setOnClickListener(onClickListener);
+        btnPrint.setOnClickListener(onClickListener);
+        btnCreateFourteenLog.setOnClickListener(onClickListener);
+        btnSaveDiffLogFile.setOnClickListener(onClickListener);
+        btnZipCurrentLog.setOnClickListener(onClickListener);
+        btnZipAllLog.setOnClickListener(onClickListener);
+        btnShowLog.setOnClickListener(onClickListener);
+    }
+
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+    }
+
+    private View.OnClickListener onClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int viewId = v.getId();
+            switch (viewId) {
+                case R.id.btn_init_log:
+                    initLog();
+                    LogUtil.i(TAG, systemInfo(MainActivity.this));
+                    LogUtil.appenderFlush(false);
+                    break;
+                case R.id.btn_init_twelve_log:
+                    initTwelveLog();
+                    LogUtil.i(TAG, systemInfo(MainActivity.this));
+                    LogUtil.appenderFlush(false);
+                    break;
+                case R.id.btn_close_log:
+                    LogUtil.appenderClose();
+                    break;
+                case R.id.btn_print_log:
+                    LogUtil.v(TAG, "测试日志-v");
+                    LogUtil.d(TAG, "测试日志-d");
+                    LogUtil.i(TAG, "测试日志-i");
+                    LogUtil.w(TAG, "测试日志-w");
+                    LogUtil.e(TAG, "测试日志-e");
+                    LogUtil.appenderFlush(true);
+                    break;
+                case R.id.btn_create_fourteen_log:
+                    createFourteenDayLogFile();
+                    break;
+                case R.id.btn_save_diff_log_file:
+                    LogUtil.appenderClose();
+                    initLog();
+                    break;
+                case R.id.btn_zip_current_log:
+                    File currentLogZipFile = null;
+                    try {
+                        currentLogZipFile = LogUtil.getCurrentUserLogFileZip(MainActivity.this);
+                    } catch (ZipException e) {
+                        e.printStackTrace();
+                    }
+                    if (currentLogZipFile != null) {
+                        LogUtil.i(TAG, "currentLogZipFile路径:" + currentLogZipFile.getAbsolutePath());
+                        ZipFile zipFile = null;
+                        try {
+                            zipFile = new ZipFile(currentLogZipFile.getAbsolutePath());
+                            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+                            LogUtil.i(TAG, "压缩包中包含的文件");
+                            while(entries.hasMoreElements()){
+                                ZipEntry entry = entries.nextElement();
+                                LogUtil.i(TAG, "名称:" + entry.getName());
+                            }
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    break;
+                case R.id.btn_zip_all_log:
+                    File allLogZipFile = null;
+                    try {
+                        allLogZipFile = LogUtil.getAppAllLogFileZip(MainActivity.this);
+                    } catch (ZipException e) {
+                        e.printStackTrace();
+                    }
+                    if (allLogZipFile != null) {
+                        LogUtil.i(TAG, "allLogZipFile路径:" + allLogZipFile.getAbsolutePath());
+                        ZipFile zipFile = null;
+                        try {
+                            zipFile = new ZipFile(allLogZipFile.getAbsolutePath());
+                            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+                            LogUtil.i(TAG, "压缩包中包含的文件");
+                            while(entries.hasMoreElements()){
+                                ZipEntry entry = entries.nextElement();
+                                LogUtil.i(TAG, "名称:" + entry.getName());
+                            }
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    break;
+                case R.id.btn_show_log:
+                    Intent intent = new Intent(MainActivity.this, DebugLogActivity.class);
+                    intent.putExtra("logEndWith", LogUtil.TAG);
+                    startActivity(intent);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+
+    private void initLog() {
+        MarsXLogInit.getInstance().setDebugStatus(BuildConfig.DEBUG);
+        if (BuildConfig.DEBUG) {
+            MarsXLogInit.getInstance().setPUBKEY("");
+            MarsXLogInit.getInstance().setConsoleLogOpen(true);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Debug_" + System.currentTimeMillis());
+        } else {
+            MarsXLogInit.getInstance().setPUBKEY("94e62d97637f357fbd20f0c1f667a67c2f675e158e46015dd0cc54cb3995d0a5d468f7e98b20aec266effb61ec0a2321fb1f8c61af72bf76567921a0d8305005");
+            MarsXLogInit.getInstance().setConsoleLogOpen(false);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Release_" + System.currentTimeMillis());
+        }
+        MarsXLogInit.getInstance().openXlog(this);
+        LogUtil.logPrintInfo((priority, tag, message) -> {
+            switch (priority) {
+                case LEVEL_DEBUG:
+                    Log.d(TAG, message);
+                    break;
+                case LEVEL_ERROR:
+                    Log.e(TAG, message);
+                    break;
+                case LEVEL_INFO:
+                    Log.i(TAG, message);
+                    break;
+                case LEVEL_WARNING:
+                    Log.w(TAG, message);
+                    break;
+                case LEVEL_VERBOSE:
+                default:
+                    Log.v(TAG, message);
+                    break;
+            }
+        });
+    }
+
+
+    private void initTwelveLog() {
+        MarsXLogInit.getInstance().setDebugStatus(BuildConfig.DEBUG);
+        if (BuildConfig.DEBUG) {
+            MarsXLogInit.getInstance().setPUBKEY("");
+            MarsXLogInit.getInstance().setConsoleLogOpen(true);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Debug_" + System.currentTimeMillis());
+            MarsXLogInit.getInstance().setLogFileSaveDays(12);
+        } else {
+            MarsXLogInit.getInstance().setPUBKEY("94e62d97637f357fbd20f0c1f667a67c2f675e158e46015dd0cc54cb3995d0a5d468f7e98b20aec266effb61ec0a2321fb1f8c61af72bf76567921a0d8305005");
+            MarsXLogInit.getInstance().setConsoleLogOpen(false);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Release_" + System.currentTimeMillis());
+            MarsXLogInit.getInstance().setLogFileSaveDays(12);
+        }
+        MarsXLogInit.getInstance().openXlog(this);
+        LogUtil.logPrintInfo((priority, tag, message) -> {
+            switch (priority) {
+                case LEVEL_DEBUG:
+                    Log.d(TAG, message);
+                    break;
+                case LEVEL_ERROR:
+                    Log.e(TAG, message);
+                    break;
+                case LEVEL_INFO:
+                    Log.i(TAG, message);
+                    break;
+                case LEVEL_WARNING:
+                    Log.w(TAG, message);
+                    break;
+                case LEVEL_VERBOSE:
+                default:
+                    Log.v(TAG, message);
+                    break;
+            }
+        });
+    }
+
+    private void createFourteenDayLogFile() {
+        File externalFileDir = getExternalFilesDir("XLog");
+        String logPath = null;
+        if (externalFileDir != null) {
+            logPath = externalFileDir.getAbsolutePath();
+        }
+        String fileName = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + getPastDate(14, "yyyyMMdd") + ".xlog";
+        File fourteenLogFile = new File(logPath + "/" + fileName);
+        if (!fourteenLogFile.exists()) {
+            try {
+                boolean result = fourteenLogFile.createNewFile();
+                LogUtil.i(TAG, "创建文件:" + fileName + ",结果:" + result);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 获取过去第几天的日期
+     *
+     * @param past 天数
+     * @param formatString 格式化
+     * @return 日期
+     */
+    private String getPastDate(final int past, final String formatString) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) - past);
+        Date today = calendar.getTime();
+        SimpleDateFormat format = new SimpleDateFormat(formatString, Locale.SIMPLIFIED_CHINESE);
+        String result = format.format(today);
+        return result;
+    }
+
+
+    private String systemInfo(Context context) {
+        final StringBuilder sb = new StringBuilder();
+        try {
+            String appVersionName = context.getPackageManager().getPackageInfo(context.getApplicationInfo().packageName, 0).versionName;
+            long appVersionCode = context.getPackageManager().getPackageInfo(context.getApplicationInfo().packageName, 0).versionCode;
+            sb.append("APP.VESIONNAME:[").append(appVersionName);
+            sb.append("] APP.VERSIONCODE:[").append(appVersionCode);
+            sb.append("] VERSION.RELEASE:[").append(Build.VERSION.RELEASE);
+            sb.append("] VERSION.CODENAME:[").append(Build.VERSION.CODENAME);
+            sb.append("] VERSION.INCREMENTAL:[").append(Build.VERSION.INCREMENTAL);
+            sb.append("] BOARD:[").append(Build.BOARD);
+            sb.append("] DEVICE:[").append(Build.DEVICE);
+            sb.append("] DISPLAY:[").append(Build.DISPLAY);
+            sb.append("] FINGERPRINT:[").append(Build.FINGERPRINT);
+            sb.append("] HOST:[").append(Build.HOST);
+            sb.append("] MANUFACTURER:[").append(Build.MANUFACTURER);
+            sb.append("] MODEL:[").append(Build.MODEL);
+            sb.append("] PRODUCT:[").append(Build.PRODUCT);
+            sb.append("] TAGS:[").append(Build.TAGS);
+            sb.append("] TYPE:[").append(Build.TYPE);
+            sb.append("] USER:[").append(Build.USER).append("]");
+        } catch (Throwable e) {
+            LogUtil.e(TAG, "异常信息:" + e.getMessage());
+        }
+        return sb.toString();
+    }
+
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 30 - 0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml


+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 82 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_init_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="初始化日志"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_init_twelve_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="初始化日志保存日志文件12天"
+        app:layout_constraintStart_toEndOf="@id/btn_init_log"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_close_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="关闭日志"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_init_log" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_print_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="打印日志"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_close_log" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_create_fourteen_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="创建14天前日志文件"
+        app:layout_constraintStart_toEndOf="@id/btn_print_log"
+        app:layout_constraintTop_toBottomOf="@id/btn_close_log" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_save_diff_log_file"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="保存到新的日志文件中"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_print_log"
+        />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_zip_current_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="压缩当前日志"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_save_diff_log_file" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_zip_all_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="压缩全部日志"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_zip_current_log" />
+
+    <androidx.appcompat.widget.AppCompatButton
+        android:id="@+id/btn_show_log"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="显示日志"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_zip_all_log" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 5 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

binární
app/src/main/res/mipmap-hdpi/ic_launcher.png


binární
app/src/main/res/mipmap-hdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-mdpi/ic_launcher.png


binární
app/src/main/res/mipmap-mdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


binární
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


binární
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 6 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#6200EE</color>
+    <color name="colorPrimaryDark">#3700B3</color>
+    <color name="colorAccent">#03DAC5</color>
+</resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">loglibrary</string>
+</resources>

+ 11 - 0
app/src/main/res/values/styles.xml

@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 17 - 0
app/src/test/java/cn/hgits/loglibrary/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package cn.hgits.loglibrary;
+
+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);
+    }
+}

+ 33 - 0
build.gradle

@@ -0,0 +1,33 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    
+    repositories {
+        google()
+        jcenter()
+        
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.2'
+        
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        mavenLocal()
+        maven { url "https://nexus.hgits.cn/nexus/repository/hgsoft"}
+        maven { url "https://maven.aliyun.com/repository/google" }
+        maven { url "https://maven.aliyun.com/repository/jcenter" }
+        google()
+        jcenter()
+        mavenCentral()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 1 - 0
debuglogshow/.gitignore

@@ -0,0 +1 @@
+/build

+ 62 - 0
debuglogshow/build.gradle

@@ -0,0 +1,62 @@
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 19
+        targetSdkVersion 30
+        versionCode 121000
+        versionName "1.2100.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            mavenPublish(MavenPublication) {
+                // Applies the component for the release build variant.
+                from components.release
+
+                // You can then customize attributes of the publication as shown below.
+                groupId = 'com.hgsoft.app'
+                artifactId = 'debuglogShow'
+                version = '1.2100.1'
+            }
+        }
+
+        repositories {
+            maven {
+                credentials {
+                    username 'yinxueqin'
+                    password 'hgits1024'
+                }
+
+                url = 'https://nexus.hgits.cn/nexus/repository/hgsoft/'
+            }
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    compileOnly 'com.google.android.material:material:1.4.0'
+    compileOnly 'androidx.appcompat:appcompat:1.3.1'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}

+ 32 - 0
debuglogshow/consumer-rules.pro

@@ -0,0 +1,32 @@
+-ignorewarnings
+-optimizationpasses 5          # 指定代码的压缩级别
+-dontusemixedcaseclassnames   # 是否使用大小写混合
+-dontpreverify           # 混淆时是否做预校验
+-verbose                # 混淆时是否记录日志
+# 保持行号
+-keepattributes SourceFile,LineNumberTable
+# androidX
+-dontwarn org.xmlpull.v1.XmlPullParser
+-dontwarn org.xmlpull.v1.XmlSerializer
+-keep class org.xmlpull.v1.* {*;}
+-keep class com.google.android.material.** { *; }
+-dontwarn com.google.android.material.**
+-dontnote com.google.android.material.**
+-dontwarn androidx.**
+-keep class androidx.** { *; }
+-keep interface androidx.** { *; }
+
+# kotlin
+-keep class kotlin.** { *; }
+-keep class kotlin.Metadata { *; }
+-dontwarn kotlin.**
+-keepclassmembers class **$WhenMappings {
+    <fields>;
+}
+-keepclassmembers class kotlin.Metadata {
+    public <methods>;
+}
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
+}
+-keep class com.hgsoft.debuglogshow.** { *; }

+ 53 - 0
debuglogshow/proguard-rules.pro

@@ -0,0 +1,53 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+-ignorewarnings
+-optimizationpasses 5          # 指定代码的压缩级别
+-dontusemixedcaseclassnames   # 是否使用大小写混合
+-dontpreverify           # 混淆时是否做预校验
+-verbose                # 混淆时是否记录日志
+# 保持行号
+-keepattributes SourceFile,LineNumberTable
+# androidX
+-dontwarn org.xmlpull.v1.XmlPullParser
+-dontwarn org.xmlpull.v1.XmlSerializer
+-keep class org.xmlpull.v1.* {*;}
+-keep class com.google.android.material.** { *; }
+-dontwarn com.google.android.material.**
+-dontnote com.google.android.material.**
+-dontwarn androidx.**
+-keep class androidx.** { *; }
+-keep interface androidx.** { *; }
+
+# kotlin
+-keep class kotlin.** { *; }
+-keep class kotlin.Metadata { *; }
+-dontwarn kotlin.**
+-keepclassmembers class **$WhenMappings {
+    <fields>;
+}
+-keepclassmembers class kotlin.Metadata {
+    public <methods>;
+}
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
+}
+-keep class com.hgsoft.debuglogshow.** { *; }

+ 27 - 0
debuglogshow/src/androidTest/java/com/hgsoft/debuglogshow/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.hgsoft.debuglogshow;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.hgsoft.debuglogshow.test", appContext.getPackageName());
+    }
+}

+ 14 - 0
debuglogshow/src/main/AndroidManifest.xml

@@ -0,0 +1,14 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.hgsoft.debuglogshow" >
+
+    <!-- 后台服务权限 -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+
+    <application>
+        <activity android:name=".DebugLogActivity"
+            android:launchMode="singleTask" />
+        <service android:name=".DebugLogService"
+            android:process=":logger" />
+    </application>
+
+</manifest>

+ 15 - 0
debuglogshow/src/main/aidl/com/hgsoft/debuglogshow/IDebugLogService.aidl

@@ -0,0 +1,15 @@
+// IDebugLogService.aidl
+package com.hgsoft.debuglogshow;
+
+import com.hgsoft.debuglogshow.IDebugLogServiceCallback;
+// Declare any non-default types here with import statements
+
+interface IDebugLogService
+{
+    void start();
+    void stop();
+    void clear();
+    void save();
+    void registerCallback(IDebugLogServiceCallback cb);
+    void unregisterCallback(IDebugLogServiceCallback cb);
+}

+ 12 - 0
debuglogshow/src/main/aidl/com/hgsoft/debuglogshow/IDebugLogServiceCallback.aidl

@@ -0,0 +1,12 @@
+// IDebugLogServiceCallback.aidl
+package com.hgsoft.debuglogshow;
+
+// Declare any non-default types here with import statements
+
+interface IDebugLogServiceCallback
+{
+    void onStarted(in List<String> logList);
+    void onStopped();
+    void onLog(String msg);
+    void onSaved(boolean success, String path);
+}

+ 232 - 0
debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugLogActivity.java

@@ -0,0 +1,232 @@
+package com.hgsoft.debuglogshow;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import com.google.android.material.snackbar.Snackbar;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DebugLogActivity extends Activity implements DebugLogService.Client.Callback {
+
+    public final static String TAG = "DebugLogActivity";
+    private DebugLogService.Client mClient = null;
+    private Button mStartButton = null;
+    private Button mStopButton = null;
+    private Button mCopyButton = null;
+    private Button mClearButton = null;
+    private Button mSaveButton = null;
+    private Button mTestButton = null;
+    private Button mBackLastView = null;
+    private ListView mLogView;
+    private ArrayList<Spanned> mLogList = null;
+    private ArrayAdapter<Spanned> mLogAdapter;
+    private String logEndsWith = "";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.debug_log);
+
+        mStartButton = (Button)findViewById(R.id.start_log);
+        mStopButton = (Button)findViewById(R.id.stop_log);
+        mLogView = (ListView) findViewById(R.id.log_list);
+        mCopyButton = (Button)findViewById(R.id.copy_to_clipboard);
+        mClearButton = (Button)findViewById(R.id.clear_log);
+        mSaveButton = (Button)findViewById(R.id.save_to_file);
+        mTestButton = (Button)findViewById(R.id.test_log);
+        mBackLastView = (Button)findViewById(R.id.back_last_view);
+
+        if (!TextUtils.isEmpty(getIntent().getStringExtra("logEndWith"))) {
+            DebugUtil.saveSetting(DebugLogActivity.this, "logEndWith", getIntent().getStringExtra("logEndWith"));
+        }
+
+        mClient = new DebugLogService.Client(this, this);
+
+        mStartButton.setEnabled(false);
+        mStopButton.setEnabled(false);
+        setOptionsButtonsEnabled(false);
+
+        mStartButton.setOnClickListener(mStartClickListener);
+        mStopButton.setOnClickListener(mStopClickListener);
+        mClearButton.setOnClickListener(mClearClickListener);
+        mSaveButton.setOnClickListener(mSaveClickListener);
+
+        mCopyButton.setOnClickListener(mCopyClickListener);
+
+        mTestButton.setOnClickListener(mTestClickListener);
+
+        mBackLastView.setOnClickListener(mBackClickListener);
+    }
+
+    @Override
+    protected void onDestroy() {
+        mClient.release();
+        super.onDestroy();
+    }
+
+    private View.OnClickListener mStartClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mStartButton.setEnabled(false);
+            mStopButton.setEnabled(false);
+            mClient.start();
+        }
+    };
+
+    private View.OnClickListener mStopClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mStartButton.setEnabled(false);
+            mStopButton.setEnabled(false);
+            mClient.stop();
+        }
+    };
+
+    private void setOptionsButtonsEnabled(boolean enabled) {
+        mClearButton.setEnabled(enabled);
+        mCopyButton.setEnabled(enabled);
+        mSaveButton.setEnabled(enabled);
+    }
+
+    private View.OnClickListener mClearClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mClient.clear();
+            if (mLogList != null) {
+                mLogList.clear();
+                mLogAdapter.notifyDataSetChanged();
+            }
+            setOptionsButtonsEnabled(false);
+        }
+    };
+
+    private View.OnClickListener mSaveClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            mClient.save();
+        }
+    };
+
+    @SuppressWarnings("deprecation")
+    private View.OnClickListener mCopyClickListener = new View.OnClickListener() {
+        @SuppressLint("WrongConstant")
+        @Override
+        public void onClick(View v) {
+            final StringBuffer buffer = new StringBuffer();
+            for (Spanned line : mLogList) {
+                buffer.append(line.toString()).append("\n");
+            }
+
+            android.text.ClipboardManager clipboard = (android.text.ClipboardManager)getApplicationContext().getSystemService(CLIPBOARD_SERVICE);
+            clipboard.setText(buffer);
+
+            Snackbar.make(v.getRootView(), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show();
+        }
+    };
+
+
+    private View.OnClickListener mTestClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            Log.v(TAG + logEndsWith, "测试日志-v");
+            Log.d(TAG + logEndsWith, "测试日志-d");
+            Log.i(TAG + logEndsWith, "测试日志-i");
+            Log.w(TAG + logEndsWith, "测试日志-w");
+            Log.e(TAG + logEndsWith, "测试日志-e");
+        }
+    };
+
+    private View.OnClickListener mBackClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            onBackPressed();
+        }
+    };
+
+    @Override
+    public void onStarted(List<String> logList) {
+        logEndsWith = DebugUtil.getSetting(DebugLogActivity.this, "logEndWith", "");
+        mStartButton.setEnabled(false);
+        mStopButton.setEnabled(true);
+        if (logList.size() > 0) {
+            setOptionsButtonsEnabled(true);
+        }
+        mLogList = new ArrayList<Spanned>(getSpannedList(logList));
+        mLogAdapter = new ArrayAdapter<Spanned>(this, R.layout.debug_log_item, mLogList);
+        mLogView.setAdapter(mLogAdapter);
+        mLogView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
+        if (mLogList.size() > 0) {
+            mLogView.setSelection(mLogList.size() - 1);
+        }
+    }
+
+    @Override
+    public void onStopped() {
+        mStartButton.setEnabled(true);
+        mStopButton.setEnabled(false);
+    }
+
+    @Override
+    public void onLog(String msg) {
+        if (mLogList != null && msg.contains(logEndsWith)) {
+            mLogList.add(getSpanned(msg));
+            mLogAdapter.notifyDataSetChanged();
+            setOptionsButtonsEnabled(true);
+        }
+    }
+
+    private List<Spanned> getSpannedList(List<String> loglist) {
+        List<Spanned> logSpannedList = new ArrayList<>();
+        if (loglist != null && loglist.size() > 0) {
+            for (String log : loglist) {
+                if (log.contains(logEndsWith)) {
+                    logSpannedList.add(getSpanned(log));
+                }
+            }
+        }
+        return logSpannedList;
+    }
+
+    private Spanned getSpanned(String msg) {
+        if (msg.contains(" V ")) {
+            String html = "<b><font color=\"#5D8C00\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        } else if (msg.contains(" D ")) {
+            String html = "<b><font color=\"#0CB2B3\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        } else if (msg.contains(" I ")) {
+            String html = "<b><font color=\"#57BF4B\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        } else if (msg.contains(" W ")) {
+            String html = "<b><font color=\"#BF9F03\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        } else if (msg.contains(" E ")) {
+            String html = "<b><font color=\"#CC1921\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        } else {
+            String html = "<b><font color=\"#CC4141\">" + msg + "</font></b>";
+            return Html.fromHtml(html);
+        }
+    }
+
+    @SuppressLint("WrongConstant")
+    @Override
+    public void onSaved(boolean success, String path) {
+        if (success) {
+            Snackbar.make(getWindow().getDecorView().findViewById(android.R.id.content), String.format(
+                    getString(R.string.dump_logcat_success),
+                    path), Snackbar.LENGTH_LONG).show();
+        } else {
+            Snackbar.make(getWindow().getDecorView().findViewById(android.R.id.content), R.string.dump_logcat_failure, Snackbar.LENGTH_SHORT).show();
+        }
+    }
+}

+ 383 - 0
debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugLogService.java

@@ -0,0 +1,383 @@
+package com.hgsoft.debuglogshow;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.text.format.DateFormat;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.LinkedList;
+import java.util.List;
+
+public class DebugLogService extends Service implements Logcat.Callback, Runnable {
+
+    private static final int MSG_STARTED = 0;
+    private static final int MSG_STOPPED = 1;
+    private static final int MSG_ONLOG = 2;
+    private static final int MSG_SAVED = 3;
+
+    private static final int MAX_LINES = 20000;
+
+    public static final boolean isPOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+    public static final boolean isOOrLater = isPOrLater || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+
+    private Logcat mLogcat = null;
+    private LinkedList<String> mLogList = new LinkedList<String>();
+    private Thread mSaveThread = null;
+    private final RemoteCallbackList<IDebugLogServiceCallback> mCallbacks = new RemoteCallbackList<IDebugLogServiceCallback>();
+    private final IBinder mBinder = new DebugLogServiceStub(this);
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    static class DebugLogServiceStub extends IDebugLogService.Stub {
+        private DebugLogService mService;
+        DebugLogServiceStub(DebugLogService service) {
+            mService = service;
+        }
+        public void start() {
+            mService.start();
+        }
+        public void stop() {
+            mService.stop();
+        }
+        public void clear() {
+            mService.clear();
+        }
+        public void save() {
+            mService.save();
+        }
+        public void registerCallback(IDebugLogServiceCallback cb) {
+            mService.registerCallback(cb);
+        }
+        public void unregisterCallback(IDebugLogServiceCallback cb) {
+            mService.unregisterCallback(cb);
+        }
+    }
+
+    private synchronized void sendMessage(int what, String str) {
+        int i = mCallbacks.beginBroadcast();
+        while (i > 0) {
+            i--;
+            final IDebugLogServiceCallback cb = mCallbacks.getBroadcastItem(i);
+            try {
+                switch (what) {
+                case MSG_STOPPED:
+                    cb.onStopped();
+                    break;
+                case MSG_STARTED: {
+                    cb.onStarted(mLogList);
+                    break;
+                }
+                case MSG_ONLOG:
+                    cb.onLog(str);
+                    break;
+                case MSG_SAVED:
+                    cb.onSaved(str != null ? true : false, str);
+                    break;
+                }
+            } catch (RemoteException e) {
+            }
+        }
+        mCallbacks.finishBroadcast();
+    }
+
+    @Override
+    public synchronized void onLog(String log) {
+        if (mLogList.size() > MAX_LINES) {
+            mLogList.remove(0);
+        }
+        mLogList.add(log);
+        sendMessage(MSG_ONLOG, log);
+    }
+
+    public synchronized void start() {
+        if (mLogcat != null)
+            return;
+        clear();
+        mLogcat = new Logcat();
+        mLogcat.start(this);
+
+        ContextCompat.startForegroundService(this, new Intent(this, DebugLogService.class));
+        sendMessage(MSG_STARTED, null);
+
+    }
+
+    public synchronized void stop() {
+        mLogcat.stop();
+        mLogcat = null;
+        sendMessage(MSG_STOPPED, null);
+        stopForeground(true);
+        stopSelf();
+    }
+
+    public synchronized void clear() {
+        mLogList.clear();
+    }
+
+    @Override
+    public void run() {
+        final CharSequence timestamp = DateFormat.format(
+                "yyyyMMdd_kkmmss", System.currentTimeMillis());
+        final String filename = getApplicationContext().getExternalFilesDir("Log").getAbsolutePath() + "_" + timestamp + ".log";
+        boolean saved = true;
+        FileOutputStream fos = null;
+        OutputStreamWriter output = null;
+        BufferedWriter bw = null;
+
+        try {
+            fos = new FileOutputStream(filename);
+            output = new OutputStreamWriter(fos);
+            bw = new BufferedWriter(output);
+            synchronized (this) {
+                for (String line : mLogList) {
+                    bw.write(line);
+                    bw.newLine();
+                }
+            }
+        } catch (FileNotFoundException e) {
+            saved = false;
+        } catch (IOException ioe) {
+            saved = false;
+        } finally {
+            saved &= DebugUtil.close(bw);
+            saved &= DebugUtil.close(output);
+            saved &= DebugUtil.close(fos);
+        }
+        synchronized (this) {
+            mSaveThread = null;
+            sendMessage(MSG_SAVED, saved ? filename : null);
+        }
+    }
+
+    public synchronized void save() {
+        if (mSaveThread != null) {
+            try {
+                mSaveThread.join();
+            } catch (InterruptedException e) {}
+            mSaveThread = null;
+        }
+        mSaveThread = new Thread(this);
+        mSaveThread.start();
+    }
+
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isOOrLater) {
+            forceForeground();
+        }
+        return START_STICKY;
+    }
+
+    @TargetApi(Build.VERSION_CODES.O)
+    private void forceForeground() {
+        if (isOOrLater) {
+            createDebugServiceChannel(getApplicationContext());
+        }
+        final Intent debugLogIntent = new Intent(this, DebugLogActivity.class);
+        debugLogIntent.setAction("android.intent.action.MAIN");
+        debugLogIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        final PendingIntent pi = PendingIntent.getActivity(this, 0, debugLogIntent, 0);
+
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "vlc_debug");
+        builder.setContentTitle(getResources().getString(R.string.log_service_title));
+        builder.setContentText(getResources().getString(R.string.log_service_text));
+        builder.setSmallIcon(R.drawable.ic_stat_vlc);
+        builder.setContentIntent(pi);
+        final Notification notification = builder.build();
+        startForeground(3, notification);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private void createDebugServiceChannel(Context context) {
+        NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
+        String name = context.getString(R.string.debug_logs);
+        NotificationChannel channel = new NotificationChannel("vlc_debug", name, NotificationManager.IMPORTANCE_LOW);
+        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
+        notificationManager.createNotificationChannel(channel);
+    }
+
+    private void registerCallback(IDebugLogServiceCallback cb) {
+        if (cb != null) {
+            mCallbacks.register(cb);
+            sendMessage(mLogcat != null ? MSG_STARTED : MSG_STOPPED, null);
+        }
+    }
+
+    private void unregisterCallback(IDebugLogServiceCallback cb) {
+        if (cb != null)
+            mCallbacks.unregister(cb);
+    }
+
+    public static class Client {
+
+        public interface Callback {
+            void onStarted(List<String> lostList);
+            void onStopped();
+            void onLog(String msg);
+            void onSaved(boolean success, String path);
+        }
+
+        private boolean mBound = false;
+        private final Context mContext;
+        private Callback mCallback;
+        private IDebugLogService mIDebugLogService;
+        private Handler mHandler;
+
+        private final IDebugLogServiceCallback.Stub mICallback = new IDebugLogServiceCallback.Stub() {
+            @Override
+            public void onStopped() throws RemoteException {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onStopped();
+                    }
+                });
+            }
+            @Override
+            public void onStarted(final List<String> logList) throws RemoteException {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onStarted(logList);
+                    }
+                });
+            }
+            @Override
+            public void onLog(final String msg) throws RemoteException {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onLog(msg);
+                    }
+                });
+            }
+            @Override
+            public void onSaved(final boolean success, final String path) throws RemoteException {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback.onSaved(success, path);
+                    }
+                });
+            }
+        };
+
+        private final ServiceConnection mServiceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                synchronized (Client.this) {
+                    mIDebugLogService = IDebugLogService.Stub.asInterface(service);
+                    try {
+                        mIDebugLogService.registerCallback(mICallback);
+                    } catch (RemoteException e) {
+                        release();
+                        mContext.stopService(new Intent(mContext, DebugLogService.class));
+                        mCallback.onStopped();
+                    }
+                }
+            }
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                release();
+                mContext.stopService(new Intent(mContext, DebugLogService.class));
+                mCallback.onStopped();
+            }
+        };
+
+        public Client(Context context, Callback cb) throws IllegalArgumentException {
+            if (context == null | cb == null)
+                throw new IllegalArgumentException("Context and Callback can't be null");
+
+            mContext = context;
+            mCallback = cb;
+            mHandler = new Handler(Looper.getMainLooper());
+            mBound = mContext.bindService(new Intent(mContext, DebugLogService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
+        }
+
+        public boolean start() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.start();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+        public boolean stop() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.stop();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+        public boolean clear() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.clear();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+        public boolean save() {
+            synchronized (this) {
+                if (mIDebugLogService != null) {
+                    try {
+                        mIDebugLogService.save();
+                        return true;
+                    } catch (RemoteException e) {
+                    }
+                }
+                return false;
+            }
+        }
+        public void release() {
+            if (mBound) {
+                synchronized (this) {
+                    if (mIDebugLogService != null && mICallback != null) {
+                        try {
+                            mIDebugLogService.unregisterCallback(mICallback);
+                        } catch (RemoteException e) {
+                        }
+                        mIDebugLogService = null;
+                    }
+                }
+                mBound = false;
+                mContext.unbindService(mServiceConnection);
+            }
+            mHandler.removeCallbacksAndMessages(null);
+        }
+    }
+}

+ 43 - 0
debuglogshow/src/main/java/com/hgsoft/debuglogshow/DebugUtil.java

@@ -0,0 +1,43 @@
+package com.hgsoft.debuglogshow;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public class DebugUtil {
+
+    static String mainPid = String.valueOf(android.os.Process.myPid());
+
+    static boolean close(Closeable closeable) {
+        if (closeable != null)
+            try {
+                closeable.close();
+                return true;
+            } catch (IOException e) {}
+        return false;
+    }
+
+
+    protected static String getSetting(Context context, String key, String defaultValue) {
+        try {
+            SharedPreferences sharedata = context.getSharedPreferences("logEndsWith", 0);
+            return sharedata.getString(key, defaultValue);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return defaultValue;
+    }
+
+
+    protected static void saveSetting(Context context, String key, String value) {
+        try {
+            SharedPreferences.Editor sharedata = context.getSharedPreferences("logEndsWith", 0).edit();
+            sharedata.putString(key, value);
+            sharedata.apply();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 147 - 0
debuglogshow/src/main/java/com/hgsoft/debuglogshow/Logcat.java

@@ -0,0 +1,147 @@
+package com.hgsoft.debuglogshow;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+public class Logcat implements Runnable {
+    public final static String TAG = "Logcat";
+    private Callback mCallback = null;
+    private Thread mThread = null;
+    private Process mProcess = null;
+    private boolean mRun = false;
+
+    public interface Callback {
+        public void onLog(String log);
+    }
+
+    public Logcat() {
+    }
+
+    @Override
+    public void run() {
+        final String[] args = { "logcat", "|", "grep", DebugUtil.mainPid };
+        InputStreamReader input = null;
+        BufferedReader br = null;
+        try {
+            synchronized (this) {
+                if (!mRun)
+                    return;
+                mProcess = Runtime.getRuntime().exec(args);
+                input = new InputStreamReader(
+                        mProcess.getInputStream());
+            }
+            br = new BufferedReader(input);
+            String line;
+
+            while ((line = br.readLine()) != null) {
+                mCallback.onLog(line);
+            }
+
+        } catch (IOException e) {
+        } finally {
+            DebugUtil.close(input);
+            DebugUtil.close(br);
+        }
+    }
+
+    /**
+     * Start a thread that will send logcat via a callback
+     * @param callback
+     */
+    public synchronized void start(Callback callback) {
+        if (callback == null)
+            throw new IllegalArgumentException("callback should not be null");
+        if (mThread != null || mProcess != null)
+            throw new IllegalStateException("logcat is already started");
+        mCallback = callback;
+        mRun = true;
+        mThread = new Thread(this);
+        mThread.start();
+    }
+
+    /**
+     * Stop the thread previously started
+     */
+    public synchronized void stop() {
+        mRun = false;
+        if (mProcess != null) {
+            mProcess.destroy();
+            mProcess = null;
+        }
+        try {
+            mThread.join();
+        } catch (InterruptedException e) {
+        }
+        mThread = null;
+        mCallback = null;
+    }
+
+    /**
+     * Writes the current app logcat to a file.
+     *
+     * @param filename The filename to save it as
+     * @throws IOException
+     */
+    public static void writeLogcat(String filename) throws IOException {
+        String[] args = { "logcat", "-v", "time", "-d" };
+
+        Process process = Runtime.getRuntime().exec(args);
+
+        InputStreamReader input = new InputStreamReader(process.getInputStream());
+
+        FileOutputStream fileStream;
+        try {
+            fileStream = new FileOutputStream(filename);
+        } catch( FileNotFoundException e) {
+            return;
+        }
+
+        OutputStreamWriter output = new OutputStreamWriter(fileStream);
+        BufferedReader br = new BufferedReader(input);
+        BufferedWriter bw = new BufferedWriter(output);
+
+        try {
+            String line;
+            while ((line = br.readLine()) != null) {
+                bw.write(line);
+                bw.newLine();
+            }
+        }catch(Exception e) {}
+        finally {
+            DebugUtil.close(bw);
+            DebugUtil.close(output);
+            DebugUtil.close(br);
+            DebugUtil.close(input);
+        }
+    }
+
+    /**
+     * Get the last 500 lines of the application logcat.
+     *
+     * @return the log string.
+     * @throws IOException
+     */
+    public static String getLogcat() throws IOException {
+        String[] args = { "logcat", "-v", "time", "-d", "-t", "500" };
+
+        Process process = Runtime.getRuntime().exec(args);
+        InputStreamReader input = new InputStreamReader(
+                process.getInputStream());
+        BufferedReader br = new BufferedReader(input);
+        StringBuilder log = new StringBuilder();
+        String line;
+
+        while ((line = br.readLine()) != null)
+            log.append(line + "\n");
+
+        DebugUtil.close(br);
+        DebugUtil.close(input);
+
+        return log.toString();
+    }
+}

binární
debuglogshow/src/main/res/drawable/ic_stat_vlc.png


+ 80 - 0
debuglogshow/src/main/res/layout/debug_log.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="16dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" >
+
+        <Button
+            android:id="@+id/start_log"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/start_logging" />
+
+        <Button
+            android:id="@+id/stop_log"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/stop_logging" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <Button
+            android:id="@+id/copy_to_clipboard"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/copy_to_clipboard" />
+
+        <Button
+            android:id="@+id/save_to_file"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/dump_logcat" />
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/test_log"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:text="test" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <Button
+            android:id="@+id/back_last_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/back_last_view" />
+
+        <Button
+            android:id="@+id/clear_log"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.50"
+            android:text="@string/clear_log" />
+    </LinearLayout>
+
+
+
+    <ListView
+        android:id="@+id/log_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="10dip" />
+
+</LinearLayout>

+ 5 - 0
debuglogshow/src/main/res/layout/debug_log_item.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:typeface="monospace" />

+ 15 - 0
debuglogshow/src/main/res/values/strings.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="debug_logs">调试日志</string>
+    <string name="start_logging">开始记录日志</string>
+    <string name="stop_logging">停止记录日志</string>
+    <string name="clear_log">清除日志</string>
+    <string name="log_service_title">日志正在记录</string>
+    <string name="log_service_text">打开日志控制台</string>
+    <string name="copy_to_clipboard">复制到剪贴板</string>
+    <string name="copied_to_clipboard">日志内容已复制到剪贴板。</string>
+    <string name="dump_logcat">logcat 日志转储</string>
+    <string name="dump_logcat_success">Logcat 日志已成功转储至 %1$s!</string>
+    <string name="dump_logcat_failure">logcat 转储失败。</string>
+    <string name="back_last_view">返回</string>
+</resources>

+ 17 - 0
debuglogshow/src/test/java/com/hgsoft/debuglogshow/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.hgsoft.debuglogshow;
+
+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);
+    }
+}

+ 208 - 0
doc/decode_mars_crypt_log_file.py

@@ -0,0 +1,208 @@
+#!/usr/bin/python
+
+import sys
+import os
+import glob
+import zlib
+import struct
+import binascii
+import pyelliptic
+import traceback
+
+
+MAGIC_NO_COMPRESS_START = 0x03
+MAGIC_NO_COMPRESS_START1 = 0x06
+MAGIC_NO_COMPRESS_NO_CRYPT_START = 0x08
+MAGIC_COMPRESS_START = 0x04
+MAGIC_COMPRESS_START1 = 0x05
+MAGIC_COMPRESS_START2 = 0x07
+MAGIC_COMPRESS_NO_CRYPT_START = 0x09
+
+MAGIC_END = 0x00
+
+lastseq = 0
+
+PRIV_KEY = "05cb6f67b111a49660d706b15875b1ffc840db68e3545bd2786f02ac9a1233ef"
+PUB_KEY = "94e62d97637f357fbd20f0c1f667a67c2f675e158e46015dd0cc54cb3995d0a5d468f7e98b20aec266effb61ec0a2321fb1f8c61af72bf76567921a0d8305005"
+
+def tea_decipher(v, k):
+    op = 0xffffffffL
+    v0, v1 = struct.unpack('=LL', v[0:8])
+    k1, k2, k3, k4 = struct.unpack('=LLLL', k[0:16])
+    delta = 0x9E3779B9L
+    s = (delta << 4) & op
+    for i in xrange(16):
+        v1 = (v1 - (((v0<<4) + k3) ^ (v0 + s) ^ ((v0>>5) + k4))) & op
+        v0 = (v0 - (((v1<<4) + k1) ^ (v1 + s) ^ ((v1>>5) + k2))) & op
+        s = (s - delta) & op
+    return struct.pack('=LL', v0, v1)
+
+
+def tea_decrypt(v, k):
+    num = len(v) / 8 * 8
+    ret = ''
+    for i in xrange(0, num, 8):
+        x = tea_decipher(v[i:i+8], k) 
+        ret += x
+
+    ret += v[num:]
+    return ret
+
+
+def IsGoodLogBuffer(_buffer, _offset, count):
+
+    if _offset == len(_buffer): return (True, '')
+
+    magic_start = _buffer[_offset] 
+    if MAGIC_NO_COMPRESS_START==magic_start or MAGIC_COMPRESS_START==magic_start or MAGIC_COMPRESS_START1==magic_start:
+        crypt_key_len = 4
+    elif MAGIC_COMPRESS_START2==magic_start or MAGIC_NO_COMPRESS_START1==magic_start or MAGIC_NO_COMPRESS_NO_CRYPT_START==magic_start or MAGIC_COMPRESS_NO_CRYPT_START==magic_start:
+        crypt_key_len = 64
+    else:
+        return (False, '_buffer[%d]:%d != MAGIC_NUM_START'%(_offset, _buffer[_offset]))
+
+    headerLen = 1 + 2 + 1 + 1 + 4 + crypt_key_len
+
+    if _offset + headerLen + 1 + 1 > len(_buffer): return (False, 'offset:%d > len(buffer):%d'%(_offset, len(_buffer)))
+    length = struct.unpack_from("I", buffer(_buffer, _offset+headerLen-4-crypt_key_len, 4))[0]
+    if _offset + headerLen + length + 1 > len(_buffer): return (False, 'log length:%d, end pos %d > len(buffer):%d'%(length, _offset + headerLen + length + 1, len(_buffer)))
+    if MAGIC_END!=_buffer[_offset + headerLen + length]: return (False, 'log length:%d, buffer[%d]:%d != MAGIC_END'%(length, _offset + headerLen + length, _buffer[_offset + headerLen + length]))
+
+
+    if (1>=count): return (True, '')
+    else: return IsGoodLogBuffer(_buffer, _offset+headerLen+length+1, count-1)
+        
+    
+def GetLogStartPos(_buffer, _count):
+    offset = 0
+    while True:
+        if offset >= len(_buffer): break
+        
+        if MAGIC_NO_COMPRESS_START==_buffer[offset] or MAGIC_NO_COMPRESS_START1==_buffer[offset] or MAGIC_COMPRESS_START==_buffer[offset] or MAGIC_COMPRESS_START1==_buffer[offset] or MAGIC_COMPRESS_START2==_buffer[offset] or MAGIC_COMPRESS_NO_CRYPT_START==_buffer[offset] or MAGIC_NO_COMPRESS_NO_CRYPT_START==_buffer[offset]:
+            if IsGoodLogBuffer(_buffer, offset, _count)[0]: return offset
+        offset+=1
+        
+    return -1    
+    
+def DecodeBuffer(_buffer, _offset, _outbuffer):
+    
+    if _offset >= len(_buffer): return -1
+    # if _offset + 1 + 4 + 1 + 1 > len(_buffer): return -1
+    ret = IsGoodLogBuffer(_buffer, _offset, 1)
+    if not ret[0]:
+        fixpos = GetLogStartPos(_buffer[_offset:], 1)
+        if -1==fixpos: 
+            return -1
+        else:
+            _outbuffer.extend("[F]decode_log_file.py decode error len=%d, result:%s \n"%(fixpos, ret[1]))
+            _offset += fixpos 
+
+    magic_start = _buffer[_offset]
+    if MAGIC_NO_COMPRESS_START==magic_start or MAGIC_COMPRESS_START==magic_start or MAGIC_COMPRESS_START1==magic_start:
+        crypt_key_len = 4
+    elif MAGIC_COMPRESS_START2==magic_start or MAGIC_NO_COMPRESS_START1==magic_start or MAGIC_NO_COMPRESS_NO_CRYPT_START==magic_start or MAGIC_COMPRESS_NO_CRYPT_START==magic_start:
+        crypt_key_len = 64
+    else:
+        _outbuffer.extend('in DecodeBuffer _buffer[%d]:%d != MAGIC_NUM_START'%(_offset, magic_start))
+        return -1
+
+    headerLen = 1 + 2 + 1 + 1 + 4 + crypt_key_len
+    length = struct.unpack_from("I", buffer(_buffer, _offset+headerLen-4-crypt_key_len, 4))[0]
+    tmpbuffer = bytearray(length)
+
+    seq=struct.unpack_from("H", buffer(_buffer, _offset+headerLen-4-crypt_key_len-2-2, 2))[0]
+    begin_hour=struct.unpack_from("c", buffer(_buffer, _offset+headerLen-4-crypt_key_len-1-1, 1))[0]
+    end_hour=struct.unpack_from("c", buffer(_buffer, _offset+headerLen-4-crypt_key_len-1, 1))[0]
+
+    global lastseq
+    if seq != 0 and seq != 1 and lastseq != 0 and seq != (lastseq+1):
+        _outbuffer.extend("[F]decode_log_file.py log seq:%d-%d is missing\n" %(lastseq+1, seq-1))
+
+    if seq != 0:
+        lastseq = seq
+
+    tmpbuffer[:] = _buffer[_offset+headerLen:_offset+headerLen+length]
+
+    try:
+        decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
+
+        if MAGIC_NO_COMPRESS_START1==_buffer[_offset]:
+            pass
+        
+        elif MAGIC_COMPRESS_START2==_buffer[_offset]:
+            svr = pyelliptic.ECC(curve='secp256k1')
+            client = pyelliptic.ECC(curve='secp256k1')
+            client.pubkey_x = str(buffer(_buffer, _offset+headerLen-crypt_key_len, crypt_key_len/2))
+            client.pubkey_y = str(buffer(_buffer, _offset+headerLen-crypt_key_len/2, crypt_key_len/2))
+
+            svr.privkey = binascii.unhexlify(PRIV_KEY)
+            tea_key = svr.get_ecdh_key(client.get_pubkey())
+
+            tmpbuffer = tea_decrypt(tmpbuffer, tea_key)
+            tmpbuffer = decompressor.decompress(str(tmpbuffer))
+        elif MAGIC_COMPRESS_START==_buffer[_offset] or MAGIC_COMPRESS_NO_CRYPT_START==_buffer[_offset]:
+            tmpbuffer = decompressor.decompress(str(tmpbuffer))
+        elif MAGIC_COMPRESS_START1==_buffer[_offset]:
+            decompress_data = bytearray()
+            while len(tmpbuffer) > 0:
+                single_log_len = struct.unpack_from("H", buffer(tmpbuffer, 0, 2))[0]
+                decompress_data.extend(tmpbuffer[2:single_log_len+2])
+                tmpbuffer[:] = tmpbuffer[single_log_len+2:len(tmpbuffer)]
+
+            tmpbuffer = decompressor.decompress(str(decompress_data))
+
+        else:
+            pass
+
+            # _outbuffer.extend('seq:%d, hour:%d-%d len:%d decompress:%d\n' %(seq, ord(begin_hour), ord(end_hour), length, len(tmpbuffer)))
+    except Exception, e:
+        traceback.print_exc()  
+        _outbuffer.extend("[F]decode_log_file.py decompress err, " + str(e) + "\n")
+        return _offset+headerLen+length+1
+
+    _outbuffer.extend(tmpbuffer)
+    
+    return _offset+headerLen+length+1
+
+
+def ParseFile(_file, _outfile):
+    fp = open(_file, "rb")
+    _buffer = bytearray(os.path.getsize(_file))
+    fp.readinto(_buffer)
+    fp.close()
+    startpos = GetLogStartPos(_buffer, 2)
+    if -1==startpos:
+        return
+    
+    outbuffer = bytearray()
+    
+    while True:
+        startpos = DecodeBuffer(_buffer, startpos, outbuffer)
+        if -1==startpos: break;
+    
+    if 0==len(outbuffer): return
+    
+    fpout = open(_outfile, "wb")
+    fpout.write(outbuffer)
+    fpout.close()
+    
+def main(args):
+    global lastseq
+
+    if 1==len(args):
+        if os.path.isdir(args[0]):
+            filelist = glob.glob(args[0] + "/*.xlog")
+            for filepath in filelist:
+                lastseq = 0
+                ParseFile(filepath, filepath+".log")
+        else: ParseFile(args[0], args[0]+".log")    
+    elif 2==len(args):
+        ParseFile(args[0], args[1])    
+    else: 
+        filelist = glob.glob("*.xlog")
+        for filepath in filelist:
+            lastseq = 0
+            ParseFile(filepath, filepath+".log")
+
+if __name__ == "__main__":
+    main(sys.argv[1:])

+ 169 - 0
doc/decode_mars_nocrypt_log_file.py

@@ -0,0 +1,169 @@
+#!/usr/bin/python
+
+import sys
+import os
+import glob
+import zlib
+import struct
+import binascii
+import traceback
+
+
+MAGIC_NO_COMPRESS_START = 0x03
+MAGIC_NO_COMPRESS_START1 = 0x06
+MAGIC_NO_COMPRESS_NO_CRYPT_START = 0x08
+MAGIC_COMPRESS_START = 0x04
+MAGIC_COMPRESS_START1 = 0x05
+MAGIC_COMPRESS_START2 = 0x07
+MAGIC_COMPRESS_NO_CRYPT_START = 0x09
+
+MAGIC_END = 0x00
+
+lastseq = 0
+
+
+def IsGoodLogBuffer(_buffer, _offset, count):
+
+    if _offset == len(_buffer): return (True, '')
+
+    magic_start = _buffer[_offset] 
+    if MAGIC_NO_COMPRESS_START==magic_start or MAGIC_COMPRESS_START==magic_start or MAGIC_COMPRESS_START1==magic_start:
+        crypt_key_len = 4
+    elif MAGIC_COMPRESS_START2==magic_start or MAGIC_NO_COMPRESS_START1==magic_start or MAGIC_NO_COMPRESS_NO_CRYPT_START==magic_start or MAGIC_COMPRESS_NO_CRYPT_START==magic_start:
+        crypt_key_len = 64
+    else:
+        return (False, '_buffer[%d]:%d != MAGIC_NUM_START'%(_offset, _buffer[_offset]))
+
+    headerLen = 1 + 2 + 1 + 1 + 4 + crypt_key_len
+
+    if _offset + headerLen + 1 + 1 > len(_buffer): return (False, 'offset:%d > len(buffer):%d'%(_offset, len(_buffer)))
+    length = struct.unpack_from("I", buffer(_buffer, _offset+headerLen-4-crypt_key_len, 4))[0]
+    if _offset + headerLen + length + 1 > len(_buffer): return (False, 'log length:%d, end pos %d > len(buffer):%d'%(length, _offset + headerLen + length + 1, len(_buffer)))
+    if MAGIC_END!=_buffer[_offset + headerLen + length]: return (False, 'log length:%d, buffer[%d]:%d != MAGIC_END'%(length, _offset + headerLen + length, _buffer[_offset + headerLen + length]))
+
+
+    if (1>=count): return (True, '')
+    else: return IsGoodLogBuffer(_buffer, _offset+headerLen+length+1, count-1)
+        
+    
+def GetLogStartPos(_buffer, _count):
+    offset = 0
+    while True:
+        if offset >= len(_buffer): break
+        
+        if MAGIC_NO_COMPRESS_START==_buffer[offset] or MAGIC_NO_COMPRESS_START1==_buffer[offset] or MAGIC_COMPRESS_START==_buffer[offset] or MAGIC_COMPRESS_START1==_buffer[offset] or MAGIC_COMPRESS_START2==_buffer[offset] or MAGIC_COMPRESS_NO_CRYPT_START==_buffer[offset] or MAGIC_NO_COMPRESS_NO_CRYPT_START==_buffer[offset]:
+            if IsGoodLogBuffer(_buffer, offset, _count)[0]: return offset
+        offset+=1
+        
+    return -1    
+    
+def DecodeBuffer(_buffer, _offset, _outbuffer):
+    
+    if _offset >= len(_buffer): return -1
+    # if _offset + 1 + 4 + 1 + 1 > len(_buffer): return -1
+    ret = IsGoodLogBuffer(_buffer, _offset, 1)
+    if not ret[0]:
+        fixpos = GetLogStartPos(_buffer[_offset:], 1)
+        if -1==fixpos: 
+            return -1
+        else:
+            _outbuffer.extend("[F]decode_log_file.py decode error len=%d, result:%s \n"%(fixpos, ret[1]))
+            _offset += fixpos 
+
+    magic_start = _buffer[_offset]
+    if MAGIC_NO_COMPRESS_START==magic_start or MAGIC_COMPRESS_START==magic_start or MAGIC_COMPRESS_START1==magic_start:
+        crypt_key_len = 4
+    elif MAGIC_COMPRESS_START2==magic_start or MAGIC_NO_COMPRESS_START1==magic_start or MAGIC_NO_COMPRESS_NO_CRYPT_START==magic_start or MAGIC_COMPRESS_NO_CRYPT_START==magic_start:
+        crypt_key_len = 64
+    else:
+        _outbuffer.extend('in DecodeBuffer _buffer[%d]:%d != MAGIC_NUM_START'%(_offset, magic_start))
+        return -1
+
+    headerLen = 1 + 2 + 1 + 1 + 4 + crypt_key_len
+    length = struct.unpack_from("I", buffer(_buffer, _offset+headerLen-4-crypt_key_len, 4))[0]
+    tmpbuffer = bytearray(length)
+
+    seq=struct.unpack_from("H", buffer(_buffer, _offset+headerLen-4-crypt_key_len-2-2, 2))[0]
+    begin_hour=struct.unpack_from("c", buffer(_buffer, _offset+headerLen-4-crypt_key_len-1-1, 1))[0]
+    end_hour=struct.unpack_from("c", buffer(_buffer, _offset+headerLen-4-crypt_key_len-1, 1))[0]
+
+    global lastseq
+    if seq != 0 and seq != 1 and lastseq != 0 and seq != (lastseq+1):
+        _outbuffer.extend("[F]decode_log_file.py log seq:%d-%d is missing\n" %(lastseq+1, seq-1))
+
+    if seq != 0:
+        lastseq = seq
+
+    tmpbuffer[:] = _buffer[_offset+headerLen:_offset+headerLen+length]
+
+    try:
+        decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
+
+        if MAGIC_NO_COMPRESS_START1==_buffer[_offset] or MAGIC_COMPRESS_START2==_buffer[_offset]:
+            print("use wrong decode script")
+        elif MAGIC_COMPRESS_START==_buffer[_offset] or MAGIC_COMPRESS_NO_CRYPT_START==_buffer[_offset]:
+            tmpbuffer = decompressor.decompress(str(tmpbuffer))
+        elif MAGIC_COMPRESS_START1==_buffer[_offset]:
+            decompress_data = bytearray()
+            while len(tmpbuffer) > 0:
+                single_log_len = struct.unpack_from("H", buffer(tmpbuffer, 0, 2))[0]
+                decompress_data.extend(tmpbuffer[2:single_log_len+2])
+                tmpbuffer[:] = tmpbuffer[single_log_len+2:len(tmpbuffer)]
+
+            tmpbuffer = decompressor.decompress(str(decompress_data))
+
+        else:
+            pass
+
+            # _outbuffer.extend('seq:%d, hour:%d-%d len:%d decompress:%d\n' %(seq, ord(begin_hour), ord(end_hour), length, len(tmpbuffer)))
+    except Exception, e:
+        traceback.print_exc()  
+        _outbuffer.extend("[F]decode_log_file.py decompress err, " + str(e) + "\n")
+        return _offset+headerLen+length+1
+
+    _outbuffer.extend(tmpbuffer)
+    
+    return _offset+headerLen+length+1
+
+
+def ParseFile(_file, _outfile):
+    fp = open(_file, "rb")
+    _buffer = bytearray(os.path.getsize(_file))
+    fp.readinto(_buffer)
+    fp.close()
+    startpos = GetLogStartPos(_buffer, 2)
+    if -1==startpos:
+        return
+    
+    outbuffer = bytearray()
+    
+    while True:
+        startpos = DecodeBuffer(_buffer, startpos, outbuffer)
+        if -1==startpos: break;
+    
+    if 0==len(outbuffer): return
+    
+    fpout = open(_outfile, "wb")
+    fpout.write(outbuffer)
+    fpout.close()
+    
+def main(args):
+    global lastseq
+
+    if 1==len(args):
+        if os.path.isdir(args[0]):
+            filelist = glob.glob(args[0] + "/*.xlog")
+            for filepath in filelist:
+                lastseq = 0
+                ParseFile(filepath, filepath+".log")
+        else: ParseFile(args[0], args[0]+".log")    
+    elif 2==len(args):
+        ParseFile(args[0], args[1])    
+    else: 
+        filelist = glob.glob("*.xlog")
+        for filepath in filelist:
+            lastseq = 0
+            ParseFile(filepath, filepath+".log")
+
+if __name__ == "__main__":
+    main(sys.argv[1:])

+ 19 - 0
doc/gen_key.py

@@ -0,0 +1,19 @@
+from binascii import hexlify, unhexlify
+
+import pyelliptic
+
+CURVE = 'secp256k1'
+
+svr = pyelliptic.ECC(curve=CURVE)
+
+svr_pubkey = svr.get_pubkey()
+svr_privkey = svr.get_privkey()
+
+
+print("save private key")
+
+print(hexlify(svr_privkey))
+
+print("\nappender_open's parameter:")
+print("%s%s" %(hexlify(svr.pubkey_x),  hexlify(svr.pubkey_y)))
+

binární
doc/logOperator.zip


binární
doc/logOperator/AdbWinApi.dll


binární
doc/logOperator/AdbWinUsbApi.dll


binární
doc/logOperator/XlogDecode.exe


binární
doc/logOperator/adb.exe


+ 4 - 0
doc/logOperator/导出日志.bat

@@ -0,0 +1,4 @@
+CHCP 65001
+cd /d %~dp0
+adb.exe pull /sdcard/marsxloglibrary/XLog/. XLog
+cmd.exe

binární
doc/logOperator/日志操作教程/日志操作第1步.png


binární
doc/logOperator/日志操作教程/日志操作第2步.png


binární
doc/logOperator/日志操作教程/日志操作第3步.png


+ 0 - 0
doc/logOperator/此目录最好不要放入有中文的文件夹中.txt


+ 5 - 0
doc/日志解密密钥.txt

@@ -0,0 +1,5 @@
+save private key
+fc4b37a83b7b51a7d4070a404b2aeef4fbc4e6cfdce4dbacb986a00a43aba012
+
+appender_open's parameter:
+0a10a144931f83492bb0c559d525bb1babb1ed23ca889eacf87ccb8357e638d77a36297599a6324861776148206c5ae8329dfb67a3b6325ea8f5920145b6182c

+ 52 - 0
doc/解密说明.txt

@@ -0,0 +1,52 @@
+http://svn.hgits.cn:81/svn/研发资料/创新研究院/研发中心/端组APP/03.公共模块/01 Android和IOS日志模块
+
+1, 没有加密
+Windows
+1,python 版本要求: 2.7.12 (实测2.7.0版本会报错)。
+请注意区分python版本是32位还是64位,
+如果windows系统为64位,则请下载64位python版本。
+如果windows系统为32位,则请下载32位python版本。
+同时请配置python的环境变量,
+可参考:http://www.cnblogs.com/dangeal/p/5455005.html
+
+解压缩命令:python decode_mars_nocrypt_log_file.py 文件名
+
+2,加密
+
+Windows
+1,python 版本要求: 2.7.12 (实测2.7.0版本会报错)。
+请注意区分python版本是32位还是64位,
+如果windows系统为64位,则请下载64位python版本。
+如果windows系统为32位,则请下载32位python版本。
+同时请配置python的环境变量,
+可参考:http://www.cnblogs.com/dangeal/p/5455005.html
+
+2,下载安装 openssl windows,注意区分自己机器Win32还是Win64。
+(必须是1.0.2o版本,http://slproweb.com/download/Win64OpenSSL-1_0_2o.exe)
+需要配置bin目录环境变量
+
+3,下载安装python setuptools 工具 ,解压之后在终端进入到解压的当前目录中:使用命令:
+python setup.py install(https://pypi.org/project/setuptools/) 也可选择其它方式
+
+4,下载安装python Pip工具 解压之后,在终端进入到解压目录的pip目录下,使用命令: python setup.py install 进行安装
+(https://pip.pypa.io/en/stable/installing/) (https://pypi.org/project/pip/#files)也可选择其它方式
+
+5,下载 pyelliptic1.5.7 解压后在终端进入到执行:python setup.py install 安装pyelliptic1.5.7(必须是1.5.7版本)
+
+OS X
+下载 pyelliptic1.5.7
+解压执行:python setup.py install 安装 pyelliptic1.5.7 注:如果没权限sudo python setup.py install
+
+Linux/Unix
+安装 openssl。如已安装忽略此步骤。
+下载 pyelliptic1.5.7
+解压执行:python setup.py install 安装 pyelliptic1.5.7
+
+
+在 mars\log\crypt 下执行python gen_key.py 如果能生成成功则表示配置成功。
+python gen_key.py会生成private key 和public key,把pulic key作为appender_open 函数参数设置进去,
+private key务必保存在安全的位置,防止泄露。并把这两个key设置到 mars\log\crypt 中 decode_mars_crypt_log_file.py脚本中。
+
+解密密令-例子:python decode_mars_crypt_log_file.py Debug_20180731.xlog
+
+如果遇到 error: unpack requires a string argument of length 8。 请试着把python版本换成2.7.10以后的版本,但注意还是2.7x版本。

+ 20 - 0
gradle.properties

@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+

+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Wed Feb 19 14:27:41 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

+ 172 - 0
gradlew

@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"

+ 84 - 0
gradlew.bat

@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
hgsoftloglibrary/.gitignore

@@ -0,0 +1 @@
+/build

+ 149 - 0
hgsoftloglibrary/build.gradle

@@ -0,0 +1,149 @@
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+// https://github.com/sky-uk/gradle-maven-plugin
+//apply from: "https://gitee.com/woyiqiankun/gradle-maven-plugin/raw/master/gradle-mavenizer.gradle"
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 19
+        targetSdkVersion 30
+        versionCode 121017
+        versionName "1.2101.7"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+}
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+task androidJavadocs(type: Javadoc) {
+    failOnError false
+    title = "hgsoftloglibrary ${project.version} API"
+    description "Generates Javadoc"
+    source = android.sourceSets.main.java.srcDirs
+    classpath += files(android.bootClasspath)
+
+    android.libraryVariants.all { variant ->
+        if (variant.name == 'release') {
+            owner.classpath += variant.javaCompileProvider.get().classpath
+        }
+    }
+
+    exclude '**/R.html', '**/R.*.html', '**/index.html', '**/*.kt'
+
+    options {
+        windowTitle("hgsoftloglibrary ${project.version} Reference")
+        locale = 'zh_CN'
+        encoding = 'UTF-8'
+        charSet = 'UTF-8'
+        links("http://docs.oracle.com/javase/8/docs/api/")
+        linksOffline("http://d.android.com/reference", "${android.sdkDirectory}/docs/reference")
+        setMemberLevel(JavadocMemberLevel.PUBLIC)
+    }
+}
+
+task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
+    archiveClassifier.convention('javadoc')
+    archiveClassifier.set('javadoc')
+    from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+    archiveClassifier.convention('sources')
+    archiveClassifier.set('sources')
+    from android.sourceSets.main.java.srcDirs
+}
+
+afterEvaluate {
+    publishing {
+        publications {
+            // Creates a Maven publication called "release".
+            mavenPublish(MavenPublication) {
+                // Applies the component for the release build variant.
+                from components.release
+                artifact androidSourcesJar
+                artifact androidJavadocsJar
+                // You can then customize attributes of the publication as shown below.
+                groupId = 'com.hgsoft.app'
+                artifactId = 'loglibrary'
+                version = '1.2202.0'
+            }
+        }
+
+        repositories {
+            maven {
+                credentials {
+                    username 'yinxueqin'
+                    password 'hgits1024'
+                }
+
+                url = 'https://nexus.hgits.cn/nexus/repository/hgsoft/'
+            }
+        }
+
+        repositories {
+            maven {
+                credentials {
+                    username '60de898e6da2f73831c32a9d'
+                    password 'G-O=rDpLTK7B'
+                }
+
+                url = 'https://packages.aliyun.com/maven/repository/2117453-release-bMZITa/'
+            }
+        }
+    }
+}
+
+//project.ext {
+//    // 填你的名字
+//    mavDevelopers = ["Ch":"yinxueqin"]
+//    // 项目的网址,没有就填svn地址
+//    mavSiteUrl = "http://svn.hgits.cn:81/svn/研发资料/创新研究院/研发中心/端组APP/03.公共模块/01 Android和IOS日志模块/代码参考/loglibrary"
+//    // 填svn或者git地址
+//    mavGitUrl = "http://svn.hgits.cn:81/svn/研发资料/创新研究院/研发中心/端组APP/03.公共模块/01 Android和IOS日志模块/代码参考/loglibrary"
+//    // 填项目名称
+//    mavProjectName = 'hgsoftloglibrary'
+//    // 填GroupId,相对唯一
+//    mavProjectGroup = "hgits.cn"
+//    // 填ArtifactId,相对唯一
+//    mavProjectArtifact = "hgsoftloglibrary"
+//    // 包的版本号
+//    mavProjectVersion = "1.2100.1"
+//    // 填true
+//    mavPublishToRemoteRepo = true
+//    // 填用户名
+//    mavRemoteRepoUser = "admin"
+//    // 填对应的用户密码
+//    mavRemoteRepoPassword = "nexus_hgits"
+//    // 固定此地址
+//    mavRepoRemoteUrl = "http://218.107.19.106:19999/repository/apphgits/"
+//}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    compileOnly 'androidx.appcompat:appcompat:1.3.1'
+    api 'com.tencent.mars:mars-xlog:1.2.5'
+    api 'net.lingala.zip4j:zip4j:2.1.3'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}

+ 34 - 0
hgsoftloglibrary/consumer-rules.pro

@@ -0,0 +1,34 @@
+-ignorewarnings
+-optimizationpasses 5          # 指定代码的压缩级别
+-dontusemixedcaseclassnames   # 是否使用大小写混合
+-dontpreverify           # 混淆时是否做预校验
+-verbose                # 混淆时是否记录日志
+# 保持行号
+-keepattributes SourceFile,LineNumberTable
+# androidX
+-dontwarn org.xmlpull.v1.XmlPullParser
+-dontwarn org.xmlpull.v1.XmlSerializer
+-keep class org.xmlpull.v1.* {*;}
+-keep class com.google.android.material.** { *; }
+-dontwarn com.google.android.material.**
+-dontnote com.google.android.material.**
+-dontwarn androidx.**
+-keep class androidx.** { *; }
+-keep interface androidx.** { *; }
+
+# kotlin
+-keep class kotlin.** { *; }
+-keep class kotlin.Metadata { *; }
+-dontwarn kotlin.**
+-keepclassmembers class **$WhenMappings {
+    <fields>;
+}
+-keepclassmembers class kotlin.Metadata {
+    public <methods>;
+}
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
+}
+-keep class com.hgsoft.log.** { *; }
+# keep Xlog
+-keep class com.tencent.mars.** {*;}

+ 55 - 0
hgsoftloglibrary/proguard-rules.pro

@@ -0,0 +1,55 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+-ignorewarnings
+-optimizationpasses 5          # 指定代码的压缩级别
+-dontusemixedcaseclassnames   # 是否使用大小写混合
+-dontpreverify           # 混淆时是否做预校验
+-verbose                # 混淆时是否记录日志
+# 保持行号
+-keepattributes SourceFile,LineNumberTable
+# androidX
+-dontwarn org.xmlpull.v1.XmlPullParser
+-dontwarn org.xmlpull.v1.XmlSerializer
+-keep class org.xmlpull.v1.* {*;}
+-keep class com.google.android.material.** { *; }
+-dontwarn com.google.android.material.**
+-dontnote com.google.android.material.**
+-dontwarn androidx.**
+-keep class androidx.** { *; }
+-keep interface androidx.** { *; }
+
+# kotlin
+-keep class kotlin.** { *; }
+-keep class kotlin.Metadata { *; }
+-dontwarn kotlin.**
+-keepclassmembers class **$WhenMappings {
+    <fields>;
+}
+-keepclassmembers class kotlin.Metadata {
+    public <methods>;
+}
+-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
+    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
+}
+-keep class com.hgsoft.log.** { *; }
+# keep Xlog
+-keep class com.tencent.mars.** {*;}

+ 27 - 0
hgsoftloglibrary/src/androidTest/java/com/hgsoft/log/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.hgsoft.log;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.hgsoft.log.test", appContext.getPackageName());
+    }
+}

+ 2 - 0
hgsoftloglibrary/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.hgsoft.log"/>

+ 18 - 0
hgsoftloglibrary/src/main/java/com/hgsoft/log/LogInfo.java

@@ -0,0 +1,18 @@
+package com.hgsoft.log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * <p>用途:</p>
+ *
+ * @author yinxueqin@rd.hgits.cn
+ * 创建时间: 2021/3/5 13:56
+ * @author 更新者:
+ * 更新时间:
+ * 更新说明:
+ * @since 1.0
+ */
+public interface LogInfo {
+
+    void log(@NonNull final LogLevel priority, @NonNull final String tag, @NonNull final String message);
+}

+ 22 - 0
hgsoftloglibrary/src/main/java/com/hgsoft/log/LogLevel.java

@@ -0,0 +1,22 @@
+package com.hgsoft.log;
+
+/**
+ * <p>用途:</p>
+ *
+ * @author yinxueqin@rd.hgits.cn
+ * 创建时间: 2021/3/5 13:51
+ * @author 更新者:
+ * 更新时间:
+ * 更新说明:
+ * @since 1.0
+ */
+public enum LogLevel {
+
+    LEVEL_VERBOSE,
+    LEVEL_DEBUG,
+    LEVEL_INFO,
+    LEVEL_WARNING,
+    LEVEL_ERROR,
+    LEVEL_EXTRA;
+
+}

+ 865 - 0
hgsoftloglibrary/src/main/java/com/hgsoft/log/LogUtil.java

@@ -0,0 +1,865 @@
+package com.hgsoft.log;
+
+import android.content.Context;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import com.tencent.mars.xlog.Log;
+import net.lingala.zip4j.ZipFile;
+import net.lingala.zip4j.exception.ZipException;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * 打印日志封装
+ *
+ * @author  yinxueqin
+ */
+public final class LogUtil {
+
+    /**
+     * 可以全局控制android.util.Log是否打印log日志
+     */
+    private static boolean isPrintLog = true;
+    /**
+     * 在某些机器上小于4000,小于2000
+     */
+    private static int logMaxLength = 1800;
+    /**
+     * 长度最好不超过 23 个字符
+     */
+    public static final String TAG = "-hgits-log";
+
+    public static boolean xlogInitComplete = false;
+
+    /**
+     * 向外提供日志输出
+     */
+    private static LogInfo mLogInfo;
+
+    /**
+     * 初始化Xlog(默认方式(文件默认保存10天,不设置文件最大字节)
+     * @param context 上下文
+     * @param isDebugStatus 是否debug false:不是,true:是
+     * @param consoleLogOpen 控制台是否打印
+     * @param logFileNamePrefix 日志文件名前缀
+     * @param releasePubKey release版本时需要密钥
+     * @param needPrintSystemInfo 需要打印的系统信息
+     */
+    public static void initXlog(Context context, boolean isDebugStatus, boolean consoleLogOpen, @NonNull String logFileNamePrefix,
+                         @NonNull String releasePubKey, @NonNull String needPrintSystemInfo) {
+        LogUtil.appenderFlush(false);
+        LogUtil.appenderClose();
+        MarsXLogInit.getInstance().setXlogOpenStatus(false);
+        MarsXLogInit.getInstance().setDebugStatus(isDebugStatus);
+        MarsXLogInit.getInstance().setConsoleLogOpen(consoleLogOpen);
+        if (isDebugStatus) {
+            MarsXLogInit.getInstance().setPUBKEY("");
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Debug_" + logFileNamePrefix);
+        } else {
+            MarsXLogInit.getInstance().setPUBKEY(releasePubKey);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Release_" + logFileNamePrefix);
+        }
+        MarsXLogInit.getInstance().openXlog(context);
+        LogUtil.ei(TAG, needPrintSystemInfo);
+        LogUtil.appenderFlush(false);
+    }
+
+    /**
+     * 初始化Xlog(默认方式(文件默认保存10天,不设置文件最大字节)
+     * @param context 上下文
+     * @param isDebugStatus 是否debug false:不是,true:是
+     * @param consoleLogOpen 控制台是否打印
+     * @param logFileNamePrefix 日志文件名前缀
+     * @param releasePubKey release版本时需要密钥
+     * @param needPrintSystemInfo 需要打印的系统信息
+     * @param logFileSaveDays  文件保存天数,这个保存天数根据文件创建属性决定。最小1天,默认10天
+     * @param logFileMaxSize 单个文件最大字节 0:表示不分割 单位字节
+     */
+    public static void initXlog(Context context, boolean isDebugStatus, boolean consoleLogOpen, @NonNull String logFileNamePrefix,
+                                @NonNull String releasePubKey, @NonNull String needPrintSystemInfo, int logFileSaveDays, long logFileMaxSize) {
+        LogUtil.appenderFlush(false);
+        LogUtil.appenderClose();
+        MarsXLogInit.getInstance().setXlogOpenStatus(false);
+        MarsXLogInit.getInstance().setDebugStatus(isDebugStatus);
+        MarsXLogInit.getInstance().setConsoleLogOpen(consoleLogOpen);
+        MarsXLogInit.getInstance().setLogFileSaveDays(logFileSaveDays);
+        MarsXLogInit.getInstance().setLogFileMaxSize(logFileMaxSize);
+        if (isDebugStatus) {
+            MarsXLogInit.getInstance().setPUBKEY("");
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Debug_" + logFileNamePrefix);
+        } else {
+            MarsXLogInit.getInstance().setPUBKEY(releasePubKey);
+            MarsXLogInit.getInstance().setLogFileNamePrefix("Release_" + logFileNamePrefix);
+        }
+        MarsXLogInit.getInstance().openXlog(context);
+        LogUtil.ei(TAG, needPrintSystemInfo);
+        LogUtil.appenderFlush(false);
+    }
+
+    public static void initXlogState(boolean state) {
+        xlogInitComplete = state;
+    }
+
+    /**
+     * 设置android原生日志打印是否可以打印
+     *
+     * @param isPrint true:可以,false:不可以
+     */
+    public static void setAndroidLogPrintConsoleLog(final boolean isPrint) {
+        isPrintLog = isPrint;
+    }
+
+    /**
+     * 设置android logcat最大打印长度,超出将自动分割
+     * @param logMaxLength 一条日志最大长度
+     */
+    public static void setLogPrintMaxLength(final int logMaxLength) {
+        LogUtil.logMaxLength = logMaxLength;
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_VERBOSE
+     *
+     * @param msg 日志信息
+     */
+    public static void v(final String msg) {
+        v(TAG, msg);
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_VERBOSE
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void v(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogV(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.v(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_VERBOSE, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_DEBUG
+     *
+     * @param msg 日志信息
+     */
+    public static void d(final String msg) {
+        d(TAG, msg);
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_DEBUG
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void d(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogD(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.d(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_DEBUG, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_INFO
+     *
+     * @param msg 日志信息
+     */
+    public static void i(final String msg) {
+        i(TAG, msg);
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_INFO
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void i(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogI(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.i(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_INFO, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_INFO,有额外返回
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void ei(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogI(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.i(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_INFO, tagName + TAG, log);
+                mLogInfo.log(LogLevel.LEVEL_EXTRA, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_WARNING
+     *
+     * @param msg 日志信息
+     */
+    public static void w(final String msg) {
+        w(TAG, msg);
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_WARNING
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void w(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogW(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.w(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_WARNING, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_WARNING,有额外返回
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void ew(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogW(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.w(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_WARNING, tagName + TAG, log);
+                mLogInfo.log(LogLevel.LEVEL_EXTRA, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_ERROR
+     *
+     * @param msg 日志信息
+     */
+    public static void e(final String msg) {
+        e(TAG, msg);
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_ERROR
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void e(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogE(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.e(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_ERROR, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_ERROR,有额外返回
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void ee(final String tagName, final String msg) {
+        if (!xlogInitComplete) {
+            androidLogE(tagName, msg);
+            return;
+        }
+        List<String> logMsg = splitString(msg, logMaxLength);
+        for (String log: logMsg) {
+            Log.e(tagName + TAG, log);
+            if (mLogInfo != null) {
+                mLogInfo.log(LogLevel.LEVEL_ERROR, tagName + TAG, log);
+                mLogInfo.log(LogLevel.LEVEL_EXTRA, tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_VERBOSE
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void androidLogV(final String tagName, final String msg) {
+        if (isPrintLog) {
+            List<String> logMsg = splitString(msg, logMaxLength);
+            for (String log: logMsg) {
+                android.util.Log.v(tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_DEBUG
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void androidLogD(final String tagName, final String msg) {
+        if (isPrintLog) {
+            List<String> logMsg = splitString(msg, logMaxLength);
+            for (String log: logMsg) {
+                android.util.Log.d(tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_INFO
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void androidLogI(final String tagName, final String msg) {
+        if (isPrintLog) {
+            List<String> logMsg = splitString(msg, logMaxLength);
+            for (String log: logMsg) {
+                android.util.Log.i(tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_WARNING
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void androidLogW(final String tagName, final String msg) {
+        if (isPrintLog) {
+            List<String> logMsg = splitString(msg, logMaxLength);
+            for (String log: logMsg) {
+                android.util.Log.w(tagName + TAG, log);
+            }
+        }
+    }
+
+    /**
+     * 打印日志,级别为:LEVEL_ERROR
+     *
+     * @param tagName 日志tag
+     * @param msg 日志信息
+     */
+    public static void androidLogE(final String tagName, final String msg) {
+        if (isPrintLog) {
+            final List<String> logMsg = splitString(msg, logMaxLength);
+            for (String log: logMsg) {
+                android.util.Log.e(tagName + TAG, log);
+            }
+        }
+    }
+
+
+    /**
+     * 获取当前使用者今天的日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserLogFileZip(final Context context) throws ZipException {
+        return getCurrentUserLogFileZip(context, null);
+    }
+
+    /**
+     * 获取当前使用者今天的日志文件压缩包,耗时操作,请在子线程调用
+     * @param context  上下文对象
+     * @param sideFiles 额外文件
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserLogFileZip(final Context context, final List<File> sideFiles) throws ZipException {
+        return getCurrentUserLogFileZip(context, sideFiles, null);
+    }
+
+    /**
+     * 获取当前使用者今天的日志文件压缩包,耗时操作,请在子线程调用
+     * @param context  上下文对象
+     * @param sideFiles 额外文件
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserLogFileZip(final Context context, final List<File> sideFiles, final String customZipFileName) throws ZipException {
+        ZipFile zipFile = getLogZipFile(context, true, true,false, customZipFileName);
+        if (sideFiles != null && sideFiles.size() > 0) {
+            zipFile.addFiles(sideFiles);
+        }
+        return zipFile.getFile();
+    }
+
+
+    /**
+     * 获取当前使用者的所有日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserAllLogFileZip(final Context context) throws ZipException {
+        return getCurrentUserAllLogFileZip(context, null);
+    }
+
+    /**
+     * 获取当前使用者的所有日志文件压缩包,耗时操作,请在子线程调用
+     * @param context  上下文对象
+     * @param sideFiles 额外文件
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserAllLogFileZip(final Context context, final List<File> sideFiles) throws ZipException {
+        return getCurrentUserAllLogFileZip(context, sideFiles, null);
+    }
+
+    /**
+     * 获取当前使用者的所有日志文件压缩包,耗时操作,请在子线程调用
+     * @param context  上下文对象
+     * @param sideFiles 额外文件
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserAllLogFileZip(final Context context, final List<File> sideFiles, final String customZipFileName) throws ZipException {
+        ZipFile zipFile = getLogZipFile(context, true, false,false, customZipFileName);
+        if (sideFiles != null && sideFiles.size() > 0) {
+            zipFile.addFiles(sideFiles);
+        }
+        return zipFile.getFile();
+    }
+
+    /**
+     * 获取当前使用者昨天的日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserYesterdayLogFileZip(final Context context) throws ZipException {
+        return getCurrentUserYesterdayLogFileZip(context, null);
+    }
+
+    /**
+     * 获取当前使用者昨天的日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @param sideFiles 额外文件
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserYesterdayLogFileZip(final Context context, final List<File> sideFiles) throws ZipException {
+        return getCurrentUserYesterdayLogFileZip(context, sideFiles, null);
+    }
+
+
+    /**
+     * 获取当前使用者昨天的日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @param sideFiles 额外文件
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getCurrentUserYesterdayLogFileZip(final Context context, final List<File> sideFiles, final String customZipFileName) throws ZipException {
+        ZipFile zipFile = getLogZipFile(context, true, false,true, customZipFileName);
+        if (sideFiles != null && sideFiles.size() > 0) {
+            zipFile.addFiles(sideFiles);
+        }
+        return zipFile.getFile();
+    }
+
+    /**
+     * 获取APP所有日志文件压缩包,耗时操作,请在子线程调用
+     *
+     * @param context 上下文对象
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getAppAllLogFileZip(final Context context) throws ZipException {
+        return getAppAllLogFileZip(context, null);
+    }
+
+    /**
+     * 获取APP所有日志文件压缩包,耗时操作,请在子线程调用
+     * @param context 上下文对象
+     * @param sideFiles 额外文件
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getAppAllLogFileZip(final Context context, final List<File> sideFiles) throws ZipException {
+        return getAppAllLogFileZip(context, sideFiles, null);
+    }
+
+    /**
+     * 获取APP所有日志文件压缩包,耗时操作,请在子线程调用
+     * @param context 上下文对象
+     * @param sideFiles 额外文件
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 如果存在这个文件就返回,否则根据默认的日志目录,创建一个压缩包对象,不能确定是否存在文件
+     * @throws ZipException 压缩异常
+     */
+    public static File getAppAllLogFileZip(final Context context, final List<File> sideFiles, final String customZipFileName) throws ZipException {
+        ZipFile zipFile = getLogZipFile(context, false, false, false, customZipFileName);
+        if (sideFiles != null && sideFiles.size() > 0) {
+            zipFile.addFiles(sideFiles);
+        }
+        return zipFile.getFile();
+    }
+
+    /**
+     * 创建压缩包
+     * @param context 上下文
+     * @param isCurrent 当前用户
+     * @param isToday 是否只压缩今天
+     * @param isYesterday 是否只压缩明天
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 日志压缩文件对象
+     * @throws ZipException 压缩异常
+     */
+    private static ZipFile getLogZipFile(final Context context, final boolean isCurrent, final boolean isToday,
+                                         final boolean isYesterday, final String customZipFileName) throws ZipException {
+        File externalFileDir = context.getExternalFilesDir("XLog");
+        String logPath = null;
+        if (externalFileDir != null) {
+            logPath = externalFileDir.getAbsolutePath();
+        }
+        if (TextUtils.isEmpty(logPath)) {
+            LogUtil.i(TAG, "Log缓存目录存在");
+            logPath = context.getFilesDir().getPath() + "/XLog";
+        } else {
+            LogUtil.i(TAG, "Log目录存在");
+        }
+        File logPathFile = new File(logPath);
+        if (logPathFile.exists()) {
+            ZipFile logPathZipFile = createLogZipFile(logPath, context, isCurrent, isToday, isYesterday, customZipFileName);
+            if (logPathZipFile != null && logPathZipFile.getFile() != null) {
+                LogUtil.i(TAG, "压缩日志文件成功");
+                return logPathZipFile;
+            } else {
+                LogUtil.i(TAG, "压缩日志文件失败");
+            }
+        }
+        return new ZipFile(logPath);
+    }
+
+    /**
+     * 创建压缩包
+     * @param path 路径
+     * @param context 上下文
+     * @param isCurrent 是否当前用户
+     * @param isToday 是否只压缩今天
+     * @param isYesterday 是否只压缩明天
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 压缩文件
+     * @throws ZipException 压缩异常
+     */
+    private static ZipFile createLogZipFile(final String path, final Context context, final boolean isCurrent,
+                                            final boolean isToday, final boolean isYesterday, final String customZipFileName) throws ZipException {
+        //或者压缩文件文件名称
+        String zipFileName = getZipFileName(isCurrent, isToday, isYesterday, customZipFileName);
+        File logZipDirFile = context.getExternalFilesDir("logZip");
+        if (logZipDirFile != null && logZipDirFile.exists() && logZipDirFile.isDirectory()) {
+            String zipPath = logZipDirFile.getAbsolutePath();
+            //删除旧的压缩文件
+            deleteZipFile(logZipDirFile);
+
+            ZipFile zipFile = new ZipFile(zipPath + "/" + zipFileName);
+            if (isCurrent && isToday) {
+                getCurrentToday(zipFile, path);
+            } else if (isCurrent && isYesterday) {
+                getCurrentYesterday(zipFile, path);
+            } else if (isCurrent) {
+                getCurrentAll(zipFile, path);
+            } else {
+                getAll(zipFile, path);
+            }
+            return zipFile;
+        }
+        return null;
+    }
+
+    /**
+     * 获取当前用户今天的日志
+     * @param zipFile 压缩文件
+     * @param path 日志目录
+     */
+    private static void getCurrentToday(final ZipFile zipFile, final String path) throws ZipException {
+        List<String> startsWithName = new ArrayList<>();
+        //主日志文件
+        String logStartsWith = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + dateToDay(new Date());
+        startsWithName.add(logStartsWith);
+        //获取没有前缀的文件
+        if (MarsXLogInit.getInstance().isDebugStatus()) {
+            String logFileNameDebug = "Debug__" + dateToDay(new Date());
+            startsWithName.add(logFileNameDebug);
+        } else {
+            String logFileNameRelease = "Release__" + dateToDay(new Date());
+            startsWithName.add(logFileNameRelease);
+        }
+        List<File> logFiles = getStartsWithStringFile(startsWithName, path);
+        zipFile.addFiles(logFiles);
+    }
+
+    /**
+     * 获取当前用户昨天的日志
+     * @param zipFile 压缩文件
+     * @param path 日志目录
+     */
+    private static void getCurrentYesterday(final ZipFile zipFile, final String path) throws ZipException {
+        List<String> startsWithName = new ArrayList<>();
+        //主日志文件
+        String logStartsWith = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + getPastDate(1, "yyyyMMdd");
+        startsWithName.add(logStartsWith);
+        //获取没有前缀的文件
+        if (MarsXLogInit.getInstance().isDebugStatus()) {
+            String logFileNameDebug = "Debug__" + getPastDate(1, "yyyyMMdd");
+            startsWithName.add(logFileNameDebug);
+        } else {
+            String logFileNameRelease = "Release__" + getPastDate(1, "yyyyMMdd");
+            startsWithName.add(logFileNameRelease);
+        }
+        List<File> logFiles = getStartsWithStringFile(startsWithName, path);
+        zipFile.addFiles(logFiles);
+    }
+
+    /**
+     * 获取当前用户所有的日志
+     * @param zipFile 压缩文件
+     * @param path 日志目录
+     */
+    private static void getCurrentAll(final ZipFile zipFile, final String path) throws ZipException {
+        List<String> startsWithName = new ArrayList<>();
+        //主日志文件
+        String logStartsWith = MarsXLogInit.getInstance().getLogFileNamePrefix();
+        startsWithName.add(logStartsWith);
+        //获取没有前缀的文件
+        if (MarsXLogInit.getInstance().isDebugStatus()) {
+            String logFileNameDebug = "Debug__";
+            startsWithName.add(logFileNameDebug);
+        } else {
+            String logFileNameRelease = "Release__";
+            startsWithName.add(logFileNameRelease);
+        }
+        List<File> logFiles = getStartsWithStringFile(startsWithName, path);
+        zipFile.addFiles(logFiles);
+    }
+
+    /**
+     * 获取所有用户所有的日志
+     * @param zipFile 压缩文件
+     * @param path 日志目录
+     */
+    private static void getAll(final ZipFile zipFile, final String path) throws ZipException {
+        File logPathFile = new File(path);
+        File[] logFiles = logPathFile.listFiles();
+        if (logFiles != null) {
+            zipFile.addFiles(Arrays.asList(logFiles));
+        }
+    }
+
+    /**
+     * 获取以一些字符串开头的文件名的文件
+     * @param startsWithName 需要的文件名开头集合
+     * @param path 日志目录
+     * @return 需要的文件
+     */
+    private static List<File> getStartsWithStringFile(final List<String> startsWithName, final String path) {
+        List<File> needLogFile = new ArrayList<>();
+        File logPathFile = new File(path);
+        File[] logFiles = logPathFile.listFiles();
+        if (logFiles != null) {
+            List<File> logFileList = Arrays.asList(logFiles);
+            for (File logFile: logFileList) {
+                boolean isNeed = false;
+                for (String name: startsWithName) {
+                    if (logFile.getName().startsWith(name)) {
+                        isNeed = true;
+                    }
+                }
+                if (isNeed) {
+                    needLogFile.add(logFile);
+                }
+            }
+        }
+        return needLogFile;
+    }
+
+    /**
+     * 获取压缩包文件名称
+     * @param isCurrent 是否当前用户
+     * @param isToday 是否只压缩今天
+     * @param isYesterday 是否只压缩明天
+     * @param customZipFileName 压缩包名称,不包含后缀
+     * @return 压缩包文件名称
+     */
+    private static String getZipFileName(final boolean isCurrent,
+                                         final boolean isToday, final boolean isYesterday, final String customZipFileName) {
+        String zipFileName;
+        if (TextUtils.isEmpty(customZipFileName)) {
+            if (isCurrent && isToday) {
+                zipFileName = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + dateToDay(new Date()) + ".zip";
+            } else if (isCurrent && isYesterday) {
+                zipFileName = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + getPastDate(1, "yyyyMMdd") + ".zip";
+            } else if (isCurrent) {
+                if (MarsXLogInit.getInstance().getLogFileSaveDays() == 0) {
+                    zipFileName = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + getPastDate(10, "yyyyMMdd") + "~" + dateToDay(new Date()) + ".zip";
+                } else {
+                    zipFileName = MarsXLogInit.getInstance().getLogFileNamePrefix() + "_" + getPastDate(MarsXLogInit.getInstance().getLogFileSaveDays(), "yyyyMMdd") + "~" + dateToDay(new Date()) + ".zip";
+                }
+            } else {
+                String fileNamePrefix = MarsXLogInit.getInstance().getLogFileNamePrefix().substring(0, MarsXLogInit.getInstance().getLogFileNamePrefix().indexOf("_"));
+                if (MarsXLogInit.getInstance().getLogFileSaveDays() == 0) {
+                    zipFileName = fileNamePrefix + "_all_user_" + getPastDate(10, "yyyyMMdd") + "~" + dateToDay(new Date()) + ".zip";
+                } else {
+                    zipFileName = fileNamePrefix + "_all_user_" + getPastDate(MarsXLogInit.getInstance().getLogFileSaveDays(), "yyyyMMdd") + "~" + dateToDay(new Date()) + ".zip";
+                }
+            }
+        } else {
+            zipFileName = customZipFileName + ".zip";
+        }
+        return zipFileName;
+    }
+
+    /**
+     * 删除旧的压缩文件
+     * @param logZipDirFile 压缩文件目录
+     */
+    private static void deleteZipFile(File logZipDirFile) {
+        File[] files = logZipDirFile.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                if (file.exists() && file.getName().endsWith(".zip")) {
+                    boolean result = file.delete();
+                    LogUtil.i(TAG, "删除旧的日志打包文件结果:" + result);
+                }
+            }
+        }
+    }
+
+    private static String dateToDay(Date date) {
+        return dateToStr(date, "yyyyMMdd");
+    }
+
+    private static String dateToStr(Date datetime, String format) {
+        return new SimpleDateFormat(format, Locale.SIMPLIFIED_CHINESE).format(datetime);
+    }
+
+    /**
+     * 获取过去第几天的日期
+     *
+     * @param past 天数
+     * @param formatString 格式化
+     * @return 日期
+     */
+    private static String getPastDate(final int past, final String formatString) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.DAY_OF_YEAR, calendar.get(Calendar.DAY_OF_YEAR) - past);
+        Date today = calendar.getTime();
+        SimpleDateFormat format = new SimpleDateFormat(formatString, Locale.SIMPLIFIED_CHINESE);
+        String result = format.format(today);
+        return result;
+    }
+
+
+
+    /**
+     * 关闭日志,在程序退出时调用。
+     */
+    public static void appenderClose() {
+        MarsXLogInit.getInstance().setXlogOpenStatus(false);
+    }
+
+    /**
+     * 当日志写入模式为异步时,调用该接口会把内存中的日志写入到文件。
+     *
+     * @param isSync isSync : true 为同步 flush,flush 结束后才会返回。 false 为异步 flush,不等待 flush 结束就返回。
+     */
+    public static void appenderFlush(final boolean isSync) {
+        LogUtil.i(TAG, "appenderFlush:" + isSync);
+        Log.appenderFlushSync(isSync);
+    }
+
+    /**
+     * 设置日志回调
+     * @param logInfo 回调对象
+     */
+    public static void logPrintInfo(final LogInfo logInfo) {
+        mLogInfo = logInfo;
+    }
+
+    private static List<String> splitString(final String text, final int splitLength) {
+        List<String> temp = new ArrayList<>();
+        if (text != null && text.length() > 0) {
+            if (text.length() <= splitLength) {
+                temp.add(text);
+            } else {
+                int num = text.length() / splitLength;
+                if (num * splitLength < text.length()) {
+                    num = num + 1;
+                }
+                for (int i = 0; i < num - 1; i++) {
+                    temp.add(text.substring(i * splitLength, (i + 1) * splitLength));
+                }
+                temp.add(text.substring((num -1) * splitLength));
+            }
+        }
+        return temp;
+    }
+}

+ 199 - 0
hgsoftloglibrary/src/main/java/com/hgsoft/log/MarsXLogInit.java

@@ -0,0 +1,199 @@
+package com.hgsoft.log;
+
+import android.content.Context;
+import android.text.TextUtils;
+import com.tencent.mars.xlog.Log;
+import com.tencent.mars.xlog.Xlog;
+import java.io.File;
+
+/**
+ * 打印日志初始化设置
+ *
+ * @author  yinxueqin
+ */
+public final class MarsXLogInit {
+
+    private static volatile MarsXLogInit INSTANCE;
+    //加密公钥
+    private String PUB_KEY = "";
+    //设置日志打开状态,如果日志需要写入不同的文件需要先设置关闭。
+    private boolean xlogOpenStatus = false;
+    //设置日志文件的前缀
+    private String prefix = "log";
+    //日志是否控制台打印
+    private boolean consoleLogOpen = false;
+    // 设置是否是调试模式,调试模式会把所有日志写入文件,不是调试模式,只会写入大于INFO级别的日志
+    private boolean debugStatus = false;
+    //文件保存天数,这个保存天数根据文件创建属性决定。最小1天,默认10天
+    private int logFileSaveDays = 0;
+    //单个文件最大字节 0:表示不分割 单位字节
+    private long logFileMaxSize = 0;
+
+    static {
+        System.loadLibrary("c++_shared");
+        System.loadLibrary("marsxlog");
+    }
+
+    private MarsXLogInit(){
+    }
+
+    public static MarsXLogInit getInstance() {
+        if (INSTANCE == null) {
+            synchronized (MarsXLogInit.class) {
+                if (INSTANCE == null) {
+                    INSTANCE = new MarsXLogInit();
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    /**
+     * 设置日志打开状态,如果日志需要写入不同的文件需要先设置关闭。
+     *
+     * @param xlogOpenStatus true:已经打开,false:关闭
+     */
+    public void setXlogOpenStatus(final boolean xlogOpenStatus) {
+        this.xlogOpenStatus = xlogOpenStatus;
+        Log.appenderFlush();
+        Log.appenderClose();
+    }
+
+    /**
+     * 设置日志文件加密的公钥
+     *
+     * @param pubkey 公钥字符串
+     */
+    public void setPUBKEY(final String pubkey) {
+        PUB_KEY = pubkey;
+    }
+
+    /**
+     * 设置日志文件的前缀
+     *
+     * @param prefix 日志文件前缀
+     */
+    public void setLogFileNamePrefix(final String prefix) {
+        this.prefix = prefix;
+    }
+
+    /**
+     * 获取日志文件前缀
+     *
+     * @return 日志文件前缀
+     */
+    public String getLogFileNamePrefix() {
+        return prefix;
+    }
+
+    /**
+     * 设置控制台是否打印
+     *
+     * @param consoleLogOpen true:打印,false:不打印
+     */
+    public void setConsoleLogOpen(final boolean consoleLogOpen) {
+        this.consoleLogOpen = consoleLogOpen;
+    }
+
+    /**
+     * 是否是调试模式
+     * @return 是否是调试模式
+     */
+    public boolean isDebugStatus() {
+        return debugStatus;
+    }
+
+    /**
+     * 设置是否是调试模式,调试模式会把所有日志写入文件,不是调试模式,只会写入大于INFO级别的日志
+     *
+     * @param debugStatus true:是,false:否
+     */
+    public void setDebugStatus(final boolean debugStatus) {
+        this.debugStatus = debugStatus;
+    }
+
+    /**
+     * 设置文件保存天数
+     * @param logFileSaveDays 0:表示默认10天,其它表示其它天数,最小不能小于1
+     */
+    public void setLogFileSaveDays(final int logFileSaveDays) {
+        this.logFileSaveDays = logFileSaveDays;
+    }
+
+    /**
+     * 获取文件缓存天数
+     * @return 缓存天数
+     */
+    public int getLogFileSaveDays() {
+        return logFileSaveDays;
+    }
+
+    /**
+     * 获取单个文件最大字节,0:表示不分割
+     * @return 文件最大字节
+     */
+    public long getLogFileMaxSize() {
+        return logFileMaxSize;
+    }
+
+    /**
+     * 设置单个文件最大字节,设置后文件会分包。(log_20200710.xlog,log_20200710_1.xlog,log_20200710_2.xlog)
+     * @param logFileMaxSize 最大字节数,单位是字节
+     */
+    public void setLogFileMaxSize(long logFileMaxSize) {
+        this.logFileMaxSize = logFileMaxSize;
+    }
+
+    /**
+     * 打开日志使日志数据能够写入文件和在控制台打印(内部根据 {@link #xlogOpenStatus} 的状态来判断调用是否有效),
+     * 文件默认保存在/sdcard/android/{packageName}/XLog如果前者不可以就保存在context.getFilesDir() + "/XLog"中
+     *
+     * @param context 上下文对象
+     */
+    public void openXlog(final Context context) {
+        if (context != null) {
+            if (!xlogOpenStatus) {
+                LogUtil.initXlogState(false);
+                File externalFileDir = context.getExternalFilesDir("XLog");
+                String logPath = null;
+                if (externalFileDir != null) {
+                    logPath = externalFileDir.getAbsolutePath();
+                } else {
+                    android.util.Log.i("LOG", "创建文件夹失败,可能需要重启设备。");
+                }
+                final String cachePath = context.getFilesDir() + "/XLog";
+                android.util.Log.i("LOG", "logPath:" + logPath);
+                //init xlog
+                if (debugStatus) {
+                    Xlog.open(false, Xlog.LEVEL_ALL, Xlog.AppednerModeAsync, cachePath, logPath, prefix, PUB_KEY);
+                } else {
+                    Xlog.open(false,  Xlog.LEVEL_INFO, Xlog.AppednerModeAsync, cachePath, logPath, prefix, PUB_KEY);
+                }
+                //下面需要放到appenderOpen方法后面
+                Xlog xlog = new Xlog();
+                xlog.setConsoleLogOpen(0, consoleLogOpen);
+                if (logFileSaveDays != 0) {
+                    xlog.setMaxAliveTime(0,logFileSaveDays * 24 * 60 * 60);
+                }
+                if (logFileMaxSize != 0) {
+                    xlog.setMaxFileSize(0, logFileMaxSize);
+                }
+                Log.setLogImp(xlog);
+                Log.i("SystemInfo", Log.getSysInfo());
+                LogUtil.initXlogState(true);
+                File logPathDir;
+                if (TextUtils.isEmpty(logPath)) {
+                    logPathDir = new File(cachePath);
+                } else {
+                    logPathDir = new File(logPath);
+                }
+                if (!logPathDir.exists()) {
+                    xlogOpenStatus = false;
+                } else {
+                    xlogOpenStatus = true;
+                    LogUtil.i("初始化日志成功");
+                }
+            }
+        }
+    }
+}

+ 17 - 0
hgsoftloglibrary/src/test/java/com/hgsoft/log/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.hgsoft.log;
+
+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);
+    }
+}

+ 4 - 0
settings.gradle

@@ -0,0 +1,4 @@
+rootProject.name='loglibrary'
+include ':app'
+include ':hgsoftloglibrary'
+include ':debuglogshow'