├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── bosphere
│ │ │ │ └── fileloggerdemo
│ │ │ │ ├── App.java
│ │ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── bosphere
│ │ │ └── fileloggerdemo
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── bosphere
│ │ └── fileloggerdemo
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── filelogger
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── bosphere
│ │ └── filelogger
│ │ ├── FileFormatter.java
│ │ ├── Loggable.java
│ │ ├── FLConst.java
│ │ ├── FLUtil.java
│ │ ├── FL.java
│ │ ├── FLConfig.java
│ │ └── FileLoggerService.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── CHANGELOG.md
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/filelogger/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':filelogger'
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FileLogger Demo
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/filelogger/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bosphere/Android-FileLogger/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 03 23:34:43 SGT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | **/local.properties
3 | **.DS_Store
4 | .idea
5 | *.class
6 | # Since moved to gradle therefore ignore all *.iml files
7 | *.iml
8 | # Emacs cache
9 | *.*~
10 | build
11 | **/nbproject/
12 | misc/scripts/*.json
13 | misc/scripts/*.xml
14 | captures
15 | projectFilesBackup
16 | .externalNativeBuild/
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FileFormatter.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | /**
4 | * Created by yangbo on 22/9/17.
5 | */
6 |
7 | public interface FileFormatter {
8 | String formatLine(long timeInMillis, String level, String tag, String log);
9 | String formatFileName(long timeInMillis);
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/Loggable.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | /**
4 | * Created by yangbo on 22/9/17.
5 | */
6 |
7 | public interface Loggable {
8 |
9 | void v(String tag, String log);
10 | void d(String tag, String log);
11 | void i(String tag, String log);
12 | void w(String tag, String log);
13 | void e(String tag, String log);
14 | void e(String tag, String log, Throwable tr);
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/test/java/com/bosphere/fileloggerdemo/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.fileloggerdemo;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | android.enableJetifier=true
13 | android.useAndroidX=true
14 | org.gradle.jvmargs=-Xmx1536m
15 |
16 | # When configured, Gradle will run in incubating parallel mode.
17 | # This option should only be used with decoupled projects. More details, visit
18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
19 | # org.gradle.parallel=true
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change Log
2 | ============================
3 |
4 | ## Version 1.0.7
5 | _2018-10-03_
6 | + Remove redundant manifest declaration
7 | + Migrate to AndroidX
8 |
9 | ## Version 1.0.6
10 | _2018-07-14_
11 | + Flush buffer to file immediately when logging at error-level
12 |
13 | ## Version 1.0.5
14 | _2018-05-05_
15 | + Support customize minimum logging level
16 |
17 | ## Version 1.0.4
18 | _2018-03-09_
19 | + Fix file logging might lead to crashes when file system is unavailable
20 | + Use Android's Log utility to format throwable
21 |
22 | ## Version 1.0.3
23 | _2017-12-23_
24 | + Comply with background execution limit imposed in Android Oreo
25 |
26 | ## Version 1.0.2
27 | _2017-11-20_
28 | + Delimit custom message and stack trace with a new line when using `FL.e(String tag, Throwable tr, String fmt, Object... args)`
29 |
30 | ## Version 1.0.1
31 | _2017-09-24_
32 | + Initial release
--------------------------------------------------------------------------------
/app/src/main/java/com/bosphere/fileloggerdemo/App.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.fileloggerdemo;
2 |
3 | import android.app.Application;
4 | import android.os.Environment;
5 |
6 | import com.bosphere.filelogger.FL;
7 | import com.bosphere.filelogger.FLConfig;
8 | import com.bosphere.filelogger.FLConst;
9 |
10 | import java.io.File;
11 |
12 | /**
13 | * Created by yangbo on 22/9/17.
14 | */
15 |
16 | public class App extends Application {
17 |
18 | @Override
19 | public void onCreate() {
20 | super.onCreate();
21 | FL.init(new FLConfig.Builder(this)
22 | .minLevel(FLConst.Level.V)
23 | .logToFile(true)
24 | .dir(new File(Environment.getExternalStorageDirectory(), "file_logger_demo"))
25 | .retentionPolicy(FLConst.RetentionPolicy.FILE_COUNT)
26 | .build());
27 | FL.setEnabled(true);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/bosphere/fileloggerdemo/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.fileloggerdemo;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.bosphere.filelogger", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FLConst.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | import android.util.SparseArray;
4 |
5 | /**
6 | * Created by bo on 23/9/17.
7 | */
8 |
9 | public interface FLConst {
10 |
11 | String TAG = "FileLogger";
12 |
13 | interface Level {
14 | int V = 0;
15 | int D = 1;
16 | int I = 2;
17 | int W = 3;
18 | int E = 4;
19 | }
20 |
21 | SparseArray LevelName = new SparseArray(5) {{
22 | append(Level.V, "V");
23 | append(Level.D, "D");
24 | append(Level.I, "I");
25 | append(Level.W, "W");
26 | append(Level.E, "E");
27 | }};
28 |
29 | interface RetentionPolicy {
30 | int NONE = 0;
31 | int FILE_COUNT = 1;
32 | int TOTAL_SIZE = 2;
33 | }
34 |
35 | long DEFAULT_MAX_TOTAL_SIZE = 32 * 1024 * 1024; // 32mb
36 | int DEFAULT_MAX_FILE_COUNT = 24 * 7; // ~7 days of restless logging
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Volumes/Data/Users/bo/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/filelogger/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Volumes/Data/Users/bo/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | buildToolsVersion "28.0.3"
6 | defaultConfig {
7 | applicationId "com.bosphere.filelogger"
8 | minSdkVersion 14
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation fileTree(dir: 'libs', include: ['*.jar'])
24 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0-alpha4', {
25 | exclude group: 'com.android.support', module: 'support-annotations'
26 | })
27 | implementation 'androidx.appcompat:appcompat:1.0.0'
28 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
29 | testImplementation 'junit:junit:4.12'
30 |
31 | implementation project(':filelogger')
32 | // implementation('com.github.bosphere.android-filelogger:filelogger:1.0.1')
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/bosphere/fileloggerdemo/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.fileloggerdemo;
2 |
3 | import android.Manifest;
4 | import android.content.pm.PackageManager;
5 | import android.os.Bundle;
6 | import androidx.annotation.NonNull;
7 | import androidx.core.app.ActivityCompat;
8 | import androidx.core.content.ContextCompat;
9 | import androidx.appcompat.app.AppCompatActivity;
10 | import android.view.View;
11 |
12 | import com.bosphere.filelogger.FL;
13 |
14 | public class MainActivity extends AppCompatActivity {
15 |
16 | private static final int REQ_PERMISSION = 1233;
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 | setContentView(R.layout.activity_main);
22 |
23 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
24 | PackageManager.PERMISSION_GRANTED) {
25 | ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQ_PERMISSION);
26 | }
27 | }
28 |
29 | @Override
30 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
31 | @NonNull int[] grantResults) {
32 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
33 | if (requestCode == REQ_PERMISSION) {
34 | if (grantResults.length > 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
35 | finish();
36 | }
37 | }
38 | }
39 |
40 | public void onClickLog(View view) {
41 | FL.v("this is a log");
42 | FL.d("this is a log");
43 | FL.i("this is a log");
44 | FL.w("this is a log");
45 | FL.e("this is a log");
46 | FL.e("this is a log with exception", new RuntimeException("dummy exception"));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FLUtil.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.os.Looper;
6 | import android.text.TextUtils;
7 |
8 | import java.io.File;
9 | import java.util.Locale;
10 |
11 | /**
12 | * Created by yangbo on 22/9/17.
13 | */
14 |
15 | class FLUtil {
16 |
17 | static String format(String fmt, Object... args) {
18 | if (args == null || args.length == 0) {
19 | return fmt;
20 | }
21 |
22 | return String.format(Locale.ENGLISH, fmt, args);
23 | }
24 |
25 | static String getAppName(Context context) {
26 | ApplicationInfo info = context.getApplicationInfo();
27 | int stringRes = info.labelRes;
28 | if (stringRes > 0) {
29 | return context.getString(stringRes);
30 | } else if (!TextUtils.isEmpty(info.nonLocalizedLabel)) {
31 | return info.nonLocalizedLabel.toString();
32 | } else {
33 | return "App";
34 | }
35 | }
36 |
37 | static void ensureUiThread() {
38 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
39 | throw new IllegalStateException("UI thread only");
40 | }
41 | }
42 |
43 | static boolean ensureDir(File dir) {
44 | if (dir.exists()) {
45 | if (dir.isDirectory()) {
46 | return true;
47 | }
48 |
49 | if (!dir.delete()) {
50 | FL.w("failed to delete file that occupies log dir path: [" +
51 | dir.getAbsolutePath() + "]");
52 | return false;
53 | }
54 | }
55 |
56 | if (!dir.mkdir()) {
57 | FL.w("failed to create log dir: [" + dir.getAbsolutePath() + "]");
58 | return false;
59 | }
60 |
61 | return true;
62 | }
63 |
64 | static void ensureFile(File file) {
65 | if (file.exists() && !file.isFile() && !file.delete()) {
66 | throw new IllegalStateException(
67 | "failed to delete directory that occupies log file path: [" +
68 | file.getAbsolutePath() + "]");
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Android-FileLogger
3 | ============================
4 |
5 | A general-purpose logging library with built-in support to save logs to file.
6 |
7 | For performance concern, I/O operation is buffered and executed in background thread.
8 |
9 |
10 | Usage
11 | -----
12 | ```gradle
13 | dependencies {
14 | compile 'com.github.bosphere.android-filelogger:filelogger:1.0.7'
15 | }
16 | ```
17 |
18 | Please refer to the sample app for more details.
19 |
20 | ```java
21 | FL.init(new FLConfig.Builder(this)
22 | .logger(...) // customise how to hook up with logcat
23 | .defaultTag("Default Tag") // customise default tag
24 | .minLevel(FLConst.Level.V) // customise minimum logging level
25 | .logToFile(true) // enable logging to file
26 | .dir(directory) // customise directory to hold log files
27 | .formatter(...) // customise log format and file name
28 | .retentionPolicy(FLConst.RetentionPolicy.FILE_COUNT) // customise retention strategy
29 | .maxFileCount(FLConst.DEFAULT_MAX_FILE_COUNT) // customise how many log files to keep if retention by file count
30 | .maxTotalSize(FLConst.DEFAULT_MAX_TOTAL_SIZE) // customise how much space log files can occupy if retention by total size
31 | .build());
32 |
33 |
34 | // overall toggle to enable/disable logging
35 | FL.setEnabled(true);
36 |
37 | // to log with default tag
38 | FL.d("this is a debug message");
39 |
40 | // to log with placeholder and interpolation
41 | FL.d("this is a %s message", "debug");
42 |
43 | // to log with specified tag
44 | FL.d("Tag", "this is a %s message", "debug");
45 |
46 | // to log exception
47 | FL.e("Tag", throwable);
48 | FL.e("Tag", throwable, "extra %s info", "debug");
49 |
50 | ```
51 |
52 | Compatibility
53 | -------------
54 |
55 | API 9 (Android 2.3) and up
56 |
57 | License
58 | -------
59 |
60 | Copyright 2018 Yang Bo
61 |
62 | Licensed under the Apache License, Version 2.0 (the "License");
63 | you may not use this file except in compliance with the License.
64 | You may obtain a copy of the License at
65 |
66 | http://www.apache.org/licenses/LICENSE-2.0
67 |
68 | Unless required by applicable law or agreed to in writing, software
69 | distributed under the License is distributed on an "AS IS" BASIS,
70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
71 | See the License for the specific language governing permissions and
72 | limitations under the License.
73 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FL.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | import android.content.Context;
4 | import android.text.TextUtils;
5 | import android.util.Log;
6 |
7 | import static com.bosphere.filelogger.FLConst.Level.D;
8 | import static com.bosphere.filelogger.FLConst.Level.E;
9 | import static com.bosphere.filelogger.FLConst.Level.I;
10 | import static com.bosphere.filelogger.FLConst.Level.V;
11 | import static com.bosphere.filelogger.FLConst.Level.W;
12 |
13 | /**
14 | * Created by yangbo on 22/9/17.
15 | */
16 |
17 | public class FL {
18 |
19 | private volatile static boolean sEnabled;
20 | private volatile static FLConfig sConfig;
21 |
22 | public static void setEnabled(boolean enabled) {
23 | sEnabled = enabled;
24 | }
25 |
26 | public static void init(Context context) {
27 | init(new FLConfig.Builder(context).build());
28 | }
29 |
30 | public static void init(FLConfig config) {
31 | sConfig = config;
32 | }
33 |
34 | public static void v(String fmt, Object... args) {
35 | v(null, fmt, args);
36 | }
37 |
38 | public static void v(String tag, String fmt, Object... args) {
39 | log(V, tag, FLUtil.format(fmt, args));
40 | }
41 |
42 | public static void d(String fmt, Object... args) {
43 | d(null, fmt, args);
44 | }
45 |
46 | public static void d(String tag, String fmt, Object... args) {
47 | log(D, tag, FLUtil.format(fmt, args));
48 | }
49 |
50 | public static void i(String fmt, Object... args) {
51 | log(I, null, FLUtil.format(fmt, args));
52 | }
53 |
54 | public static void i(String tag, String fmt, Object... args) {
55 | log(I, tag, FLUtil.format(fmt, args));
56 | }
57 |
58 | public static void w(String fmt, Object... args) {
59 | w(null, fmt, args);
60 | }
61 |
62 | public static void w(String tag, String fmt, Object... args) {
63 | log(W, tag, FLUtil.format(fmt, args));
64 | }
65 |
66 | public static void e(String fmt, Object... args) {
67 | e((String) null, fmt, args);
68 | }
69 |
70 | public static void e(String tag, String fmt, Object... args) {
71 | log(E, tag, FLUtil.format(fmt, args));
72 | }
73 |
74 | public static void e(Throwable tr) {
75 | e(null, tr);
76 | }
77 |
78 | public static void e(String tag, Throwable tr) {
79 | e(tag, tr, null);
80 | }
81 |
82 | public static void e(Throwable tr, String fmt, Object... args) {
83 | e(null, tr, fmt, args);
84 | }
85 |
86 | public static void e(String tag, Throwable tr, String fmt, Object... args) {
87 | StringBuilder sb = new StringBuilder();
88 | if (!TextUtils.isEmpty(fmt)) {
89 | sb.append(FLUtil.format(fmt, args));
90 | sb.append("\n");
91 | }
92 | if (tr != null) {
93 | sb.append(Log.getStackTraceString(tr));
94 | }
95 | log(E, tag, sb.toString());
96 | }
97 |
98 | private static void log(int level, String tag, String log) {
99 | if (!sEnabled) {
100 | return;
101 | }
102 |
103 | ensureStatus();
104 |
105 | FLConfig config = sConfig;
106 | if (level < config.b.minLevel) {
107 | return;
108 | }
109 |
110 | if (TextUtils.isEmpty(tag)) {
111 | tag = config.b.defaultTag;
112 | }
113 |
114 | Loggable logger = config.b.logger;
115 | if (logger != null) {
116 | switch (level) {
117 | case V:
118 | logger.v(tag, log);
119 | break;
120 | case D:
121 | logger.d(tag, log);
122 | break;
123 | case I:
124 | logger.i(tag, log);
125 | break;
126 | case W:
127 | logger.w(tag, log);
128 | break;
129 | case E:
130 | logger.e(tag, log);
131 | break;
132 | }
133 | }
134 |
135 | if (config.b.logToFile && !TextUtils.isEmpty(config.b.dirPath)) {
136 | long timeMs = System.currentTimeMillis();
137 | String fileName = config.b.formatter.formatFileName(timeMs);
138 | String line = config.b.formatter.formatLine(timeMs, FLConst.LevelName.get(level), tag, log);
139 | boolean flush = level == E;
140 | FileLoggerService.instance().logFile(config.b.context, fileName, config.b.dirPath, line,
141 | config.b.retentionPolicy, config.b.maxFileCount, config.b.maxSize, flush);
142 | }
143 | }
144 |
145 | private static void ensureStatus() {
146 | if (sConfig == null) {
147 | throw new IllegalStateException(
148 | "FileLogger is not initialized. Forgot to call FL.init()?");
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/filelogger/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | /*
4 | define in local.properties these variables:
5 | bintray.user=[BINTRAY_USERNAME]
6 | bintray.apikey=[BINTRAY_API_KEY]
7 | bintray.gpg.password=[DEPENDS_WHETHER_GPG_AUTO_SIGNING_KEY_HAS_A_PASSPHRASE]
8 | developer.id=[USERNAME]
9 | developer.name=[FULL_NAME]
10 | developer.email=[EMAIL]
11 | */
12 |
13 | ext {
14 | LIB_VERSION = '1.0.7'
15 |
16 | bintrayRepo = 'maven'
17 | bintrayName = 'android-file-logger'
18 |
19 | publishedGroupId = 'com.github.bosphere.android-filelogger'
20 | libraryName = 'Android-FileLogger'
21 | artifact = 'filelogger'
22 |
23 | libraryDescription = 'A general-purpose logging library with built-in support to save logs to file.'
24 |
25 | siteUrl = 'https://github.com/bosphere/Android-FileLogger'
26 | gitUrl = 'https://github.com/bosphere/Android-FileLogger.git'
27 |
28 | libraryVersion = "$LIB_VERSION"
29 |
30 | Properties properties = new Properties()
31 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
32 | developerId = properties.getProperty('developer.id')
33 | developerName = properties.getProperty('developer.name')
34 | developerEmail = properties.getProperty('developer.email')
35 |
36 | licenseName = 'The Apache Software License, Version 2.0'
37 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
38 | allLicenses = ["Apache-2.0"]
39 | }
40 |
41 | android {
42 | compileSdkVersion 28
43 | buildToolsVersion "28.0.3"
44 |
45 | defaultConfig {
46 | minSdkVersion 9
47 | }
48 | buildTypes {
49 | release {
50 | minifyEnabled false
51 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
52 | }
53 | }
54 | lintOptions {
55 | abortOnError false
56 | }
57 | }
58 |
59 | dependencies {
60 | }
61 |
62 | // ./gradlew clean install bintrayUpload
63 |
64 | ///////////////////////////////////////////////////////////////////////////////////////////
65 |
66 | apply plugin: 'com.github.dcendents.android-maven'
67 |
68 | group = publishedGroupId // Maven Group ID for the artifact
69 |
70 | install {
71 | repositories.mavenInstaller {
72 | // This generates POM.xml with proper parameters
73 | pom {
74 | project {
75 | packaging 'aar'
76 | groupId publishedGroupId
77 | artifactId artifact
78 |
79 | // Add your description here
80 | name libraryName
81 | description libraryDescription
82 | url siteUrl
83 |
84 | // Set your license
85 | licenses {
86 | license {
87 | name licenseName
88 | url licenseUrl
89 | }
90 | }
91 | developers {
92 | developer {
93 | id developerId
94 | name developerName
95 | email developerEmail
96 | }
97 | }
98 | scm {
99 | connection gitUrl
100 | developerConnection gitUrl
101 | url siteUrl
102 |
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
109 | ///////////////////////////////////////////////////////////////////////////////////////////
110 |
111 | apply plugin: 'com.jfrog.bintray'
112 |
113 | version = libraryVersion
114 |
115 | task sourcesJar(type: Jar) {
116 | from android.sourceSets.main.java.srcDirs
117 | classifier = 'sources'
118 | }
119 |
120 | task javadoc(type: Javadoc) {
121 | source = android.sourceSets.main.java.srcDirs
122 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
123 | }
124 |
125 | task javadocJar(type: Jar, dependsOn: javadoc) {
126 | classifier = 'javadoc'
127 | from javadoc.destinationDir
128 | }
129 | artifacts {
130 | archives javadocJar
131 | archives sourcesJar
132 | }
133 |
134 | // Bintray
135 | Properties properties = new Properties()
136 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
137 |
138 | bintray {
139 | user = properties.getProperty("bintray.user")
140 | key = properties.getProperty("bintray.apikey")
141 |
142 | configurations = ['archives']
143 | pkg {
144 | repo = bintrayRepo
145 | name = bintrayName
146 | desc = libraryDescription
147 | websiteUrl = siteUrl
148 | vcsUrl = gitUrl
149 | licenses = allLicenses
150 | publish = true
151 | publicDownloadNumbers = true
152 | version {
153 | desc = libraryDescription
154 | gpg {
155 | sign = true //Determines whether to GPG sign the files. The default is false
156 | passphrase = properties.getProperty("bintray.gpg.password")
157 | //Optional. The passphrase for GPG signing'
158 | }
159 | }
160 | }
161 | }
162 |
163 | ///////////////////////////////////////////////////////////////////////////////////////////
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FLConfig.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | import android.content.Context;
4 | import android.os.Process;
5 | import android.text.TextUtils;
6 | import android.util.Log;
7 |
8 | import java.io.File;
9 | import java.text.SimpleDateFormat;
10 | import java.util.Date;
11 | import java.util.Locale;
12 |
13 | import static com.bosphere.filelogger.FLConst.RetentionPolicy.FILE_COUNT;
14 | import static com.bosphere.filelogger.FLConst.RetentionPolicy.TOTAL_SIZE;
15 |
16 | /**
17 | * Created by yangbo on 22/9/17.
18 | */
19 |
20 | public class FLConfig {
21 |
22 | final Builder b;
23 |
24 | private FLConfig(Builder b) {
25 | this.b = b;
26 | }
27 |
28 | public static class Builder {
29 |
30 | final Context context;
31 | Loggable logger = new DefaultLog();
32 | FileFormatter formatter;
33 | String dirPath;
34 | String defaultTag;
35 | int minLevel = FLConst.Level.V;
36 | boolean logToFile;
37 | int retentionPolicy = FILE_COUNT;
38 | int maxFileCount = FLConst.DEFAULT_MAX_FILE_COUNT;
39 | long maxSize = FLConst.DEFAULT_MAX_TOTAL_SIZE;
40 |
41 | public Builder(Context context) {
42 | this.context = context.getApplicationContext();
43 | }
44 |
45 | /**
46 | * Defines how to output to logcat. {@link DefaultLog} is used by default. Pass {@code NULL} to disable output to logcat.
47 | *
48 | * @param logger
49 | * @return
50 | */
51 | public Builder logger(Loggable logger) {
52 | this.logger = logger;
53 | return this;
54 | }
55 |
56 | /**
57 | * Defines how each log looks in file, as well as how log files are named.
58 | *
59 | * @param formatter
60 | * @return
61 | */
62 | public Builder formatter(FileFormatter formatter) {
63 | this.formatter = formatter;
64 | return this;
65 | }
66 |
67 | /**
68 | * Defines the default log file directory.
69 | *
70 | * @param dir
71 | * @return
72 | */
73 | public Builder dir(File dir) {
74 | if (dir != null) {
75 | dirPath = dir.getAbsolutePath();
76 | }
77 | return this;
78 | }
79 |
80 | /**
81 | * Defines the default tag to use.
82 | *
83 | * @param tag
84 | * @return
85 | */
86 | public Builder defaultTag(String tag) {
87 | this.defaultTag = tag;
88 | return this;
89 | }
90 |
91 | /**
92 | * Defines the minimum logging level. Default is {@link com.bosphere.filelogger.FLConst.Level#V}.
93 | *
94 | * @param level
95 | * @return
96 | */
97 | public Builder minLevel(int level) {
98 | this.minLevel = level;
99 | return this;
100 | }
101 |
102 | /**
103 | * Defines whether to enable logging to files.
104 | *
105 | * @param logToFile
106 | * @return
107 | */
108 | public Builder logToFile(boolean logToFile) {
109 | this.logToFile = logToFile;
110 | return this;
111 | }
112 |
113 | /**
114 | * Defines how log files are managed when exceeding limit. Currently supports limit by file count or total size.
115 | *
116 | * @param retentionPolicy For possible values refer to {@link com.bosphere.filelogger.FLConst.RetentionPolicy}
117 | * @return
118 | */
119 | public Builder retentionPolicy(int retentionPolicy) {
120 | this.retentionPolicy = retentionPolicy;
121 | return this;
122 | }
123 |
124 | /**
125 | * Defines at maximum how many log files are allowed to be retained.
126 | *
127 | * @param maxFileCount
128 | * @return
129 | */
130 | public Builder maxFileCount(int maxFileCount) {
131 | this.maxFileCount = maxFileCount;
132 | return this;
133 | }
134 |
135 | /**
136 | * Defines at maximum how much space log files can occupy before trimming.
137 | *
138 | * @param maxSize
139 | * @return
140 | */
141 | public Builder maxTotalSize(long maxSize) {
142 | this.maxSize = maxSize;
143 | return this;
144 | }
145 |
146 | public FLConfig build() {
147 | if (TextUtils.isEmpty(defaultTag)) {
148 | defaultTag = FLUtil.getAppName(context);
149 | }
150 | if (logToFile) {
151 | if (formatter == null) {
152 | formatter = new DefaultFormatter();
153 | }
154 |
155 | if (TextUtils.isEmpty(dirPath)) {
156 | File dir = context.getExternalFilesDir("log");
157 | if (dir != null) {
158 | dirPath = dir.getAbsolutePath();
159 | } else {
160 | Log.e(FLConst.TAG, "failed to resolve default log file directory");
161 | }
162 | }
163 |
164 | if (retentionPolicy < 0) {
165 | throw new IllegalArgumentException("invalid retention policy: " + retentionPolicy);
166 | }
167 |
168 | switch (retentionPolicy) {
169 | case FILE_COUNT:
170 | if (maxFileCount <= 0) {
171 | throw new IllegalArgumentException("max file count must be > 0");
172 | }
173 | break;
174 | case TOTAL_SIZE:
175 | if (maxSize <= 0) {
176 | throw new IllegalArgumentException("max total size must be > 0");
177 | }
178 | break;
179 | }
180 | }
181 | return new FLConfig(this);
182 | }
183 | }
184 |
185 | public static class DefaultLog implements Loggable {
186 |
187 | @Override
188 | public void v(String tag, String log) {
189 | Log.v(tag, log);
190 | }
191 |
192 | @Override
193 | public void d(String tag, String log) {
194 | Log.d(tag, log);
195 | }
196 |
197 | @Override
198 | public void i(String tag, String log) {
199 | Log.i(tag, log);
200 | }
201 |
202 | @Override
203 | public void w(String tag, String log) {
204 | Log.w(tag, log);
205 | }
206 |
207 | @Override
208 | public void e(String tag, String log) {
209 | Log.e(tag, log);
210 | }
211 |
212 | @Override
213 | public void e(String tag, String log, Throwable tr) {
214 | Log.e(tag, log, tr);
215 | }
216 | }
217 |
218 | public static class DefaultFormatter implements FileFormatter {
219 |
220 | private final ThreadLocal mTimeFmt = new ThreadLocal() {
221 | @Override
222 | protected SimpleDateFormat initialValue() {
223 | return new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.ENGLISH);
224 | }
225 | };
226 |
227 | private final ThreadLocal mFileNameFmt = new ThreadLocal() {
228 | @Override
229 | protected SimpleDateFormat initialValue() {
230 | return new SimpleDateFormat("MM_dd_HH", Locale.ENGLISH);
231 | }
232 | };
233 |
234 | private final ThreadLocal mDate = new ThreadLocal() {
235 | @Override
236 | protected Date initialValue() {
237 | return new Date();
238 | }
239 | };
240 |
241 | // 09-23 12:31:53.839 PROCESS_ID-THREAD_ID LEVEL/TAG: LOG
242 | private final String mLineFmt = "%s %d-%d %s/%s: %s";
243 |
244 | @Override
245 | public String formatLine(long timeInMillis, String level, String tag, String log) {
246 | mDate.get().setTime(timeInMillis);
247 | String timestamp = mTimeFmt.get().format(mDate.get());
248 | int processId = Process.myPid();
249 | int threadId = Process.myTid();
250 | return String.format(Locale.ENGLISH, mLineFmt, timestamp, processId, threadId, level,
251 | tag, log);
252 | }
253 |
254 | @Override
255 | public String formatFileName(long timeInMillis) {
256 | mDate.get().setTime(timeInMillis);
257 | return mFileNameFmt.get().format(mDate.get()) + "_00.txt";
258 | }
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/filelogger/src/main/java/com/bosphere/filelogger/FileLoggerService.java:
--------------------------------------------------------------------------------
1 | package com.bosphere.filelogger;
2 |
3 | import android.content.Context;
4 | import android.text.TextUtils;
5 |
6 | import java.io.BufferedWriter;
7 | import java.io.File;
8 | import java.io.FileWriter;
9 | import java.io.IOException;
10 | import java.util.Arrays;
11 | import java.util.Comparator;
12 | import java.util.concurrent.BlockingQueue;
13 | import java.util.concurrent.LinkedBlockingDeque;
14 | import java.util.concurrent.TimeUnit;
15 |
16 | /**
17 | * Created by bo on 23/9/17.
18 | */
19 |
20 | class FileLoggerService {
21 |
22 | private static final Comparator FILE_COMPARATOR = new Comparator() {
23 | @Override
24 | public int compare(File o1, File o2) {
25 | long lm1 = o1.lastModified();
26 | long lm2 = o2.lastModified();
27 | return lm1 < lm2 ? -1 : lm1 == lm2 ? 0 : 1;
28 | }
29 | };
30 |
31 | static FileLoggerService instance() {
32 | return InstanceHolder.INSTANCE;
33 | }
34 |
35 | static class InstanceHolder {
36 | static final FileLoggerService INSTANCE = new FileLoggerService();
37 | }
38 |
39 | private final BlockingQueue mQueue;
40 | private volatile boolean mIsRunning;
41 |
42 | FileLoggerService() {
43 | mQueue = new LinkedBlockingDeque<>();
44 | }
45 |
46 | void logFile(Context context, String fileName, String dirPath, String line,
47 | int retentionPolicy, int maxFileCount, long maxTotalSize, boolean flush) {
48 | ensureThread();
49 | boolean addResult = mQueue.offer(new LogData.Builder().context(context)
50 | .fileName(fileName)
51 | .dirPath(dirPath)
52 | .line(line)
53 | .retentionPolicy(retentionPolicy)
54 | .maxFileCount(maxFileCount)
55 | .maxSize(maxTotalSize)
56 | .flush(flush)
57 | .build());
58 | if (!addResult) {
59 | FL.w("failed to add to file logger service queue");
60 | }
61 | }
62 |
63 | private void ensureThread() {
64 | if (!mIsRunning) {
65 | synchronized (this) {
66 | if (!mIsRunning) {
67 | mIsRunning = true;
68 | FL.d("start file logger service thread");
69 | new LogFileThread().start();
70 | }
71 | }
72 | }
73 | }
74 |
75 | private class LogFileThread extends Thread {
76 |
77 | private BufferedWriter mWriter;
78 | private String mPath;
79 | private int mRetentionPolicy;
80 | private int mMaxFileCount;
81 | private long mMaxSize;
82 |
83 | @Override
84 | public void run() {
85 | super.run();
86 | Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
87 | @Override
88 | public void uncaughtException(Thread thread, Throwable throwable) {
89 | throwable.printStackTrace();
90 | mIsRunning = false;
91 | }
92 | });
93 |
94 | try {
95 | for (;;) {
96 | LogData log = mQueue.take();
97 | logLine(log);
98 | collectParams(log);
99 | while ((log = mQueue.poll(2, TimeUnit.SECONDS)) != null) {
100 | logLine(log);
101 | collectParams(log);
102 | }
103 |
104 | closeWriter();
105 | startHouseKeeping();
106 | }
107 | } catch (InterruptedException e) {
108 | FL.e(e, "file logger service thread is interrupted");
109 | }
110 |
111 | FL.d("file logger service thread stopped");
112 | mIsRunning = false;
113 | }
114 |
115 | private void collectParams(LogData log) {
116 | mRetentionPolicy = log.retentionPolicy;
117 | mMaxFileCount = log.maxFileCount;
118 | mMaxSize = log.maxTotalSize;
119 | }
120 |
121 | private void logLine(LogData log) {
122 | if (TextUtils.isEmpty(log.fileName)) {
123 | throw new IllegalStateException("invalid file name: [" + log.fileName + "]");
124 | }
125 |
126 | if (TextUtils.isEmpty(log.dirPath)) {
127 | throw new IllegalStateException("invalid directory path: [" + log.dirPath + "]");
128 | }
129 |
130 | if (TextUtils.isEmpty(log.line)) {
131 | return;
132 | }
133 |
134 | File dir = new File(log.dirPath);
135 | if (!FLUtil.ensureDir(dir)) {
136 | return;
137 | }
138 |
139 | File f = new File(log.dirPath, log.fileName);
140 | String path = f.getAbsolutePath();
141 | if (mWriter != null && path.equals(mPath)) {
142 | try {
143 | mWriter.write(log.line);
144 | mWriter.write("\n");
145 | if (log.flush) {
146 | mWriter.flush();
147 | }
148 | } catch (IOException e) {
149 | FL.e(FLConst.TAG, e);
150 | }
151 | } else {
152 | closeWriter();
153 | FLUtil.ensureFile(f);
154 | try {
155 | mWriter = createWriter(f);
156 | mPath = f.getAbsolutePath();
157 |
158 | mWriter.write(log.line);
159 | mWriter.write("\n");
160 | if (log.flush) {
161 | mWriter.flush();
162 | }
163 | } catch (IOException e) {
164 | FL.e(FLConst.TAG, e);
165 | }
166 | }
167 | }
168 |
169 | private BufferedWriter createWriter(File file) throws IOException {
170 | // one line ~100 characters = ~100-400 bytes
171 | // use default buf size ~8k = ~20-80 lines
172 | return new BufferedWriter(new FileWriter(file, true));
173 | }
174 |
175 | private void startHouseKeeping() {
176 | if (TextUtils.isEmpty(mPath)) {
177 | return;
178 | }
179 |
180 | if (mRetentionPolicy == FLConst.RetentionPolicy.FILE_COUNT) {
181 | houseKeepByCount(mMaxFileCount);
182 | } else if (mRetentionPolicy == FLConst.RetentionPolicy.TOTAL_SIZE) {
183 | houseKeepBySize(mMaxSize);
184 | }
185 | }
186 |
187 | private void houseKeepByCount(int maxCount) {
188 | if (maxCount <= 0) {
189 | throw new IllegalStateException("invalid max file count: " + maxCount);
190 | }
191 |
192 | File file = new File(mPath);
193 | File dir = file.getParentFile();
194 | if (dir == null) {
195 | return;
196 | }
197 |
198 | File[] files = dir.listFiles();
199 | if (files == null || files.length <= maxCount) {
200 | return;
201 | }
202 |
203 | Arrays.sort(files, FILE_COMPARATOR);
204 | int deleteCount = files.length - maxCount;
205 | int successCount = 0;
206 | for (int i = 0; i < deleteCount; i++) {
207 | if (files[i].delete()) {
208 | successCount++;
209 | }
210 | }
211 | FL.d(FLConst.TAG, "house keeping complete: file count [%d -> %d]", files.length,
212 | files.length - successCount);
213 | }
214 |
215 | private void houseKeepBySize(long maxSize) {
216 | if (maxSize <= 0) {
217 | throw new IllegalStateException("invalid max total size: " + maxSize);
218 | }
219 |
220 | File file = new File(mPath);
221 | File dir = file.getParentFile();
222 | if (dir == null) {
223 | return;
224 | }
225 |
226 | File[] files = dir.listFiles();
227 | if (files == null) {
228 | return;
229 | }
230 |
231 | long totalSize = 0;
232 | for (File f : files) {
233 | totalSize += f.length();
234 | }
235 |
236 | if (totalSize <= maxSize) {
237 | return;
238 | }
239 |
240 | Arrays.sort(files, FILE_COMPARATOR);
241 | long newSize = totalSize;
242 | for (File f : files) {
243 | long size = f.length();
244 | if (f.delete()) {
245 | newSize -= size;
246 | if (newSize <= maxSize) {
247 | break;
248 | }
249 | }
250 | }
251 | FL.d(FLConst.TAG, "house keeping complete: total size [%d -> %d]", totalSize, newSize);
252 | }
253 |
254 | private void closeWriter() {
255 | if (mWriter != null) {
256 | try {
257 | mWriter.close();
258 | } catch (IOException e) {
259 | FL.e(FLConst.TAG, e);
260 | }
261 | mWriter = null;
262 | }
263 | }
264 | }
265 |
266 | static class LogData {
267 | final Context context;
268 | final String fileName, dirPath, line;
269 | final int retentionPolicy, maxFileCount;
270 | long maxTotalSize;
271 | boolean flush;
272 |
273 | LogData(Builder b) {
274 | context = b.context;
275 | fileName = b.fileName;
276 | dirPath = b.dirPath;
277 | line = b.line;
278 | retentionPolicy = b.retentionPolicy;
279 | maxFileCount = b.maxFileCount;
280 | maxTotalSize = b.maxTotalSize;
281 | flush = b.flush;
282 | }
283 |
284 | static class Builder {
285 | Context context;
286 | String fileName, dirPath, line;
287 | int retentionPolicy, maxFileCount;
288 | long maxTotalSize;
289 | boolean flush;
290 |
291 | Builder context(Context context) {
292 | this.context = context;
293 | return this;
294 | }
295 |
296 | Builder fileName(String fileName) {
297 | this.fileName = fileName;
298 | return this;
299 | }
300 |
301 | Builder dirPath(String dirPath) {
302 | this.dirPath = dirPath;
303 | return this;
304 | }
305 |
306 | Builder line(String line) {
307 | this.line = line;
308 | return this;
309 | }
310 |
311 | Builder retentionPolicy(int retentionPolicy) {
312 | this.retentionPolicy = retentionPolicy;
313 | return this;
314 | }
315 |
316 | Builder maxFileCount(int maxFileCount) {
317 | this.maxFileCount = maxFileCount;
318 | return this;
319 | }
320 |
321 | Builder maxSize(long maxSize) {
322 | this.maxTotalSize = maxSize;
323 | return this;
324 | }
325 |
326 | Builder flush(boolean flush) {
327 | this.flush = flush;
328 | return this;
329 | }
330 |
331 | LogData build() {
332 | return new LogData(this);
333 | }
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------