diff --git a/FCL/build.gradle b/FCL/build.gradle
index b7bcd0c3..6843c9b9 100644
--- a/FCL/build.gradle
+++ b/FCL/build.gradle
@@ -11,7 +11,7 @@ android {
minSdk 26
targetSdk 32
versionCode 1
- versionName "1.0"
+ versionName "1.0.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -31,8 +31,8 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':FCLCore')
+ implementation project(path: ':FCLLibrary')
implementation project(path: ':FCLauncher')
- implementation 'cat.ereza:customactivityoncrash:2.4.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
diff --git a/FCL/src/main/AndroidManifest.xml b/FCL/src/main/AndroidManifest.xml
index c0366a8e..5ed866c3 100644
--- a/FCL/src/main/AndroidManifest.xml
+++ b/FCL/src/main/AndroidManifest.xml
@@ -31,7 +31,7 @@
android:allowNativeHeapPointerTagging="false"
tools:targetApi="32">
+
+
+
+
\ No newline at end of file
diff --git a/FCL/src/main/assets/app_runtime/java/jre17/bin-arm.tar.xz b/FCL/src/main/assets/app_runtime/java/jre17/bin-arm.tar.xz
new file mode 100644
index 00000000..1efabb78
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre17/bin-arm.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre17/bin-arm64.tar.xz b/FCL/src/main/assets/app_runtime/java/jre17/bin-arm64.tar.xz
new file mode 100644
index 00000000..6f0f2259
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre17/bin-arm64.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre17/bin-x86.tar.xz b/FCL/src/main/assets/app_runtime/java/jre17/bin-x86.tar.xz
new file mode 100644
index 00000000..93e35764
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre17/bin-x86.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre17/bin-x86_64.tar.xz b/FCL/src/main/assets/app_runtime/java/jre17/bin-x86_64.tar.xz
new file mode 100644
index 00000000..3cf8e263
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre17/bin-x86_64.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre17/universal.tar.xz b/FCL/src/main/assets/app_runtime/java/jre17/universal.tar.xz
new file mode 100644
index 00000000..7cffb6a9
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre17/universal.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre8/bin-arm.tar.xz b/FCL/src/main/assets/app_runtime/java/jre8/bin-arm.tar.xz
new file mode 100644
index 00000000..8aaf2bd0
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre8/bin-arm.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre8/bin-arm64.tar.xz b/FCL/src/main/assets/app_runtime/java/jre8/bin-arm64.tar.xz
new file mode 100644
index 00000000..9e2fa2a0
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre8/bin-arm64.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre8/bin-x86.tar.xz b/FCL/src/main/assets/app_runtime/java/jre8/bin-x86.tar.xz
new file mode 100644
index 00000000..9d29491a
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre8/bin-x86.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre8/bin-x86_64.tar.xz b/FCL/src/main/assets/app_runtime/java/jre8/bin-x86_64.tar.xz
new file mode 100644
index 00000000..fbc98a3c
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre8/bin-x86_64.tar.xz differ
diff --git a/FCL/src/main/assets/app_runtime/java/jre8/universal.tar.xz b/FCL/src/main/assets/app_runtime/java/jre8/universal.tar.xz
new file mode 100644
index 00000000..cc1bdc2d
Binary files /dev/null and b/FCL/src/main/assets/app_runtime/java/jre8/universal.tar.xz differ
diff --git a/FCL/src/main/java/com/tungsten/fcl/MainActivity.java b/FCL/src/main/java/com/tungsten/fcl/MainActivity.java
deleted file mode 100644
index b000da5b..00000000
--- a/FCL/src/main/java/com/tungsten/fcl/MainActivity.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.tungsten.fcl;
-
-import android.os.Bundle;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-public class MainActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
-
-}
\ No newline at end of file
diff --git a/FCL/src/main/java/com/tungsten/fcl/activity/MainActivity.java b/FCL/src/main/java/com/tungsten/fcl/activity/MainActivity.java
new file mode 100644
index 00000000..14ad6ff9
--- /dev/null
+++ b/FCL/src/main/java/com/tungsten/fcl/activity/MainActivity.java
@@ -0,0 +1,28 @@
+package com.tungsten.fcl.activity;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.widget.Button;
+
+import com.tungsten.fcl.R;
+import com.tungsten.fcllibrary.component.FCLActivity;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class MainActivity extends FCLActivity {
+
+ Button button;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ //FCLSeekBar view = findViewById(R.id.test_view);
+ //view.setEnabled(false);
+ ThemeEngine.getInstance().applyAndSave(this, Color.parseColor("#FF5C6BC0"));
+
+
+ button.setOnClickListener(null);
+ }
+
+}
\ No newline at end of file
diff --git a/FCL/src/main/java/com/tungsten/fcl/activity/SplashActivity.java b/FCL/src/main/java/com/tungsten/fcl/activity/SplashActivity.java
new file mode 100644
index 00000000..ac11fc25
--- /dev/null
+++ b/FCL/src/main/java/com/tungsten/fcl/activity/SplashActivity.java
@@ -0,0 +1,4 @@
+package com.tungsten.fcl.activity;
+
+public class SplashActivity {
+}
diff --git a/FCL/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/FCL/src/main/res/drawable/ic_baseline_arrow_back_24.xml
new file mode 100644
index 00000000..e5aba6ea
--- /dev/null
+++ b/FCL/src/main/res/drawable/ic_baseline_arrow_back_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/FCL/src/main/res/layout/activity_main.xml b/FCL/src/main/res/layout/activity_main.xml
index 3f9788c8..6b9ec345 100644
--- a/FCL/src/main/res/layout/activity_main.xml
+++ b/FCL/src/main/res/layout/activity_main.xml
@@ -4,11 +4,20 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".MainActivity">
+ tools:context=".activity.MainActivity">
+
+
+
-
\ No newline at end of file
diff --git a/FCL/src/main/res/values-night/themes.xml b/FCL/src/main/res/values-night/themes.xml
index e59b723a..4ae4b1cf 100644
--- a/FCL/src/main/res/values-night/themes.xml
+++ b/FCL/src/main/res/values-night/themes.xml
@@ -1,16 +1,20 @@
-
\ No newline at end of file
diff --git a/FCL/src/main/res/values/colors.xml b/FCL/src/main/res/values/colors.xml
index f8c6127d..1f0112d1 100644
--- a/FCL/src/main/res/values/colors.xml
+++ b/FCL/src/main/res/values/colors.xml
@@ -1,10 +1,6 @@
- #FFBB86FC
- #FF6200EE
- #FF3700B3
- #FF03DAC5
- #FF018786
+ #9EFF4A
#FF000000
#FFFFFFFF
\ No newline at end of file
diff --git a/FCL/src/main/res/values/themes.xml b/FCL/src/main/res/values/themes.xml
index 2734589a..4ae4b1cf 100644
--- a/FCL/src/main/res/values/themes.xml
+++ b/FCL/src/main/res/values/themes.xml
@@ -1,16 +1,20 @@
-
\ No newline at end of file
diff --git a/FCL/src/main/res/xml/provider_paths.xml b/FCL/src/main/res/xml/provider_paths.xml
new file mode 100644
index 00000000..2cb7ada1
--- /dev/null
+++ b/FCL/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java b/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java
index c4b6f76e..0823595c 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/auth/offline/OfflineAccount.java
@@ -131,7 +131,6 @@ public class OfflineAccount extends Account {
@Override
public Arguments getLaunchArguments(LaunchOptions options) throws IOException {
- if (!options.isDaemon()) return null;
server = new YggdrasilServer(0);
server.start();
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/download/DefaultCacheRepository.java b/FCLCore/src/main/java/com/tungsten/fclcore/download/DefaultCacheRepository.java
index 3a75b4ec..078c53d0 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/download/DefaultCacheRepository.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/download/DefaultCacheRepository.java
@@ -1,7 +1,7 @@
package com.tungsten.fclcore.download;
import com.google.gson.JsonParseException;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.game.LibraryDownloadTask;
import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.game.LibraryDownloadInfo;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/download/ProcessService.java b/FCLCore/src/main/java/com/tungsten/fclcore/download/ProcessService.java
index da406b55..f56ce19d 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/download/ProcessService.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/download/ProcessService.java
@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
import com.tungsten.fclauncher.FCLConfig;
import com.tungsten.fclauncher.FCLauncher;
import com.tungsten.fclauncher.bridge.FCLBridgeCallback;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/download/forge/ForgeNewInstallTask.java b/FCLCore/src/main/java/com/tungsten/fclcore/download/forge/ForgeNewInstallTask.java
index f2b00ddc..266b26d2 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/download/forge/ForgeNewInstallTask.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/download/forge/ForgeNewInstallTask.java
@@ -8,7 +8,7 @@ import static com.tungsten.fclcore.util.gson.JsonUtils.fromNonNullJson;
import android.content.Intent;
import android.os.Bundle;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.ArtifactMalformedException;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.LibraryAnalyzer;
@@ -19,7 +19,6 @@ import com.tungsten.fclcore.game.Artifact;
import com.tungsten.fclcore.game.DefaultGameRepository;
import com.tungsten.fclcore.game.DownloadInfo;
import com.tungsten.fclcore.game.DownloadType;
-import com.tungsten.fclcore.game.JavaVersion;
import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.task.FileDownloadTask;
@@ -27,7 +26,6 @@ import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.SocketServer;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.function.ExceptionalFunction;
-import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.ChecksumMismatchException;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/download/game/LibraryDownloadTask.java b/FCLCore/src/main/java/com/tungsten/fclcore/download/game/LibraryDownloadTask.java
index dfb6331e..364cf9e4 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/download/game/LibraryDownloadTask.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/download/game/LibraryDownloadTask.java
@@ -4,7 +4,7 @@ import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import static com.tungsten.fclcore.util.Logging.LOG;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.AbstractDependencyManager;
import com.tungsten.fclcore.download.ArtifactMalformedException;
import com.tungsten.fclcore.download.DefaultCacheRepository;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/download/optifine/OptiFineInstallTask.java b/FCLCore/src/main/java/com/tungsten/fclcore/download/optifine/OptiFineInstallTask.java
index 2cee9b6f..a3dd96b4 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/download/optifine/OptiFineInstallTask.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/download/optifine/OptiFineInstallTask.java
@@ -5,7 +5,7 @@ import static com.tungsten.fclcore.util.Lang.getOrDefault;
import android.content.Intent;
import android.os.Bundle;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.download.DefaultDependencyManager;
import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.download.ProcessService;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/game/GameRepository.java b/FCLCore/src/main/java/com/tungsten/fclcore/game/GameRepository.java
index 076b126f..714497dc 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/game/GameRepository.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/game/GameRepository.java
@@ -1,6 +1,6 @@
package com.tungsten.fclcore.game;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.task.Task;
import java.io.File;
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/launch/DefaultLauncher.java b/FCLCore/src/main/java/com/tungsten/fclcore/launch/DefaultLauncher.java
index 50b3acbe..c8391f16 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/launch/DefaultLauncher.java
+++ b/FCLCore/src/main/java/com/tungsten/fclcore/launch/DefaultLauncher.java
@@ -12,7 +12,7 @@ import com.tungsten.fclauncher.bridge.FCLBridge;
import com.tungsten.fclauncher.bridge.FCLBridgeCallback;
import com.tungsten.fclauncher.utils.Architecture;
import com.tungsten.fclcore.auth.AuthInfo;
-import com.tungsten.fclcore.constant.FCLPath;
+import com.tungsten.fclauncher.FCLPath;
import com.tungsten.fclcore.game.Argument;
import com.tungsten.fclcore.game.Arguments;
import com.tungsten.fclcore.game.GameRepository;
diff --git a/FCLLibrary/.gitignore b/FCLLibrary/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/FCLLibrary/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/FCLLibrary/build.gradle b/FCLLibrary/build.gradle
new file mode 100644
index 00000000..5b2becca
--- /dev/null
+++ b/FCLLibrary/build.gradle
@@ -0,0 +1,36 @@
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ namespace 'com.tungsten.fcllibrary'
+ compileSdk 32
+
+ defaultConfig {
+ minSdk 26
+ targetSdk 32
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ 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 project(path: ':FCLauncher')
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'com.google.android.material:material:1.6.1'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
\ No newline at end of file
diff --git a/FCLLibrary/consumer-rules.pro b/FCLLibrary/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/FCLLibrary/proguard-rules.pro b/FCLLibrary/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/FCLLibrary/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
\ No newline at end of file
diff --git a/FCLLibrary/src/androidTest/java/com/tungsten/fcllibrary/ExampleInstrumentedTest.java b/FCLLibrary/src/androidTest/java/com/tungsten/fcllibrary/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..a82c4134
--- /dev/null
+++ b/FCLLibrary/src/androidTest/java/com/tungsten/fcllibrary/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.tungsten.fcllibrary;
+
+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 Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.tungsten.fcllibrary.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/AndroidManifest.xml b/FCLLibrary/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..1b630340
--- /dev/null
+++ b/FCLLibrary/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLActivity.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLActivity.java
new file mode 100644
index 00000000..8eba7f8d
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLActivity.java
@@ -0,0 +1,55 @@
+package com.tungsten.fcllibrary.component;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.tungsten.fclauncher.FCLPath;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ FCLPath.loadPaths(this);
+ ThemeEngine.getInstance().setupThemeEngine(this);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ }
+ }
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(LocaleUtils.setLanguage(base));
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ LocaleUtils.setLanguage(this);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ ThemeEngine.getInstance().applyFullscreen(this, ThemeEngine.getInstance().getTheme().isFullscreen());
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLService.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLService.java
new file mode 100644
index 00000000..4aec8d2a
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/FCLService.java
@@ -0,0 +1,41 @@
+package com.tungsten.fcllibrary.component;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.tungsten.fclauncher.FCLPath;
+import com.tungsten.fcllibrary.component.theme.Theme;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLService extends Service {
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ FCLPath.loadPaths(this);
+ ThemeEngine.getInstance().setupThemeEngine(this);
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(LocaleUtils.setLanguage(base));
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ LocaleUtils.setLanguage(this);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/LocaleUtils.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/LocaleUtils.java
new file mode 100644
index 00000000..06211fa0
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/LocaleUtils.java
@@ -0,0 +1,64 @@
+package com.tungsten.fcllibrary.component;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.os.LocaleList;
+
+import java.util.Locale;
+
+public class LocaleUtils {
+
+ /**
+ * 0: System
+ * 1: English
+ * 2: Simplified Chinese
+ * 3: Traditional Chinese
+ * 4: Vietnamese
+ */
+
+ public static boolean isChinese(Context context) {
+ SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE);
+ int lang = sharedPreferences.getInt("lang", 0);
+ return lang == 2 || (lang == 0 && getSystemLocale() == Locale.CHINA);
+ }
+
+ public static Context setLanguage(Context context){
+ SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE);
+ return updateResources(context, sharedPreferences.getInt("lang", 0));
+ }
+
+ public static void changeLanguage(Context context, int lang) {
+ SharedPreferences sharedPreferences = context.getSharedPreferences("lang", Context.MODE_PRIVATE);
+ @SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt("lang", lang);
+ editor.apply();
+ }
+
+ private static Context updateResources(Context context, int lang) {
+ Locale locale = getLocale(lang);
+ Configuration configuration = context.getResources().getConfiguration();
+ configuration.setLocale(locale);
+ configuration.setLocales(new LocaleList(locale));
+ return context.createConfigurationContext(configuration);
+ }
+
+ private static Locale getLocale(int lang) {
+ switch (lang) {
+ case 1:
+ return Locale.ENGLISH;
+ case 2:
+ return Locale.CHINA;
+ case 3:
+ return Locale.TAIWAN;
+ default:
+ return getSystemLocale();
+ }
+ }
+
+ public static Locale getSystemLocale() {
+ return LocaleList.getDefault().get(0);
+ }
+
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLAlertDialog.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLAlertDialog.java
new file mode 100644
index 00000000..a45fd7ad
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLAlertDialog.java
@@ -0,0 +1,13 @@
+package com.tungsten.fcllibrary.component.dialog;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+public class FCLAlertDialog extends FCLDialog {
+
+ public FCLAlertDialog(@NonNull Context context) {
+ super(context);
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLDialog.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLDialog.java
new file mode 100644
index 00000000..56474604
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLDialog.java
@@ -0,0 +1,17 @@
+package com.tungsten.fcllibrary.component.dialog;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatDialog;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLDialog extends AppCompatDialog {
+
+ public FCLDialog(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().applyFullscreen(this, ThemeEngine.getInstance().getTheme().isFullscreen());
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLInfoDialog.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLInfoDialog.java
new file mode 100644
index 00000000..c72d5f4e
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/dialog/FCLInfoDialog.java
@@ -0,0 +1,13 @@
+package com.tungsten.fcllibrary.component.dialog;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+public class FCLInfoDialog extends FCLDialog {
+
+ public FCLInfoDialog(@NonNull Context context) {
+ super(context);
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java
new file mode 100644
index 00000000..bd0c9ff7
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/Theme.java
@@ -0,0 +1,95 @@
+package com.tungsten.fcllibrary.component.theme;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+
+import androidx.core.graphics.ColorUtils;
+
+public class Theme {
+
+ private int color;
+ private int ltColor;
+ private int dkColor;
+ private int autoTint;
+ private boolean fullscreen;
+
+ public Theme() {
+ this(Color.parseColor("#9EFF4A"), false);
+ }
+
+ public Theme(int color, boolean fullscreen) {
+ float[] ltHsv = new float[3];
+ Color.colorToHSV(color, ltHsv);
+ ltHsv[1] -= (1 - ltHsv[1]) * 0.3f;
+ ltHsv[2] += (1 - ltHsv[2]) * 0.3f;
+ float[] dkHsv = new float[3];
+ Color.colorToHSV(color, dkHsv);
+ dkHsv[1] += (1 - dkHsv[1]) * 0.3f;
+ dkHsv[2] -= (1 - dkHsv[2]) * 0.3f;
+ this.color = color;
+ this.ltColor = Color.HSVToColor(ltHsv);
+ this.dkColor = Color.HSVToColor(dkHsv);
+ this.fullscreen = fullscreen;
+ this.autoTint = ColorUtils.calculateLuminance(color) >= 0.5 ? Color.parseColor("#FF000000") : Color.parseColor("#FFFFFFFF");
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public int getLtColor() {
+ return ltColor;
+ }
+
+ public int getDkColor() {
+ return dkColor;
+ }
+
+ public int getAutoTint() {
+ return autoTint;
+ }
+
+ public boolean isFullscreen() {
+ return fullscreen;
+ }
+
+ public void setColor(int color) {
+ float[] ltHsv = new float[3];
+ Color.colorToHSV(color, ltHsv);
+ ltHsv[1] -= (1 - ltHsv[1]) * 0.3f;
+ ltHsv[2] += (1 - ltHsv[2]) * 0.3f;
+ float[] dkHsv = new float[3];
+ Color.colorToHSV(color, dkHsv);
+ dkHsv[1] += (1 - dkHsv[1]) * 0.3f;
+ dkHsv[2] -= (1 - dkHsv[2]) * 0.3f;
+ this.color = color;
+ this.ltColor = Color.HSVToColor(ltHsv);
+ this.dkColor = Color.HSVToColor(dkHsv);
+ this.autoTint = ColorUtils.calculateLuminance(color) >= 0.5 ? Color.parseColor("#FF000000") : Color.parseColor("#FFFFFFFF");
+ }
+
+ public void setFullscreen(boolean fullscreen) {
+ this.fullscreen = fullscreen;
+ }
+
+ public static Theme getTheme(Context context) {
+ SharedPreferences sharedPreferences;
+ sharedPreferences = context.getSharedPreferences("theme", MODE_PRIVATE);
+ int color = sharedPreferences.getInt("theme_color", Color.parseColor("#9EFF4A"));
+ boolean fullscreen = sharedPreferences.getBoolean("fullscreen", false);
+ return new Theme(color, fullscreen);
+ }
+
+ public static void saveTheme(Context context, Theme theme) {
+ SharedPreferences sharedPreferences;
+ SharedPreferences.Editor editor;
+ sharedPreferences = context.getSharedPreferences("theme", MODE_PRIVATE);
+ editor = sharedPreferences.edit();
+ editor.putInt("theme_color", theme.getColor());
+ editor.putBoolean("fullscreen", theme.isFullscreen());
+ editor.apply();
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/ThemeEngine.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/ThemeEngine.java
new file mode 100644
index 00000000..2bdc9bcf
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/theme/ThemeEngine.java
@@ -0,0 +1,99 @@
+package com.tungsten.fcllibrary.component.theme;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.util.HashMap;
+
+public class ThemeEngine {
+
+ public boolean initialized;
+ public static ThemeEngine instance;
+ public Handler handler;
+ public HashMap runnables;
+ public Theme theme;
+
+ public ThemeEngine() {
+
+ }
+
+ public static ThemeEngine getInstance() {
+ if (instance == null) {
+ instance = new ThemeEngine();
+ }
+ return instance;
+ }
+
+ public void setupThemeEngine(Context context) {
+ if (!initialized) {
+ handler = new Handler();
+ theme = Theme.getTheme(context);
+ runnables = new HashMap<>();
+ initialized = true;
+ }
+ }
+
+ public void registerView(View view, Runnable runnable) {
+ runnables.put(view, runnable);
+ handler.post(runnable);
+ }
+
+ public void unregisterView(View view) {
+ runnables.remove(view);
+ }
+
+ public Theme getTheme() {
+ return theme;
+ }
+
+ public void applyColor(int color) {
+ theme.setColor(color);
+ for (View view : runnables.keySet()) {
+ if (view != null && runnables.get(view) != null) {
+ handler.post(runnables.get(view));
+ }
+ }
+ }
+
+ public void applyFullscreen(Object object, boolean fullscreen) {
+ theme.setFullscreen(fullscreen);
+ Window window = null;
+ if (object instanceof Activity) {
+ window = ((Activity) object).getWindow();
+ }
+ if (object instanceof Dialog) {
+ window = ((Dialog) object).getWindow();
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && window != null) {
+ if (fullscreen) {
+ window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ } else {
+ window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+ }
+ window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
+ }
+ }
+
+ public void applyAndSave(Context context, int color) {
+ applyColor(color);
+ Theme.saveTheme(context, theme);
+ }
+
+ public void applyAndSave(Context context, Object object, boolean fullscreen) {
+ applyFullscreen(object, fullscreen);
+ Theme.saveTheme(context, theme);
+ }
+
+ public void applyAndSave(Context context, Object object, Theme theme) {
+ applyColor(theme.getColor());
+ applyFullscreen(object, theme.isFullscreen());
+ Theme.saveTheme(context, theme);
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLButton.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLButton.java
new file mode 100644
index 00000000..5beb322c
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLButton.java
@@ -0,0 +1,110 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatButton;
+
+import com.tungsten.fcllibrary.R;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+import com.tungsten.fcllibrary.util.ConvertUtils;
+
+public class FCLButton extends AppCompatButton {
+
+ private boolean isDown;
+
+ private GradientDrawable drawableNormal;
+ private GradientDrawable drawablePress;
+
+ private final Runnable runnable = () -> {
+ drawableNormal.setStroke(ConvertUtils.dip2px(getContext(), 1.5f), Color.GRAY);
+ drawableNormal.setColor(Color.TRANSPARENT);
+ drawablePress.setStroke(ConvertUtils.dip2px(getContext(), 1.5f), ThemeEngine.getInstance().getTheme().getColor());
+ drawablePress.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ if (isDown) {
+ setBackgroundDrawable(drawablePress);
+ }
+ else {
+ setBackgroundDrawable(drawableNormal);
+ }
+ };
+
+ private void init(int shape) {
+ setSingleLine(true);
+ setAllCaps(false);
+ setGravity(Gravity.CENTER);
+ setMinWidth(0);
+ setMinHeight(0);
+ setMinimumWidth(0);
+ setMinimumHeight(0);
+ setPadding(
+ ConvertUtils.dip2px(getContext(), shape == GradientDrawable.RECTANGLE ? 16f : 10f),
+ ConvertUtils.dip2px(getContext(), 10f),
+ ConvertUtils.dip2px(getContext(), shape == GradientDrawable.RECTANGLE ? 16f : 10f),
+ ConvertUtils.dip2px(getContext(), 10f)
+ );
+ drawableNormal = new GradientDrawable();
+ drawablePress = new GradientDrawable();
+ drawableNormal.setShape(shape);
+ drawableNormal.setCornerRadius(ConvertUtils.dip2px(getContext(), 8));
+ drawableNormal.setStroke(ConvertUtils.dip2px(getContext(), 1.5f), Color.GRAY);
+ drawableNormal.setColor(Color.TRANSPARENT);
+ drawablePress.setShape(shape);
+ drawablePress.setCornerRadius(ConvertUtils.dip2px(getContext(), 5));
+ drawablePress.setStroke(ConvertUtils.dip2px(getContext(), 1.5f), Color.GRAY);
+ drawablePress.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ }
+
+ public FCLButton(@NonNull Context context) {
+ super(context);
+ init(GradientDrawable.RECTANGLE);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLButton(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLButton);
+ int shape = typedArray.getInteger(R.styleable.FCLButton_shape, GradientDrawable.RECTANGLE);
+ init(shape);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLButton);
+ int shape = typedArray.getInteger(R.styleable.FCLButton_shape, GradientDrawable.RECTANGLE);
+ init(shape);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ isDown = true;
+ setBackgroundDrawable(drawablePress);
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ isDown = false;
+ setBackgroundDrawable(drawableNormal);
+ }
+ return super.onTouchEvent(event);
+ }
+
+ public void setShape(int shape) {
+ drawableNormal.setShape(shape);
+ drawablePress.setShape(shape);
+ }
+
+ public int getShape() {
+ return ((GradientDrawable) getBackground()).getShape();
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLCheckBox.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLCheckBox.java
new file mode 100644
index 00000000..7b92d572
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLCheckBox.java
@@ -0,0 +1,46 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatCheckBox;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLCheckBox extends AppCompatCheckBox {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+ android.R.attr.state_checked
+ },
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getDkColor(),
+ Color.GRAY
+ };
+ setButtonTintList(new ColorStateList(state, color));
+ };
+
+ public FCLCheckBox(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLCheckBox(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLCheckBox(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLEditText.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLEditText.java
new file mode 100644
index 00000000..ab2d5be7
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLEditText.java
@@ -0,0 +1,50 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatEditText;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLEditText extends AppCompatEditText {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+ android.R.attr.state_focused
+ },
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getColor(),
+ Color.GRAY
+ };
+ setBackgroundTintList(new ColorStateList(state, color));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ getTextCursorDrawable().setTint(ThemeEngine.getInstance().getTheme().getColor());
+ }
+ };
+
+ public FCLEditText(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLImageButton.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLImageButton.java
new file mode 100644
index 00000000..d3802160
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLImageButton.java
@@ -0,0 +1,110 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageButton;
+
+import com.tungsten.fcllibrary.R;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+import com.tungsten.fcllibrary.util.ConvertUtils;
+
+public class FCLImageButton extends AppCompatImageButton {
+
+ private boolean autoTint;
+
+ private boolean isDown;
+
+ private GradientDrawable drawableNormal;
+ private GradientDrawable drawablePress;
+
+ private final Runnable runnable = () -> {
+ drawableNormal.setColor(Color.TRANSPARENT);
+ drawablePress.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ if (isDown) {
+ setBackgroundDrawable(drawablePress);
+ }
+ else {
+ setBackgroundDrawable(drawableNormal);
+ }
+ if (autoTint) {
+ int[][] state = {
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getAutoTint()
+ };
+ setImageTintList(new ColorStateList(state, color));
+ }
+ };
+
+ private void init() {
+ setPadding(
+ ConvertUtils.dip2px(getContext(), 8f),
+ ConvertUtils.dip2px(getContext(), 8f),
+ ConvertUtils.dip2px(getContext(), 8f),
+ ConvertUtils.dip2px(getContext(), 8f)
+ );
+ setScaleType(ScaleType.FIT_XY);
+ drawableNormal = new GradientDrawable();
+ drawablePress = new GradientDrawable();
+ drawableNormal.setShape(GradientDrawable.OVAL);
+ drawablePress.setShape(GradientDrawable.OVAL);
+ drawableNormal.setColor(Color.TRANSPARENT);
+ drawablePress.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ }
+
+ public FCLImageButton(@NonNull Context context) {
+ super(context);
+ init();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLImageButton(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLImageButton);
+ autoTint = typedArray.getBoolean(R.styleable.FCLImageButton_auto_tint, false);
+ typedArray.recycle();
+ init();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLImageButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLImageButton);
+ autoTint = typedArray.getBoolean(R.styleable.FCLImageButton_auto_tint, false);
+ typedArray.recycle();
+ init();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ isDown = true;
+ setBackgroundDrawable(drawablePress);
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ isDown = false;
+ setBackgroundDrawable(drawableNormal);
+ }
+ return super.onTouchEvent(event);
+ }
+
+ public void setAutoTint(boolean autoTint) {
+ this.autoTint = autoTint;
+ }
+
+ public boolean isAutoTint() {
+ return autoTint;
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLProgressBar.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLProgressBar.java
new file mode 100644
index 00000000..c65a1f22
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLProgressBar.java
@@ -0,0 +1,44 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLProgressBar extends ProgressBar {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getDkColor()
+ };
+ setProgressTintList(new ColorStateList(state, color));
+ setIndeterminateTintList(new ColorStateList(state, color));
+ };
+
+ public FCLProgressBar(Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLProgressBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLRadioButton.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLRadioButton.java
new file mode 100644
index 00000000..9ca6ad4c
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLRadioButton.java
@@ -0,0 +1,46 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatRadioButton;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLRadioButton extends AppCompatRadioButton {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+ android.R.attr.state_checked
+ },
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getDkColor(),
+ Color.GRAY
+ };
+ setButtonTintList(new ColorStateList(state, color));
+ };
+
+ public FCLRadioButton(Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLRadioButton(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLRadioButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSeekBar.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSeekBar.java
new file mode 100644
index 00000000..b00aab45
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSeekBar.java
@@ -0,0 +1,42 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatSeekBar;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLSeekBar extends AppCompatSeekBar {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getDkColor()
+ };
+ setThumbTintList(new ColorStateList(state, color));
+ setProgressTintList(new ColorStateList(state, color));
+ };
+
+ public FCLSeekBar(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLSeekBar(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLSeekBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSwitch.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSwitch.java
new file mode 100644
index 00000000..06a27536
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLSwitch.java
@@ -0,0 +1,51 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLSwitch extends SwitchCompat {
+
+ private final Runnable runnable = () -> {
+ int[][] state = {
+ {
+ android.R.attr.state_checked
+ },
+ {
+
+ }
+ };
+ int[] color = {
+ ThemeEngine.getInstance().getTheme().getDkColor(),
+ Color.LTGRAY
+ };
+ int[] subColor = {
+ ThemeEngine.getInstance().getTheme().getLtColor(),
+ Color.GRAY
+ };
+ setThumbTintList(new ColorStateList(state, color));
+ setTrackTintList(new ColorStateList(state, subColor));
+ };
+
+ public FCLSwitch(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLSwitch(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLSwitch(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTextView.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTextView.java
new file mode 100644
index 00000000..f96c1a6a
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTextView.java
@@ -0,0 +1,65 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+
+import com.tungsten.fcllibrary.R;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+
+public class FCLTextView extends AppCompatTextView {
+
+ private boolean autoTint;
+
+ private final Runnable runnable = () -> {
+ if (autoTint) {
+ setTextColor(ThemeEngine.getInstance().getTheme().getAutoTint());
+ }
+ };
+
+ public FCLTextView(@NonNull Context context) {
+ super(context);
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLTextView);
+ autoTint = typedArray.getBoolean(R.styleable.FCLTextView_auto_text_tint, false);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLTextView);
+ autoTint = typedArray.getBoolean(R.styleable.FCLTextView_auto_text_tint, false);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public void alert() {
+ setTextColor(Color.RED);
+ }
+
+ public void normal() {
+ setTextColor(Color.GRAY);
+ }
+
+ public void emphasize() {
+ setTextColor(Color.BLACK);
+ }
+
+ public void setAutoTint(boolean autoTint) {
+ this.autoTint = autoTint;
+ }
+
+ public boolean isAutoTint() {
+ return autoTint;
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTitleView.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTitleView.java
new file mode 100644
index 00000000..3b0cf998
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/component/view/FCLTitleView.java
@@ -0,0 +1,115 @@
+package com.tungsten.fcllibrary.component.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.tungsten.fcllibrary.R;
+import com.tungsten.fcllibrary.component.theme.ThemeEngine;
+import com.tungsten.fcllibrary.util.ConvertUtils;
+
+public class FCLTitleView extends View {
+
+ private Path outlinePath;
+ private Paint outlinePaint;
+ private Paint insidePaint;
+ private Paint textPaint;
+
+ private String title;
+
+ private final Runnable runnable = () -> {
+ outlinePaint.setAntiAlias(true);
+ outlinePaint.setColor(ThemeEngine.getInstance().getTheme().getDkColor());
+ outlinePaint.setStyle(Paint.Style.STROKE);
+ outlinePaint.setStrokeWidth(ConvertUtils.dip2px(getContext(), 3));
+ insidePaint.setAntiAlias(true);
+ insidePaint.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ insidePaint.setStyle(Paint.Style.FILL);
+ textPaint.setAntiAlias(true);
+ textPaint.setStyle(Paint.Style.FILL);
+ textPaint.setTextSize(56);
+ textPaint.setColor(ThemeEngine.getInstance().getTheme().getAutoTint());
+ invalidate();
+ };
+
+ private void init(String title) {
+ this.title = title;
+ outlinePath = new Path();
+ outlinePaint = new Paint();
+ insidePaint = new Paint();
+ textPaint = new Paint();
+ outlinePaint.setAntiAlias(true);
+ outlinePaint.setColor(ThemeEngine.getInstance().getTheme().getDkColor());
+ outlinePaint.setStyle(Paint.Style.STROKE);
+ outlinePaint.setStrokeWidth(ConvertUtils.dip2px(getContext(), 3));
+ insidePaint.setAntiAlias(true);
+ insidePaint.setColor(ThemeEngine.getInstance().getTheme().getLtColor());
+ insidePaint.setStyle(Paint.Style.FILL);
+ textPaint.setAntiAlias(true);
+ textPaint.setStyle(Paint.Style.FILL);
+ textPaint.setTextSize(56);
+ textPaint.setColor(ThemeEngine.getInstance().getTheme().getAutoTint());
+ textPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ public FCLTitleView(Context context) {
+ super(context);
+ init("");
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLTitleView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLTitleView);
+ String title = typedArray.getString(R.styleable.FCLTitleView_title);
+ if (title == null) {
+ title = "";
+ }
+ init(title);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ public FCLTitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FCLTitleView);
+ String title = typedArray.getString(R.styleable.FCLTitleView_title);
+ if (title == null) {
+ title = "";
+ }
+ init(title);
+ typedArray.recycle();
+ ThemeEngine.getInstance().registerView(this, runnable);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ float width = getWidth() - ConvertUtils.dip2px(getContext(), 3);
+ float height = getHeight() - ConvertUtils.dip2px(getContext(), 1.5f);
+ outlinePath.moveTo(ConvertUtils.dip2px(getContext(), 1.5f),0);
+ outlinePath.lineTo((int) (height * Math.sqrt(3) / 4) + ConvertUtils.dip2px(getContext(), 1.5f), (int) (height * 3 / 4));
+ outlinePath.arcTo((int) (height * (Math.sqrt(3) - 1) / 2) + ConvertUtils.dip2px(getContext(), 1.5f), 0, (int) (height * (Math.sqrt(3) + 1) / 2) + ConvertUtils.dip2px(getContext(), 1.5f), height, 150, -60, false);
+ outlinePath.lineTo((float) (width - (Math.sqrt(3) * height / 2) + ConvertUtils.dip2px(getContext(), 1.5f)), height);
+ outlinePath.arcTo((int) (width - (height * (Math.sqrt(3) + 1) / 2)) + ConvertUtils.dip2px(getContext(), 1.5f), 0, (int) (width - (height * (Math.sqrt(3) - 1) / 2) + ConvertUtils.dip2px(getContext(), 1.5f)), height, 90, -60, false);
+ outlinePath.lineTo(width + ConvertUtils.dip2px(getContext(), 1.5f), 0);
+ canvas.drawPath(outlinePath, insidePaint);
+ canvas.drawPath(outlinePath, outlinePaint);
+ canvas.drawText(title, (int) (getWidth() / 2), (int) (getHeight() / 2), textPaint);
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ invalidate();
+ }
+
+ public String getTitle() {
+ return title;
+ }
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReportActivity.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReportActivity.java
new file mode 100644
index 00000000..fe6f3bc9
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReportActivity.java
@@ -0,0 +1,101 @@
+package com.tungsten.fcllibrary.crash;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.FileProvider;
+
+import com.tungsten.fcllibrary.R;
+import com.tungsten.fcllibrary.component.FCLActivity;
+import com.tungsten.fcllibrary.component.view.FCLButton;
+import com.tungsten.fcllibrary.component.view.FCLTextView;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+public class CrashReportActivity extends FCLActivity implements View.OnClickListener {
+
+ private FCLButton restart;
+ private FCLButton close;
+ private FCLButton copy;
+ private FCLButton share;
+
+ private FCLTextView error;
+
+ private CrashReporterConfig config;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_crash);
+
+ config = CrashReporter.getConfigFromIntent(getIntent());
+
+ if (config == null) {
+ // This should never happen - Just finish the activity to avoid a recursive crash.
+ finish();
+ }
+
+ restart = findViewById(R.id.restart);
+ close = findViewById(R.id.close);
+ copy = findViewById(R.id.copy);
+ share = findViewById(R.id.share);
+
+ restart.setOnClickListener(this);
+ close.setOnClickListener(this);
+ copy.setOnClickListener(this);
+ share.setOnClickListener(this);
+
+ error = findViewById(R.id.error);
+ error.setText(CrashReporter.getAllErrorDetailsFromIntent(this, getIntent()));
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == restart) {
+ CrashReporter.restartApplication(this, config);
+ }
+ if (view == close) {
+ CrashReporter.closeApplication(this, config);
+ }
+ if (view == copy) {
+ copyErrorToClipboard();
+ }
+ if (view == share) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ File file = File.createTempFile("crash_report", ".txt");
+ Files.write(file.toPath(), CrashReporter.getAllErrorDetailsFromIntent(this, getIntent()).getBytes(StandardCharsets.UTF_8));
+ Uri uri = FileProvider.getUriForFile(this, getString(R.string.file_browser_provider), file);
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ startActivity(Intent.createChooser(intent, getString(R.string.crash_reporter_share)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void copyErrorToClipboard() {
+ String errorInformation = CrashReporter.getAllErrorDetailsFromIntent(this, getIntent());
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ // Are there any devices without clipboard...?
+ if (clipboard != null) {
+ ClipData clip = ClipData.newPlainText(null, errorInformation);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(this, R.string.crash_reporter_toast, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+}
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporter.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporter.java
new file mode 100644
index 00000000..f02bf2c3
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporter.java
@@ -0,0 +1,752 @@
+package com.tungsten.fcllibrary.crash;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import com.tungsten.fcllibrary.R;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.lang.ref.WeakReference;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayDeque;
+import java.util.Date;
+import java.util.Deque;
+import java.util.List;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public final class CrashReporter {
+
+ private final static String TAG = "CrashReporter";
+
+ // Extras passed to the error activity
+ private static final String EXTRA_CONFIG = "com.tungsten.fcllibrary.crash.EXTRA_CONFIG";
+ private static final String EXTRA_STACK_TRACE = "com.tungsten.fcllibrary.crash.EXTRA_STACK_TRACE";
+ private static final String EXTRA_ACTIVITY_LOG = "com.tungsten.fcllibrary.crash.EXTRA_ACTIVITY_LOG";
+ private static final String EXTRA_CUSTOM_CRASH_DATA = "com.tungsten.fcllibrary.crash.EXTRA_CUSTOM_CRASH_DATA";
+
+ // General constants
+ private static final String INTENT_ACTION_ERROR_ACTIVITY = "com.tungsten.fcllibrary.crash.ERROR";
+ private static final String INTENT_ACTION_RESTART_ACTIVITY = "com.tungsten.fcllibrary.crash.RESTART";
+ private static final String REPORTER_HANDLER_PACKAGE_NAME = "com.tungsten.fcllibrary.crash";
+ private static final String DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os";
+ private static final int TIME_TO_CONSIDER_FOREGROUND_MS = 500;
+ private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1
+ private static final int MAX_ACTIVITIES_IN_LOG = 50;
+
+ // Shared preferences
+ private static final String SHARED_PREFERENCES_FILE = "crash_reporter";
+ private static final String SHARED_PREFERENCES_FIELD_TIMESTAMP = "last_crash_timestamp";
+
+ // Internal variables
+ @SuppressLint("StaticFieldLeak") // This is an application-wide component
+ private static Application application;
+ private static CrashReporterConfig config = new CrashReporterConfig();
+ private static final Deque activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG);
+ private static WeakReference lastActivityCreated = new WeakReference<>(null);
+ private static long lastActivityCreatedTimestamp = 0L;
+ private static boolean isInBackground = true;
+
+
+ /**
+ * Installs CrashReporter on the application using the default error activity.
+ *
+ * @param context Context to use for obtaining the ApplicationContext. Must not be null.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static void install(@Nullable final Context context) {
+ try {
+ if (context == null) {
+ Log.e(TAG, "Install failed: context is null!");
+ } else {
+ // INSTALL!
+ final Thread.UncaughtExceptionHandler oldHandler = Thread.getDefaultUncaughtExceptionHandler();
+
+ if (oldHandler != null && oldHandler.getClass().getName().startsWith(REPORTER_HANDLER_PACKAGE_NAME)) {
+ Log.e(TAG, "CrashReporter was already installed, doing nothing!");
+ } else {
+ if (oldHandler != null && !oldHandler.getClass().getName().startsWith(DEFAULT_HANDLER_PACKAGE_NAME)) {
+ Log.e(TAG, "IMPORTANT WARNING! You already have an UncaughtExceptionHandler, are you sure this is correct? If you use a custom UncaughtExceptionHandler, you must initialize it AFTER CrashReporter! Installing anyway, but your original handler will not be called.");
+ }
+
+ application = (Application) context.getApplicationContext();
+
+ // We define a default exception handler that does what we want so it can be called from Crashlytics/ACRA
+ Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
+ if (config.isEnabled()) {
+ Log.e(TAG, "App has crashed, executing CrashReporter's UncaughtExceptionHandler", throwable);
+
+ if (hasCrashedInTheLastSeconds(application)) {
+ Log.e(TAG, "App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?", throwable);
+ if (oldHandler != null) {
+ oldHandler.uncaughtException(thread, throwable);
+ return;
+ }
+ } else {
+ setLastCrashTimestamp(application, new Date().getTime());
+
+ Class extends Activity> errorActivityClass = config.getErrorActivityClass();
+
+ if (errorActivityClass == null) {
+ errorActivityClass = guessErrorActivityClass(application);
+ }
+
+ if (isStackTraceLikelyConflictive(throwable, errorActivityClass)) {
+ Log.e(TAG, "Your application class or your error activity have crashed, the custom activity will not be launched!");
+ if (oldHandler != null) {
+ oldHandler.uncaughtException(thread, throwable);
+ return;
+ }
+ } else if (config.getBackgroundMode() == CrashReporterConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground
+ || (lastActivityCreatedTimestamp >= new Date().getTime() - TIME_TO_CONSIDER_FOREGROUND_MS)) {
+
+ final Intent intent = new Intent(application, errorActivityClass);
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw);
+ String stackTraceString = sw.toString();
+
+ // Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
+ // The limit is 1MB on Android but some devices seem to have it lower.
+ // See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
+ // And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
+ if (stackTraceString.length() > MAX_STACK_TRACE_SIZE) {
+ String disclaimer = " [stack trace too large]";
+ stackTraceString = stackTraceString.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;
+ }
+ intent.putExtra(EXTRA_STACK_TRACE, stackTraceString);
+
+ CustomCrashDataCollector collector = config.getCustomCrashDataCollector();
+ if (collector != null) {
+ try {
+ intent.putExtra(EXTRA_CUSTOM_CRASH_DATA, collector.onCrash());
+ } catch (Throwable t) {
+ Log.e(TAG, "An unknown error occurred while invoking the custom crash data collector's onCrash. Please check your implementation.", t);
+ }
+ }
+
+ if (config.isTrackActivities()) {
+ StringBuilder activityLogStringBuilder = new StringBuilder();
+ while (!activityLog.isEmpty()) {
+ activityLogStringBuilder.append(activityLog.poll());
+ }
+ intent.putExtra(EXTRA_ACTIVITY_LOG, activityLogStringBuilder.toString());
+ }
+
+ if (config.isShowRestartButton() && config.getRestartActivityClass() == null) {
+ // We can set the restartActivityClass because the app will terminate right now,
+ // and when relaunched, will be null again by default.
+ config.setRestartActivityClass(guessRestartActivityClass(application));
+ }
+
+ intent.putExtra(EXTRA_CONFIG, config);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ if (config.getEventListener() != null) {
+ try {
+ config.getEventListener().onLaunchErrorActivity();
+ } catch (Throwable t) {
+ Log.e(TAG, "An unknown error occurred while invoking the event listener's onLaunchErrorActivity. Please check your implementation.", t);
+ }
+ }
+ application.startActivity(intent);
+ } else if (config.getBackgroundMode() == CrashReporterConfig.BACKGROUND_MODE_CRASH) {
+ if (oldHandler != null) {
+ oldHandler.uncaughtException(thread, throwable);
+ return;
+ }
+ // If it is null (should not be), we let it continue and kill the process or it will be stuck
+ }
+ // Else (BACKGROUND_MODE_SILENT): do nothing and let the following code kill the process
+ }
+ final Activity lastActivity = lastActivityCreated.get();
+ if (lastActivity != null) {
+ // We finish the activity, this solves a bug which causes infinite recursion.
+ // See: https://github.com/ACRA/acra/issues/42
+ lastActivity.finish();
+ lastActivityCreated.clear();
+ }
+ killCurrentProcess();
+ } else if (oldHandler != null) {
+ oldHandler.uncaughtException(thread, throwable);
+ }
+ });
+ application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
+ int currentlyStartedActivities = 0;
+ final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+
+ @Override
+ public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
+ if (activity.getClass() != config.getErrorActivityClass()) {
+ // Copied from ACRA:
+ // Ignore activityClass because we want the last
+ // application Activity that was started so that we can
+ // explicitly kill it off.
+ lastActivityCreated = new WeakReference<>(activity);
+ lastActivityCreatedTimestamp = new Date().getTime();
+ }
+ if (config.isTrackActivities()) {
+ activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " created\n");
+ }
+ }
+
+ @Override
+ public void onActivityStarted(@NonNull Activity activity) {
+ currentlyStartedActivities++;
+ isInBackground = (currentlyStartedActivities == 0);
+ // Do nothing
+ }
+
+ @Override
+ public void onActivityResumed(@NonNull Activity activity) {
+ if (config.isTrackActivities()) {
+ activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " resumed\n");
+ }
+ }
+
+ @Override
+ public void onActivityPaused(@NonNull Activity activity) {
+ if (config.isTrackActivities()) {
+ activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " paused\n");
+ }
+ }
+
+ @Override
+ public void onActivityStopped(@NonNull Activity activity) {
+ // Do nothing
+ currentlyStartedActivities--;
+ isInBackground = (currentlyStartedActivities == 0);
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
+ // Do nothing
+ }
+
+ @Override
+ public void onActivityDestroyed(@NonNull Activity activity) {
+ if (config.isTrackActivities()) {
+ activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " destroyed\n");
+ }
+ }
+ });
+ }
+
+ Log.i(TAG, "CrashReporter has been installed.");
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "An unknown error occurred while installing CrashReporter, it may not have been properly initialized. Please report this as a bug if needed.", t);
+ }
+ }
+
+ /**
+ * Given an Intent, returns the stack trace extra from it.
+ *
+ * @param intent The Intent. Must not be null.
+ * @return The stacktrace, or null if not provided.
+ */
+ @Nullable
+ public static String getStackTraceFromIntent(@NonNull Intent intent) {
+ return intent.getStringExtra(CrashReporter.EXTRA_STACK_TRACE);
+ }
+
+ /**
+ * Given an Intent, returns the custom crash data extra from it.
+ *
+ * @param intent The Intent. Must not be null.
+ * @return The custom collector trace, or null if not provided.
+ */
+ @Nullable
+ public static String getCustomCrashDataFromIntent(@NonNull Intent intent) {
+ return intent.getStringExtra(CrashReporter.EXTRA_CUSTOM_CRASH_DATA);
+ }
+
+ /**
+ * Given an Intent, returns the config extra from it.
+ *
+ * @param intent The Intent. Must not be null.
+ * @return The config, or null if not provided.
+ */
+ @Nullable
+ public static CrashReporterConfig getConfigFromIntent(@NonNull Intent intent) {
+ CrashReporterConfig config = (CrashReporterConfig) intent.getSerializableExtra(CrashReporter.EXTRA_CONFIG);
+ if (config != null && config.isLogErrorOnRestart()) {
+ String stackTrace = getStackTraceFromIntent(intent);
+ if (stackTrace != null) {
+ Log.e(TAG, "The previous app process crashed. This is the stack trace of the crash:\n" + getStackTraceFromIntent(intent));
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Given an Intent, returns the activity log extra from it.
+ *
+ * @param intent The Intent. Must not be null.
+ * @return The activity log, or null if not provided.
+ */
+ @Nullable
+ public static String getActivityLogFromIntent(@NonNull Intent intent) {
+ return intent.getStringExtra(CrashReporter.EXTRA_ACTIVITY_LOG);
+ }
+
+ /**
+ * Given an Intent, returns several error details including the stack trace extra from the intent.
+ *
+ * @param context A valid context. Must not be null.
+ * @param intent The Intent. Must not be null.
+ * @return The full error details.
+ */
+ @NonNull
+ public static String getAllErrorDetailsFromIntent(@NonNull Context context, @NonNull Intent intent) {
+ // I don't think that this needs localization because it's a development string...
+
+ Date currentDate = new Date();
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+
+ // Get build date
+ String buildDateAsString = getBuildDateAsString(context, dateFormat);
+
+ // Get app version
+ String versionName = getVersionName(context);
+
+ String errorDetails = "";
+
+ errorDetails += context.getString(R.string.crash_reporter_hint) + " \n\n";
+
+ errorDetails += "Build version: " + versionName + " \n";
+ if (buildDateAsString != null) {
+ errorDetails += "Build date: " + buildDateAsString + " \n";
+ }
+ errorDetails += "Current date: " + dateFormat.format(currentDate) + " \n";
+ // Added a space between line feeds to fix #18.
+ // Ideally, we should not use this method at all... It is only formatted this way because of coupling with the default error activity.
+ // We should move it to a method that returns a bean, and let anyone format it as they wish.
+ errorDetails += "Device: " + getDeviceModelName() + " \n";
+ errorDetails += "OS version: Android " + Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ") \n \n";
+ errorDetails += "Stack trace: \n";
+ errorDetails += getStackTraceFromIntent(intent);
+
+ String activityLog = getActivityLogFromIntent(intent);
+
+ if (activityLog != null) {
+ errorDetails += "\nUser actions: \n";
+ errorDetails += activityLog;
+ }
+
+ String customCrashData = getCustomCrashDataFromIntent(intent);
+ if (customCrashData != null) {
+ errorDetails += "\nAdditional data: \n";
+ errorDetails += customCrashData;
+ }
+
+ return errorDetails;
+ }
+
+ /**
+ * Given an Intent, restarts the app and launches a startActivity to that intent.
+ * The flags NEW_TASK and CLEAR_TASK are set if the Intent does not have them, to ensure
+ * the app stack is fully cleared.
+ * If an event listener is provided, the restart app event is invoked.
+ * Must only be used from your error activity.
+ *
+ * @param activity The current error activity. Must not be null.
+ * @param intent The Intent. Must not be null.
+ * @param config The config object as obtained by calling getConfigFromIntent.
+ */
+ public static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CrashReporterConfig config) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ if (intent.getComponent() != null) {
+ // If the class name has been set, we force it to simulate a Launcher launch.
+ // If we don't do this, if you restart from the error activity, then press home,
+ // and then launch the activity from the launcher, the main activity appears twice on the backstack.
+ // This will most likely not have any detrimental effect because if you set the Intent component,
+ // if will always be launched regardless of the actions specified here.
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ }
+ if (config.getEventListener() != null) {
+ try {
+ config.getEventListener().onRestartAppFromErrorActivity();
+ } catch (Throwable t) {
+ Log.e(TAG, "An unknown error occurred while invoking the event listener's onRestartAppFromErrorActivity. Please check your implementation.", t);
+ }
+ }
+ activity.finish();
+ activity.startActivity(intent);
+ killCurrentProcess();
+ }
+
+ public static void restartApplication(@NonNull Activity activity, @NonNull CrashReporterConfig config) {
+ Intent intent = new Intent(activity, config.getRestartActivityClass());
+ restartApplicationWithIntent(activity, intent, config);
+ }
+
+ /**
+ * Closes the app.
+ * If an event listener is provided, the close app event is invoked.
+ * Must only be used from your error activity.
+ *
+ * @param activity The current error activity. Must not be null.
+ * @param config The config object as obtained by calling getConfigFromIntent.
+ */
+ public static void closeApplication(@NonNull Activity activity, @NonNull CrashReporterConfig config) {
+ if (config.getEventListener() != null) {
+ try {
+ config.getEventListener().onCloseAppFromErrorActivity();
+ } catch (Throwable t) {
+ Log.e(TAG, "An unknown error occurred while invoking the event listener's onCloseAppFromErrorActivity. Please check your implementation.", t);
+ }
+ }
+ activity.finish();
+ killCurrentProcess();
+ }
+
+ // INTERNAL METHODS NOT TO BE USED BY THIRD PARTIES
+
+ /**
+ * INTERNAL method that returns the current configuration of the library.
+ * If you want to check the config, use CaocConfig.Builder.get();
+ *
+ * @return the current configuration
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @NonNull
+ public static CrashReporterConfig getConfig() {
+ return config;
+ }
+
+ /**
+ * INTERNAL method that sets the configuration of the library.
+ * You must not use this, use CaocConfig.Builder.apply()
+ *
+ * @param config the configuration to use
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static void setConfig(@NonNull CrashReporterConfig config) {
+ CrashReporter.config = config;
+ }
+
+ /**
+ * INTERNAL method that checks if the stack trace that just crashed is conflictive. This is true in the following scenarios:
+ * - The application has crashed while initializing (handleBindApplication is in the stack)
+ * - The crash occurred inside the "error_activity" process
+ *
+ * @param throwable The throwable from which the stack trace will be checked
+ * @param activityClass The activity class to launch when the app crashes
+ * @return true if this stack trace is conflictive and the activity must not be launched, false otherwise
+ */
+ private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwable, @NonNull Class extends Activity> activityClass) {
+ String process;
+ try {
+ BufferedReader br = new BufferedReader(new FileReader("/proc/self/cmdline"));
+ process = br.readLine().trim();
+ br.close();
+ } catch (IOException e) {
+ process = null;
+ }
+
+ if (process != null && process.endsWith(":error_activity")) {
+ // Error happened in the error activity process - conflictive, so use default handler
+ return true;
+ }
+
+ do {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ for (StackTraceElement element : stackTrace) {
+ if (element.getClassName().equals("android.app.ActivityThread") && element.getMethodName().equals("handleBindApplication")) {
+ return true;
+ }
+ }
+ } while ((throwable = throwable.getCause()) != null);
+ return false;
+ }
+
+ /**
+ * INTERNAL method that returns the build date of the current APK as a string, or null if unable to determine it.
+ *
+ * @param context A valid context. Must not be null.
+ * @param dateFormat DateFormat to use to convert from Date to String
+ * @return The formatted date, or "Unknown" if unable to determine it.
+ */
+ @Nullable
+ private static String getBuildDateAsString(@NonNull Context context, @NonNull DateFormat dateFormat) {
+ long buildDate;
+ try {
+ ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
+ ZipFile zf = new ZipFile(ai.sourceDir);
+
+ // If this failed, try with the old zip method
+ ZipEntry ze = zf.getEntry("classes.dex");
+ buildDate = ze.getTime();
+
+
+ zf.close();
+ } catch (Exception e) {
+ buildDate = 0;
+ }
+
+ if (buildDate > 631152000000L) {
+ return dateFormat.format(new Date(buildDate));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * INTERNAL method that returns the version name of the current app, or null if unable to determine it.
+ *
+ * @param context A valid context. Must not be null.
+ * @return The version name, or "Unknown if unable to determine it.
+ */
+ @NonNull
+ private static String getVersionName(Context context) {
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return packageInfo.versionName;
+ } catch (Exception e) {
+ return "Unknown";
+ }
+ }
+
+ /**
+ * INTERNAL method that returns the device model name with correct capitalization.
+ * Taken from: http://stackoverflow.com/a/12707479/1254846
+ *
+ * @return The device model name (i.e., "LGE Nexus 5")
+ */
+ @NonNull
+ private static String getDeviceModelName() {
+ String manufacturer = Build.MANUFACTURER;
+ String model = Build.MODEL;
+ if (model.startsWith(manufacturer)) {
+ return capitalize(model);
+ } else {
+ return capitalize(manufacturer) + " " + model;
+ }
+ }
+
+ /**
+ * INTERNAL method that capitalizes the first character of a string
+ *
+ * @param s The string to capitalize
+ * @return The capitalized string
+ */
+ @NonNull
+ private static String capitalize(@Nullable String s) {
+ if (s == null || s.length() == 0) {
+ return "";
+ }
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+
+ /**
+ * INTERNAL method used to guess which activity must be called from the error activity to restart the app.
+ * It will first get activities from the AndroidManifest with intent filter ,
+ * if it cannot find them, then it will get the default launcher.
+ * If there is no default launcher, this returns null.
+ *
+ * @param context A valid context. Must not be null.
+ * @return The guessed restart activity class, or null if no suitable one is found
+ */
+ @Nullable
+ private static Class extends Activity> guessRestartActivityClass(@NonNull Context context) {
+ Class extends Activity> resolvedActivityClass;
+
+ // If action is defined, use that
+ resolvedActivityClass = getRestartActivityClassWithIntentFilter(context);
+
+ // Else, get the default launcher activity
+ if (resolvedActivityClass == null) {
+ resolvedActivityClass = getLauncherActivity(context);
+ }
+
+ return resolvedActivityClass;
+ }
+
+ /**
+ * INTERNAL method used to get the first activity with an intent-filter ,
+ * If there is no activity with that intent filter, this returns null.
+ *
+ * @param context A valid context. Must not be null.
+ * @return A valid activity class, or null if no suitable one is found
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private static Class extends Activity> getRestartActivityClassWithIntentFilter(@NonNull Context context) {
+ Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName());
+ List resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
+ PackageManager.GET_RESOLVED_FILTER);
+
+ if (resolveInfos.size() > 0) {
+ ResolveInfo resolveInfo = resolveInfos.get(0);
+ try {
+ return (Class extends Activity>) Class.forName(resolveInfo.activityInfo.name);
+ } catch (ClassNotFoundException e) {
+ // Should not happen, print it to the log!
+ Log.e(TAG, "Failed when resolving the restart activity class via intent filter, stack trace follows!", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * INTERNAL method used to get the default launcher activity for the app.
+ * If there is no launchable activity, this returns null.
+ *
+ * @param context A valid context. Must not be null.
+ * @return A valid activity class, or null if no suitable one is found
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private static Class extends Activity> getLauncherActivity(@NonNull Context context) {
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
+ if (intent != null && intent.getComponent() != null) {
+ try {
+ return (Class extends Activity>) Class.forName(intent.getComponent().getClassName());
+ } catch (ClassNotFoundException e) {
+ // Should not happen, print it to the log!
+ Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * INTERNAL method used to guess which error activity must be called when the app crashes.
+ * It will first get activities from the AndroidManifest with intent filter ,
+ * if it cannot find them, then it will use the default error activity.
+ *
+ * @param context A valid context. Must not be null.
+ * @return The guessed error activity class, or the default error activity if not found
+ */
+ @NonNull
+ private static Class extends Activity> guessErrorActivityClass(@NonNull Context context) {
+ Class extends Activity> resolvedActivityClass;
+
+ // If action is defined, use that
+ resolvedActivityClass = getErrorActivityClassWithIntentFilter(context);
+
+ // Else, get the default error activity
+ if (resolvedActivityClass == null) {
+ resolvedActivityClass = CrashReportActivity.class;
+ }
+
+ return resolvedActivityClass;
+ }
+
+ /**
+ * INTERNAL method used to get the first activity with an intent-filter ,
+ * If there is no activity with that intent filter, this returns null.
+ *
+ * @param context A valid context. Must not be null.
+ * @return A valid activity class, or null if no suitable one is found
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private static Class extends Activity> getErrorActivityClassWithIntentFilter(@NonNull Context context) {
+ Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName());
+ List resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
+ PackageManager.GET_RESOLVED_FILTER);
+
+ if (resolveInfos.size() > 0) {
+ ResolveInfo resolveInfo = resolveInfos.get(0);
+ try {
+ return (Class extends Activity>) Class.forName(resolveInfo.activityInfo.name);
+ } catch (ClassNotFoundException e) {
+ // Should not happen, print it to the log!
+ Log.e(TAG, "Failed when resolving the error activity class via intent filter, stack trace follows!", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * INTERNAL method that kills the current process.
+ * It is used after restarting or killing the app.
+ */
+ private static void killCurrentProcess() {
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(10);
+ }
+
+ /**
+ * INTERNAL method that stores the last crash timestamp
+ *
+ * @param timestamp The current timestamp.
+ */
+ @SuppressLint("ApplySharedPref") // This must be done immediately since we are killing the app
+ private static void setLastCrashTimestamp(@NonNull Context context, long timestamp) {
+ context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit().putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit();
+ }
+
+ /**
+ * INTERNAL method that gets the last crash timestamp
+ *
+ * @return The last crash timestamp, or -1 if not set.
+ */
+ private static long getLastCrashTimestamp(@NonNull Context context) {
+ return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1);
+ }
+
+ /**
+ * INTERNAL method that tells if the app has crashed in the last seconds.
+ * This is used to avoid restart loops.
+ *
+ * @return true if the app has crashed in the last seconds, false otherwise.
+ */
+ private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) {
+ long lastTimestamp = getLastCrashTimestamp(context);
+ long currentTimestamp = new Date().getTime();
+
+ return (lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < config.getMinTimeBetweenCrashesMs());
+ }
+
+ /**
+ * Interface to be called when events occur, so they can be reported
+ * by the app as, for example, Google Analytics events.
+ */
+ public interface EventListener extends Serializable {
+ void onLaunchErrorActivity();
+
+ void onRestartAppFromErrorActivity();
+
+ void onCloseAppFromErrorActivity();
+ }
+
+ /**
+ * Interface to be called to collect additional crash data when a crash occurs.
+ */
+ public interface CustomCrashDataCollector extends Serializable {
+ String onCrash();
+ }
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterConfig.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterConfig.java
new file mode 100644
index 00000000..4cb6fc11
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterConfig.java
@@ -0,0 +1,337 @@
+package com.tungsten.fcllibrary.crash;
+
+import android.app.Activity;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.Serializable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Modifier;
+
+public class CrashReporterConfig implements Serializable {
+
+ @IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface BackgroundMode {
+ //I hate empty blocks
+ }
+
+ public static final int BACKGROUND_MODE_SILENT = 0;
+ public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1;
+ public static final int BACKGROUND_MODE_CRASH = 2;
+
+ private int backgroundMode = BACKGROUND_MODE_SHOW_CUSTOM;
+ private boolean enabled = true;
+ private boolean showErrorDetails = true;
+ private boolean showRestartButton = true;
+ private boolean logErrorOnRestart = true;
+ private boolean trackActivities = false;
+ private int minTimeBetweenCrashesMs = 3000;
+ private Integer errorDrawable = null;
+ private Class extends Activity> errorActivityClass = null;
+ private Class extends Activity> restartActivityClass = null;
+ private CrashReporter.CustomCrashDataCollector customCrashDataCollector = null;
+ private CrashReporter.EventListener eventListener = null;
+
+ @BackgroundMode
+ public int getBackgroundMode() {
+ return backgroundMode;
+ }
+
+ public void setBackgroundMode(@BackgroundMode int backgroundMode) {
+ this.backgroundMode = backgroundMode;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isShowErrorDetails() {
+ return showErrorDetails;
+ }
+
+ public void setShowErrorDetails(boolean showErrorDetails) {
+ this.showErrorDetails = showErrorDetails;
+ }
+
+ public boolean isShowRestartButton() {
+ return showRestartButton;
+ }
+
+ public void setShowRestartButton(boolean showRestartButton) {
+ this.showRestartButton = showRestartButton;
+ }
+
+ public boolean isLogErrorOnRestart() {
+ return logErrorOnRestart;
+ }
+
+ public void setLogErrorOnRestart(boolean logErrorOnRestart) {
+ this.logErrorOnRestart = logErrorOnRestart;
+ }
+
+ public boolean isTrackActivities() {
+ return trackActivities;
+ }
+
+ public void setTrackActivities(boolean trackActivities) {
+ this.trackActivities = trackActivities;
+ }
+
+ public int getMinTimeBetweenCrashesMs() {
+ return minTimeBetweenCrashesMs;
+ }
+
+ public void setMinTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
+ this.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
+ }
+
+ @Nullable
+ @DrawableRes
+ public Integer getErrorDrawable() {
+ return errorDrawable;
+ }
+
+ public void setErrorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
+ this.errorDrawable = errorDrawable;
+ }
+
+ @Nullable
+ public Class extends Activity> getErrorActivityClass() {
+ return errorActivityClass;
+ }
+
+ public void setErrorActivityClass(@Nullable Class extends Activity> errorActivityClass) {
+ this.errorActivityClass = errorActivityClass;
+ }
+
+ @Nullable
+ public CrashReporter.CustomCrashDataCollector getCustomCrashDataCollector() {
+ return customCrashDataCollector;
+ }
+
+ public void setCustomCrashDataCollector(@Nullable CrashReporter.CustomCrashDataCollector collector) {
+ this.customCrashDataCollector = collector;
+ }
+
+ @Nullable
+ public Class extends Activity> getRestartActivityClass() {
+ return restartActivityClass;
+ }
+
+ public void setRestartActivityClass(@Nullable Class extends Activity> restartActivityClass) {
+ this.restartActivityClass = restartActivityClass;
+ }
+
+ @Nullable
+ public CrashReporter.EventListener getEventListener() {
+ return eventListener;
+ }
+
+ public void setEventListener(@Nullable CrashReporter.EventListener eventListener) {
+ this.eventListener = eventListener;
+ }
+
+ public static class Builder {
+ private CrashReporterConfig config;
+
+ @NonNull
+ public static Builder create() {
+ Builder builder = new Builder();
+ CrashReporterConfig currentConfig = CrashReporter.getConfig();
+
+ CrashReporterConfig config = new CrashReporterConfig();
+ config.backgroundMode = currentConfig.backgroundMode;
+ config.enabled = currentConfig.enabled;
+ config.showErrorDetails = currentConfig.showErrorDetails;
+ config.showRestartButton = currentConfig.showRestartButton;
+ config.logErrorOnRestart = currentConfig.logErrorOnRestart;
+ config.trackActivities = currentConfig.trackActivities;
+ config.minTimeBetweenCrashesMs = currentConfig.minTimeBetweenCrashesMs;
+ config.errorDrawable = currentConfig.errorDrawable;
+ config.errorActivityClass = currentConfig.errorActivityClass;
+ config.customCrashDataCollector = currentConfig.customCrashDataCollector;
+ config.restartActivityClass = currentConfig.restartActivityClass;
+ config.eventListener = currentConfig.eventListener;
+
+ builder.config = config;
+
+ return builder;
+ }
+
+ /**
+ * Defines if the error activity must be launched when the app is on background.
+ * BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM: launch the error activity when the app is in background,
+ * BackgroundMode.BACKGROUND_MODE_CRASH: launch the default system error when the app is in background,
+ * BackgroundMode.BACKGROUND_MODE_SILENT: crash silently when the app is in background,
+ * The default is BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM (the app will be brought to front when a crash occurs).
+ */
+ @NonNull
+ public Builder backgroundMode(@BackgroundMode int backgroundMode) {
+ config.backgroundMode = backgroundMode;
+ return this;
+ }
+
+ /**
+ * Defines if CustomActivityOnCrash crash interception mechanism is enabled.
+ * Set it to true if you want CustomActivityOnCrash to intercept crashes,
+ * false if you want them to be treated as if the library was not installed.
+ * The default is true.
+ */
+ @NonNull
+ public Builder enabled(boolean enabled) {
+ config.enabled = enabled;
+ return this;
+ }
+
+ /**
+ * Defines if the error activity must shown the error details button.
+ * Set it to true if you want to show the full stack trace and device info,
+ * false if you want it to be hidden.
+ * The default is true.
+ */
+ @NonNull
+ public Builder showErrorDetails(boolean showErrorDetails) {
+ config.showErrorDetails = showErrorDetails;
+ return this;
+ }
+
+ /**
+ * Defines if the error activity should show a restart button.
+ * Set it to true if you want to show a restart button,
+ * false if you want to show a close button.
+ * Note that even if restart is enabled but you app does not have any launcher activities,
+ * a close button will still be used by the default error activity.
+ * The default is true.
+ */
+ @NonNull
+ public Builder showRestartButton(boolean showRestartButton) {
+ config.showRestartButton = showRestartButton;
+ return this;
+ }
+
+ /**
+ * Defines if the stack trace must be logged again once the custom activity is shown.
+ * Set it to true if you want to log the stack trace again,
+ * false if you don't want the extra logging.
+ * This option exists because the default Android Studio logcat view only shows the output
+ * of the current process, and since the error activity runs on a new process,
+ * you can't see the previous output easily.
+ * Internally, it's logged when getConfigFromIntent() is called.
+ * The default is true.
+ */
+ @NonNull
+ public Builder logErrorOnRestart(boolean logErrorOnRestart) {
+ config.logErrorOnRestart = logErrorOnRestart;
+ return this;
+ }
+
+ /**
+ * Defines if the activities visited by the user should be tracked
+ * so they are reported when an error occurs.
+ * The default is false.
+ */
+ @NonNull
+ public Builder trackActivities(boolean trackActivities) {
+ config.trackActivities = trackActivities;
+ return this;
+ }
+
+ /**
+ * Defines the time that must pass between app crashes to determine that we are not
+ * in a crash loop. If a crash has occurred less that this time ago,
+ * the error activity will not be launched and the system crash screen will be invoked.
+ * The default is 3000.
+ */
+ @NonNull
+ public Builder minTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
+ config.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
+ return this;
+ }
+
+ /**
+ * Defines which drawable to use in the default error activity image.
+ * Set this if you want to use an image other than the default one.
+ * The default is R.drawable.customactivityoncrash_error_image (a cute upside-down bug).
+ */
+ @NonNull
+ public Builder errorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
+ config.errorDrawable = errorDrawable;
+ return this;
+ }
+
+ /**
+ * Sets the error activity class to launch when a crash occurs.
+ * If null, the default error activity will be used.
+ */
+ @NonNull
+ public Builder errorActivity(@Nullable Class extends Activity> errorActivityClass) {
+ config.errorActivityClass = errorActivityClass;
+ return this;
+ }
+
+ /**
+ * Sets the main activity class that the error activity must launch when a crash occurs.
+ * If not set or set to null, the default launch activity will be used.
+ * If your app has no launch activities and this is not set, the default error activity will close instead.
+ */
+ @NonNull
+ public Builder restartActivity(@Nullable Class extends Activity> restartActivityClass) {
+ config.restartActivityClass = restartActivityClass;
+ return this;
+ }
+
+ /**
+ * Sets an event listener to be called when events occur, so they can be reported
+ * by the app as, for example, Google Analytics events.
+ * If not set or set to null, no events will be reported.
+ *
+ * @param eventListener The event listener.
+ * @throws IllegalArgumentException if the eventListener is an inner or anonymous class
+ */
+ @NonNull
+ public Builder eventListener(@Nullable CrashReporter.EventListener eventListener) {
+ if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) {
+ throw new IllegalArgumentException("The event listener cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.");
+ } else {
+ config.eventListener = eventListener;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the custom data collector class to invoke when a crash occurs.
+ * If not set or set to null, no custom data will be collected.
+ *
+ * @param collector The custom data collector.
+ * @throws IllegalArgumentException if the collector is an inner or anonymous class
+ */
+ @NonNull
+ public Builder customCrashDataCollector(@Nullable CrashReporter.CustomCrashDataCollector collector) {
+ if (collector != null && collector.getClass().getEnclosingClass() != null && !Modifier.isStatic(collector.getClass().getModifiers())) {
+ throw new IllegalArgumentException("The custom data collector cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.");
+ } else {
+ config.customCrashDataCollector = collector;
+ }
+ return this;
+ }
+
+ @NonNull
+ public CrashReporterConfig get() {
+ return config;
+ }
+
+ public void apply() {
+ CrashReporter.setConfig(config);
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterInitProvider.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterInitProvider.java
new file mode 100644
index 00000000..efa950d5
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/crash/CrashReporterInitProvider.java
@@ -0,0 +1,45 @@
+package com.tungsten.fcllibrary.crash;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class CrashReporterInitProvider extends ContentProvider {
+
+ public boolean onCreate() {
+ CrashReporter.install(getContext());
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/java/com/tungsten/fcllibrary/util/ConvertUtils.java b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/util/ConvertUtils.java
new file mode 100644
index 00000000..a9e19f04
--- /dev/null
+++ b/FCLLibrary/src/main/java/com/tungsten/fcllibrary/util/ConvertUtils.java
@@ -0,0 +1,16 @@
+package com.tungsten.fcllibrary.util;
+
+import android.content.Context;
+
+public class ConvertUtils {
+
+ public static int dip2px(Context context, float dpValue) {
+ float scare = context.getResources().getDisplayMetrics().density;
+ return (int) (dpValue * scare + 0.5f);
+ }
+
+ public static int px2dip(Context context, float pxValue) {
+ float scare = context.getResources().getDisplayMetrics().density;
+ return (int) (pxValue / scare + 0.5f);
+ }
+}
\ No newline at end of file
diff --git a/FCLLibrary/src/main/res/layout/activity_crash.xml b/FCLLibrary/src/main/res/layout/activity_crash.xml
new file mode 100644
index 00000000..2712f31d
--- /dev/null
+++ b/FCLLibrary/src/main/res/layout/activity_crash.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FCLLibrary/src/main/res/values/attrs.xml b/FCLLibrary/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..97f6a39e
--- /dev/null
+++ b/FCLLibrary/src/main/res/values/attrs.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FCLLibrary/src/main/res/values/strings.xml b/FCLLibrary/src/main/res/values/strings.xml
new file mode 100644
index 00000000..150f6ebb
--- /dev/null
+++ b/FCLLibrary/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+
+ com.tungsten.fcl.provider
+
+ Crash Reporter
+ Fold Craft Launcher has encountered an unresolvable error, please seek help from the developer or others.
+ Restart
+ Close
+ Copy
+ Share
+ Copy successfully!
+
\ No newline at end of file
diff --git a/FCLLibrary/src/test/java/com/tungsten/fcllibrary/ExampleUnitTest.java b/FCLLibrary/src/test/java/com/tungsten/fcllibrary/ExampleUnitTest.java
new file mode 100644
index 00000000..6f511793
--- /dev/null
+++ b/FCLLibrary/src/test/java/com/tungsten/fcllibrary/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.tungsten.fcllibrary;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/FCLCore/src/main/java/com/tungsten/fclcore/constant/FCLPath.java b/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java
similarity index 96%
rename from FCLCore/src/main/java/com/tungsten/fclcore/constant/FCLPath.java
rename to FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java
index 7b3d1b21..9e1a73a6 100644
--- a/FCLCore/src/main/java/com/tungsten/fclcore/constant/FCLPath.java
+++ b/FCLauncher/src/main/java/com/tungsten/fclauncher/FCLPath.java
@@ -1,4 +1,4 @@
-package com.tungsten.fclcore.constant;
+package com.tungsten.fclauncher;
import android.content.Context;
diff --git a/settings.gradle b/settings.gradle
index 776ed2c2..be96e3be 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,3 +19,4 @@ rootProject.name = "Fold Craft Launcher"
include ':FCL'
include ':FCLCore'
include ':FCLauncher'
+include ':FCLLibrary'