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 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 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 guessRestartActivityClass(@NonNull Context context) { + Class 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 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) 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 getLauncherActivity(@NonNull Context context) { + Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + if (intent != null && intent.getComponent() != null) { + try { + return (Class) 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 guessErrorActivityClass(@NonNull Context context) { + Class 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 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) 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 errorActivityClass = null; + private Class 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 getErrorActivityClass() { + return errorActivityClass; + } + + public void setErrorActivityClass(@Nullable Class errorActivityClass) { + this.errorActivityClass = errorActivityClass; + } + + @Nullable + public CrashReporter.CustomCrashDataCollector getCustomCrashDataCollector() { + return customCrashDataCollector; + } + + public void setCustomCrashDataCollector(@Nullable CrashReporter.CustomCrashDataCollector collector) { + this.customCrashDataCollector = collector; + } + + @Nullable + public Class getRestartActivityClass() { + return restartActivityClass; + } + + public void setRestartActivityClass(@Nullable Class 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 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 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'