add a crash reporter & add some custom theme view & upload assets

This commit is contained in:
Tungstend 2022-10-26 00:04:56 +08:00
parent 78effa6543
commit 18aa63e017
64 changed files with 2676 additions and 52 deletions

View File

@ -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'

View File

@ -31,7 +31,7 @@
android:allowNativeHeapPointerTagging="false"
tools:targetApi="32">
<activity
android:name=".MainActivity"
android:name=".activity.MainActivity"
android:launchMode="standard"
android:multiprocess="true"
android:alwaysRetainTaskState="true"
@ -47,9 +47,21 @@
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name="com.tungsten.fcllibrary.crash.CrashReportActivity"
android:screenOrientation="sensorLandscape"/>
<service
android:name="com.tungsten.fclcore.download.ProcessService"
android:process=":processService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/file_browser_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>
</manifest>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
package com.tungsten.fcl.activity;
public class SplashActivity {
}

View File

@ -0,0 +1,10 @@
<vector
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -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">
<com.tungsten.fcllibrary.component.view.FCLTitleView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0"
app:layout_constraintWidth_percent="0.5"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextureView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,16 +1,20 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.FoldCraftLauncher" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.FoldCraftLauncher" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorPrimary">@color/default_theme_color</item>
<item name="colorPrimaryVariant">@color/default_theme_color</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorSecondary">@color/default_theme_color</item>
<item name="colorSecondaryVariant">@color/default_theme_color</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<item name="android:statusBarColor">@color/default_theme_color</item>
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>

View File

@ -1,10 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="default_theme_color">#9EFF4A</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -1,16 +1,20 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.FoldCraftLauncher" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.FoldCraftLauncher" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/default_theme_color</item>
<item name="colorPrimaryVariant">@color/default_theme_color</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorSecondary">@color/default_theme_color</item>
<item name="colorSecondaryVariant">@color/default_theme_color</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<item name="android:statusBarColor">@color/default_theme_color</item>
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<paths
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingDefaultResource">
<external-path
name="external_files"
path="."/>
<cache-path
name="cache_path"
path="."/>
</paths>

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

1
FCLLibrary/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

36
FCLLibrary/build.gradle Normal file
View File

@ -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'
}

View File

21
FCLLibrary/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.tungsten.fcllibrary.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name=".crash.CrashReporterInitProvider"
android:authorities="${applicationId}.crashreporterinitprovider"
android:exported="false"
android:initOrder="101" />
</application>
</manifest>

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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<View, Runnable> 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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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<String> activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG);
private static WeakReference<Activity> lastActivityCreated = new WeakReference<>(null);
private static long lastActivityCreatedTimestamp = 0L;
private static boolean isInBackground = true;
/**
* Installs CrashReporter on the application using the default error activity.
*
* @param context Context to use for obtaining the ApplicationContext. Must not be null.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void install(@Nullable final Context context) {
try {
if (context == null) {
Log.e(TAG, "Install failed: context is null!");
} else {
// INSTALL!
final Thread.UncaughtExceptionHandler oldHandler = Thread.getDefaultUncaughtExceptionHandler();
if (oldHandler != null && oldHandler.getClass().getName().startsWith(REPORTER_HANDLER_PACKAGE_NAME)) {
Log.e(TAG, "CrashReporter was already installed, doing nothing!");
} else {
if (oldHandler != null && !oldHandler.getClass().getName().startsWith(DEFAULT_HANDLER_PACKAGE_NAME)) {
Log.e(TAG, "IMPORTANT WARNING! You already have an UncaughtExceptionHandler, are you sure this is correct? If you use a custom UncaughtExceptionHandler, you must initialize it AFTER CrashReporter! Installing anyway, but your original handler will not be called.");
}
application = (Application) context.getApplicationContext();
// We define a default exception handler that does what we want so it can be called from Crashlytics/ACRA
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (config.isEnabled()) {
Log.e(TAG, "App has crashed, executing CrashReporter's UncaughtExceptionHandler", throwable);
if (hasCrashedInTheLastSeconds(application)) {
Log.e(TAG, "App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?", throwable);
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
} else {
setLastCrashTimestamp(application, new Date().getTime());
Class<? extends Activity> errorActivityClass = config.getErrorActivityClass();
if (errorActivityClass == null) {
errorActivityClass = guessErrorActivityClass(application);
}
if (isStackTraceLikelyConflictive(throwable, errorActivityClass)) {
Log.e(TAG, "Your application class or your error activity have crashed, the custom activity will not be launched!");
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
} else if (config.getBackgroundMode() == CrashReporterConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground
|| (lastActivityCreatedTimestamp >= new Date().getTime() - TIME_TO_CONSIDER_FOREGROUND_MS)) {
final Intent intent = new Intent(application, errorActivityClass);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
String stackTraceString = sw.toString();
// Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.
// The limit is 1MB on Android but some devices seem to have it lower.
// See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html
// And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171
if (stackTraceString.length() > MAX_STACK_TRACE_SIZE) {
String disclaimer = " [stack trace too large]";
stackTraceString = stackTraceString.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;
}
intent.putExtra(EXTRA_STACK_TRACE, stackTraceString);
CustomCrashDataCollector collector = config.getCustomCrashDataCollector();
if (collector != null) {
try {
intent.putExtra(EXTRA_CUSTOM_CRASH_DATA, collector.onCrash());
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while invoking the custom crash data collector's onCrash. Please check your implementation.", t);
}
}
if (config.isTrackActivities()) {
StringBuilder activityLogStringBuilder = new StringBuilder();
while (!activityLog.isEmpty()) {
activityLogStringBuilder.append(activityLog.poll());
}
intent.putExtra(EXTRA_ACTIVITY_LOG, activityLogStringBuilder.toString());
}
if (config.isShowRestartButton() && config.getRestartActivityClass() == null) {
// We can set the restartActivityClass because the app will terminate right now,
// and when relaunched, will be null again by default.
config.setRestartActivityClass(guessRestartActivityClass(application));
}
intent.putExtra(EXTRA_CONFIG, config);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (config.getEventListener() != null) {
try {
config.getEventListener().onLaunchErrorActivity();
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while invoking the event listener's onLaunchErrorActivity. Please check your implementation.", t);
}
}
application.startActivity(intent);
} else if (config.getBackgroundMode() == CrashReporterConfig.BACKGROUND_MODE_CRASH) {
if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
return;
}
// If it is null (should not be), we let it continue and kill the process or it will be stuck
}
// Else (BACKGROUND_MODE_SILENT): do nothing and let the following code kill the process
}
final Activity lastActivity = lastActivityCreated.get();
if (lastActivity != null) {
// We finish the activity, this solves a bug which causes infinite recursion.
// See: https://github.com/ACRA/acra/issues/42
lastActivity.finish();
lastActivityCreated.clear();
}
killCurrentProcess();
} else if (oldHandler != null) {
oldHandler.uncaughtException(thread, throwable);
}
});
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
int currentlyStartedActivities = 0;
final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
@Override
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
if (activity.getClass() != config.getErrorActivityClass()) {
// Copied from ACRA:
// Ignore activityClass because we want the last
// application Activity that was started so that we can
// explicitly kill it off.
lastActivityCreated = new WeakReference<>(activity);
lastActivityCreatedTimestamp = new Date().getTime();
}
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " created\n");
}
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
currentlyStartedActivities++;
isInBackground = (currentlyStartedActivities == 0);
// Do nothing
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " resumed\n");
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " paused\n");
}
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
// Do nothing
currentlyStartedActivities--;
isInBackground = (currentlyStartedActivities == 0);
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
// Do nothing
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (config.isTrackActivities()) {
activityLog.add(dateFormat.format(new Date()) + ": " + activity.getClass().getSimpleName() + " destroyed\n");
}
}
});
}
Log.i(TAG, "CrashReporter has been installed.");
}
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while installing CrashReporter, it may not have been properly initialized. Please report this as a bug if needed.", t);
}
}
/**
* Given an Intent, returns the stack trace extra from it.
*
* @param intent The Intent. Must not be null.
* @return The stacktrace, or null if not provided.
*/
@Nullable
public static String getStackTraceFromIntent(@NonNull Intent intent) {
return intent.getStringExtra(CrashReporter.EXTRA_STACK_TRACE);
}
/**
* Given an Intent, returns the custom crash data extra from it.
*
* @param intent The Intent. Must not be null.
* @return The custom collector trace, or null if not provided.
*/
@Nullable
public static String getCustomCrashDataFromIntent(@NonNull Intent intent) {
return intent.getStringExtra(CrashReporter.EXTRA_CUSTOM_CRASH_DATA);
}
/**
* Given an Intent, returns the config extra from it.
*
* @param intent The Intent. Must not be null.
* @return The config, or null if not provided.
*/
@Nullable
public static CrashReporterConfig getConfigFromIntent(@NonNull Intent intent) {
CrashReporterConfig config = (CrashReporterConfig) intent.getSerializableExtra(CrashReporter.EXTRA_CONFIG);
if (config != null && config.isLogErrorOnRestart()) {
String stackTrace = getStackTraceFromIntent(intent);
if (stackTrace != null) {
Log.e(TAG, "The previous app process crashed. This is the stack trace of the crash:\n" + getStackTraceFromIntent(intent));
}
}
return config;
}
/**
* Given an Intent, returns the activity log extra from it.
*
* @param intent The Intent. Must not be null.
* @return The activity log, or null if not provided.
*/
@Nullable
public static String getActivityLogFromIntent(@NonNull Intent intent) {
return intent.getStringExtra(CrashReporter.EXTRA_ACTIVITY_LOG);
}
/**
* Given an Intent, returns several error details including the stack trace extra from the intent.
*
* @param context A valid context. Must not be null.
* @param intent The Intent. Must not be null.
* @return The full error details.
*/
@NonNull
public static String getAllErrorDetailsFromIntent(@NonNull Context context, @NonNull Intent intent) {
// I don't think that this needs localization because it's a development string...
Date currentDate = new Date();
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
// Get build date
String buildDateAsString = getBuildDateAsString(context, dateFormat);
// Get app version
String versionName = getVersionName(context);
String errorDetails = "";
errorDetails += context.getString(R.string.crash_reporter_hint) + " \n\n";
errorDetails += "Build version: " + versionName + " \n";
if (buildDateAsString != null) {
errorDetails += "Build date: " + buildDateAsString + " \n";
}
errorDetails += "Current date: " + dateFormat.format(currentDate) + " \n";
// Added a space between line feeds to fix #18.
// Ideally, we should not use this method at all... It is only formatted this way because of coupling with the default error activity.
// We should move it to a method that returns a bean, and let anyone format it as they wish.
errorDetails += "Device: " + getDeviceModelName() + " \n";
errorDetails += "OS version: Android " + Build.VERSION.RELEASE + " (SDK " + Build.VERSION.SDK_INT + ") \n \n";
errorDetails += "Stack trace: \n";
errorDetails += getStackTraceFromIntent(intent);
String activityLog = getActivityLogFromIntent(intent);
if (activityLog != null) {
errorDetails += "\nUser actions: \n";
errorDetails += activityLog;
}
String customCrashData = getCustomCrashDataFromIntent(intent);
if (customCrashData != null) {
errorDetails += "\nAdditional data: \n";
errorDetails += customCrashData;
}
return errorDetails;
}
/**
* Given an Intent, restarts the app and launches a startActivity to that intent.
* The flags NEW_TASK and CLEAR_TASK are set if the Intent does not have them, to ensure
* the app stack is fully cleared.
* If an event listener is provided, the restart app event is invoked.
* Must only be used from your error activity.
*
* @param activity The current error activity. Must not be null.
* @param intent The Intent. Must not be null.
* @param config The config object as obtained by calling getConfigFromIntent.
*/
public static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CrashReporterConfig config) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
if (intent.getComponent() != null) {
// If the class name has been set, we force it to simulate a Launcher launch.
// If we don't do this, if you restart from the error activity, then press home,
// and then launch the activity from the launcher, the main activity appears twice on the backstack.
// This will most likely not have any detrimental effect because if you set the Intent component,
// if will always be launched regardless of the actions specified here.
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
if (config.getEventListener() != null) {
try {
config.getEventListener().onRestartAppFromErrorActivity();
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while invoking the event listener's onRestartAppFromErrorActivity. Please check your implementation.", t);
}
}
activity.finish();
activity.startActivity(intent);
killCurrentProcess();
}
public static void restartApplication(@NonNull Activity activity, @NonNull CrashReporterConfig config) {
Intent intent = new Intent(activity, config.getRestartActivityClass());
restartApplicationWithIntent(activity, intent, config);
}
/**
* Closes the app.
* If an event listener is provided, the close app event is invoked.
* Must only be used from your error activity.
*
* @param activity The current error activity. Must not be null.
* @param config The config object as obtained by calling getConfigFromIntent.
*/
public static void closeApplication(@NonNull Activity activity, @NonNull CrashReporterConfig config) {
if (config.getEventListener() != null) {
try {
config.getEventListener().onCloseAppFromErrorActivity();
} catch (Throwable t) {
Log.e(TAG, "An unknown error occurred while invoking the event listener's onCloseAppFromErrorActivity. Please check your implementation.", t);
}
}
activity.finish();
killCurrentProcess();
}
// INTERNAL METHODS NOT TO BE USED BY THIRD PARTIES
/**
* INTERNAL method that returns the current configuration of the library.
* If you want to check the config, use CaocConfig.Builder.get();
*
* @return the current configuration
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@NonNull
public static CrashReporterConfig getConfig() {
return config;
}
/**
* INTERNAL method that sets the configuration of the library.
* You must not use this, use CaocConfig.Builder.apply()
*
* @param config the configuration to use
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void setConfig(@NonNull CrashReporterConfig config) {
CrashReporter.config = config;
}
/**
* INTERNAL method that checks if the stack trace that just crashed is conflictive. This is true in the following scenarios:
* - The application has crashed while initializing (handleBindApplication is in the stack)
* - The crash occurred inside the "error_activity" process
*
* @param throwable The throwable from which the stack trace will be checked
* @param activityClass The activity class to launch when the app crashes
* @return true if this stack trace is conflictive and the activity must not be launched, false otherwise
*/
private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwable, @NonNull Class<? extends Activity> activityClass) {
String process;
try {
BufferedReader br = new BufferedReader(new FileReader("/proc/self/cmdline"));
process = br.readLine().trim();
br.close();
} catch (IOException e) {
process = null;
}
if (process != null && process.endsWith(":error_activity")) {
// Error happened in the error activity process - conflictive, so use default handler
return true;
}
do {
StackTraceElement[] stackTrace = throwable.getStackTrace();
for (StackTraceElement element : stackTrace) {
if (element.getClassName().equals("android.app.ActivityThread") && element.getMethodName().equals("handleBindApplication")) {
return true;
}
}
} while ((throwable = throwable.getCause()) != null);
return false;
}
/**
* INTERNAL method that returns the build date of the current APK as a string, or null if unable to determine it.
*
* @param context A valid context. Must not be null.
* @param dateFormat DateFormat to use to convert from Date to String
* @return The formatted date, or "Unknown" if unable to determine it.
*/
@Nullable
private static String getBuildDateAsString(@NonNull Context context, @NonNull DateFormat dateFormat) {
long buildDate;
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
// If this failed, try with the old zip method
ZipEntry ze = zf.getEntry("classes.dex");
buildDate = ze.getTime();
zf.close();
} catch (Exception e) {
buildDate = 0;
}
if (buildDate > 631152000000L) {
return dateFormat.format(new Date(buildDate));
} else {
return null;
}
}
/**
* INTERNAL method that returns the version name of the current app, or null if unable to determine it.
*
* @param context A valid context. Must not be null.
* @return The version name, or "Unknown if unable to determine it.
*/
@NonNull
private static String getVersionName(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (Exception e) {
return "Unknown";
}
}
/**
* INTERNAL method that returns the device model name with correct capitalization.
* Taken from: http://stackoverflow.com/a/12707479/1254846
*
* @return The device model name (i.e., "LGE Nexus 5")
*/
@NonNull
private static String getDeviceModelName() {
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return capitalize(model);
} else {
return capitalize(manufacturer) + " " + model;
}
}
/**
* INTERNAL method that capitalizes the first character of a string
*
* @param s The string to capitalize
* @return The capitalized string
*/
@NonNull
private static String capitalize(@Nullable String s) {
if (s == null || s.length() == 0) {
return "";
}
char first = s.charAt(0);
if (Character.isUpperCase(first)) {
return s;
} else {
return Character.toUpperCase(first) + s.substring(1);
}
}
/**
* INTERNAL method used to guess which activity must be called from the error activity to restart the app.
* It will first get activities from the AndroidManifest with intent filter <action android:name="com.tungsten.fcllibrary.crash.RESTART" />,
* if it cannot find them, then it will get the default launcher.
* If there is no default launcher, this returns null.
*
* @param context A valid context. Must not be null.
* @return The guessed restart activity class, or null if no suitable one is found
*/
@Nullable
private static Class<? extends Activity> guessRestartActivityClass(@NonNull Context context) {
Class<? extends Activity> resolvedActivityClass;
// If action is defined, use that
resolvedActivityClass = getRestartActivityClassWithIntentFilter(context);
// Else, get the default launcher activity
if (resolvedActivityClass == null) {
resolvedActivityClass = getLauncherActivity(context);
}
return resolvedActivityClass;
}
/**
* INTERNAL method used to get the first activity with an intent-filter <action android:name="com.tungsten.fcllibrary.crash.RESTART" />,
* If there is no activity with that intent filter, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getRestartActivityClassWithIntentFilter(@NonNull Context context) {
Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos.size() > 0) {
ResolveInfo resolveInfo = resolveInfos.get(0);
try {
return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);
} catch (ClassNotFoundException e) {
// Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the restart activity class via intent filter, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method used to get the default launcher activity for the app.
* If there is no launchable activity, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getLauncherActivity(@NonNull Context context) {
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
if (intent != null && intent.getComponent() != null) {
try {
return (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());
} catch (ClassNotFoundException e) {
// Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method used to guess which error activity must be called when the app crashes.
* It will first get activities from the AndroidManifest with intent filter <action android:name="com.tungsten.fcllibrary.crash.ERROR" />,
* if it cannot find them, then it will use the default error activity.
*
* @param context A valid context. Must not be null.
* @return The guessed error activity class, or the default error activity if not found
*/
@NonNull
private static Class<? extends Activity> guessErrorActivityClass(@NonNull Context context) {
Class<? extends Activity> resolvedActivityClass;
// If action is defined, use that
resolvedActivityClass = getErrorActivityClassWithIntentFilter(context);
// Else, get the default error activity
if (resolvedActivityClass == null) {
resolvedActivityClass = CrashReportActivity.class;
}
return resolvedActivityClass;
}
/**
* INTERNAL method used to get the first activity with an intent-filter <action android:name="com.tungsten.fcllibrary.crash.ERROR" />,
* If there is no activity with that intent filter, this returns null.
*
* @param context A valid context. Must not be null.
* @return A valid activity class, or null if no suitable one is found
*/
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Activity> getErrorActivityClassWithIntentFilter(@NonNull Context context) {
Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName());
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,
PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos.size() > 0) {
ResolveInfo resolveInfo = resolveInfos.get(0);
try {
return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);
} catch (ClassNotFoundException e) {
// Should not happen, print it to the log!
Log.e(TAG, "Failed when resolving the error activity class via intent filter, stack trace follows!", e);
}
}
return null;
}
/**
* INTERNAL method that kills the current process.
* It is used after restarting or killing the app.
*/
private static void killCurrentProcess() {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
/**
* INTERNAL method that stores the last crash timestamp
*
* @param timestamp The current timestamp.
*/
@SuppressLint("ApplySharedPref") // This must be done immediately since we are killing the app
private static void setLastCrashTimestamp(@NonNull Context context, long timestamp) {
context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit().putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit();
}
/**
* INTERNAL method that gets the last crash timestamp
*
* @return The last crash timestamp, or -1 if not set.
*/
private static long getLastCrashTimestamp(@NonNull Context context) {
return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1);
}
/**
* INTERNAL method that tells if the app has crashed in the last seconds.
* This is used to avoid restart loops.
*
* @return true if the app has crashed in the last seconds, false otherwise.
*/
private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) {
long lastTimestamp = getLastCrashTimestamp(context);
long currentTimestamp = new Date().getTime();
return (lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < config.getMinTimeBetweenCrashesMs());
}
/**
* Interface to be called when events occur, so they can be reported
* by the app as, for example, Google Analytics events.
*/
public interface EventListener extends Serializable {
void onLaunchErrorActivity();
void onRestartAppFromErrorActivity();
void onCloseAppFromErrorActivity();
}
/**
* Interface to be called to collect additional crash data when a crash occurs.
*/
public interface CustomCrashDataCollector extends Serializable {
String onCrash();
}
}

View File

@ -0,0 +1,337 @@
package com.tungsten.fcllibrary.crash;
import android.app.Activity;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Modifier;
public class CrashReporterConfig implements Serializable {
@IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT})
@Retention(RetentionPolicy.SOURCE)
private @interface BackgroundMode {
//I hate empty blocks
}
public static final int BACKGROUND_MODE_SILENT = 0;
public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1;
public static final int BACKGROUND_MODE_CRASH = 2;
private int backgroundMode = BACKGROUND_MODE_SHOW_CUSTOM;
private boolean enabled = true;
private boolean showErrorDetails = true;
private boolean showRestartButton = true;
private boolean logErrorOnRestart = true;
private boolean trackActivities = false;
private int minTimeBetweenCrashesMs = 3000;
private Integer errorDrawable = null;
private Class<? extends Activity> errorActivityClass = null;
private Class<? extends Activity> restartActivityClass = null;
private CrashReporter.CustomCrashDataCollector customCrashDataCollector = null;
private CrashReporter.EventListener eventListener = null;
@BackgroundMode
public int getBackgroundMode() {
return backgroundMode;
}
public void setBackgroundMode(@BackgroundMode int backgroundMode) {
this.backgroundMode = backgroundMode;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isShowErrorDetails() {
return showErrorDetails;
}
public void setShowErrorDetails(boolean showErrorDetails) {
this.showErrorDetails = showErrorDetails;
}
public boolean isShowRestartButton() {
return showRestartButton;
}
public void setShowRestartButton(boolean showRestartButton) {
this.showRestartButton = showRestartButton;
}
public boolean isLogErrorOnRestart() {
return logErrorOnRestart;
}
public void setLogErrorOnRestart(boolean logErrorOnRestart) {
this.logErrorOnRestart = logErrorOnRestart;
}
public boolean isTrackActivities() {
return trackActivities;
}
public void setTrackActivities(boolean trackActivities) {
this.trackActivities = trackActivities;
}
public int getMinTimeBetweenCrashesMs() {
return minTimeBetweenCrashesMs;
}
public void setMinTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
this.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
}
@Nullable
@DrawableRes
public Integer getErrorDrawable() {
return errorDrawable;
}
public void setErrorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
this.errorDrawable = errorDrawable;
}
@Nullable
public Class<? extends Activity> getErrorActivityClass() {
return errorActivityClass;
}
public void setErrorActivityClass(@Nullable Class<? extends Activity> errorActivityClass) {
this.errorActivityClass = errorActivityClass;
}
@Nullable
public CrashReporter.CustomCrashDataCollector getCustomCrashDataCollector() {
return customCrashDataCollector;
}
public void setCustomCrashDataCollector(@Nullable CrashReporter.CustomCrashDataCollector collector) {
this.customCrashDataCollector = collector;
}
@Nullable
public Class<? extends Activity> getRestartActivityClass() {
return restartActivityClass;
}
public void setRestartActivityClass(@Nullable Class<? extends Activity> restartActivityClass) {
this.restartActivityClass = restartActivityClass;
}
@Nullable
public CrashReporter.EventListener getEventListener() {
return eventListener;
}
public void setEventListener(@Nullable CrashReporter.EventListener eventListener) {
this.eventListener = eventListener;
}
public static class Builder {
private CrashReporterConfig config;
@NonNull
public static Builder create() {
Builder builder = new Builder();
CrashReporterConfig currentConfig = CrashReporter.getConfig();
CrashReporterConfig config = new CrashReporterConfig();
config.backgroundMode = currentConfig.backgroundMode;
config.enabled = currentConfig.enabled;
config.showErrorDetails = currentConfig.showErrorDetails;
config.showRestartButton = currentConfig.showRestartButton;
config.logErrorOnRestart = currentConfig.logErrorOnRestart;
config.trackActivities = currentConfig.trackActivities;
config.minTimeBetweenCrashesMs = currentConfig.minTimeBetweenCrashesMs;
config.errorDrawable = currentConfig.errorDrawable;
config.errorActivityClass = currentConfig.errorActivityClass;
config.customCrashDataCollector = currentConfig.customCrashDataCollector;
config.restartActivityClass = currentConfig.restartActivityClass;
config.eventListener = currentConfig.eventListener;
builder.config = config;
return builder;
}
/**
* Defines if the error activity must be launched when the app is on background.
* BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM: launch the error activity when the app is in background,
* BackgroundMode.BACKGROUND_MODE_CRASH: launch the default system error when the app is in background,
* BackgroundMode.BACKGROUND_MODE_SILENT: crash silently when the app is in background,
* The default is BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM (the app will be brought to front when a crash occurs).
*/
@NonNull
public Builder backgroundMode(@BackgroundMode int backgroundMode) {
config.backgroundMode = backgroundMode;
return this;
}
/**
* Defines if CustomActivityOnCrash crash interception mechanism is enabled.
* Set it to true if you want CustomActivityOnCrash to intercept crashes,
* false if you want them to be treated as if the library was not installed.
* The default is true.
*/
@NonNull
public Builder enabled(boolean enabled) {
config.enabled = enabled;
return this;
}
/**
* Defines if the error activity must shown the error details button.
* Set it to true if you want to show the full stack trace and device info,
* false if you want it to be hidden.
* The default is true.
*/
@NonNull
public Builder showErrorDetails(boolean showErrorDetails) {
config.showErrorDetails = showErrorDetails;
return this;
}
/**
* Defines if the error activity should show a restart button.
* Set it to true if you want to show a restart button,
* false if you want to show a close button.
* Note that even if restart is enabled but you app does not have any launcher activities,
* a close button will still be used by the default error activity.
* The default is true.
*/
@NonNull
public Builder showRestartButton(boolean showRestartButton) {
config.showRestartButton = showRestartButton;
return this;
}
/**
* Defines if the stack trace must be logged again once the custom activity is shown.
* Set it to true if you want to log the stack trace again,
* false if you don't want the extra logging.
* This option exists because the default Android Studio logcat view only shows the output
* of the current process, and since the error activity runs on a new process,
* you can't see the previous output easily.
* Internally, it's logged when getConfigFromIntent() is called.
* The default is true.
*/
@NonNull
public Builder logErrorOnRestart(boolean logErrorOnRestart) {
config.logErrorOnRestart = logErrorOnRestart;
return this;
}
/**
* Defines if the activities visited by the user should be tracked
* so they are reported when an error occurs.
* The default is false.
*/
@NonNull
public Builder trackActivities(boolean trackActivities) {
config.trackActivities = trackActivities;
return this;
}
/**
* Defines the time that must pass between app crashes to determine that we are not
* in a crash loop. If a crash has occurred less that this time ago,
* the error activity will not be launched and the system crash screen will be invoked.
* The default is 3000.
*/
@NonNull
public Builder minTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {
config.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;
return this;
}
/**
* Defines which drawable to use in the default error activity image.
* Set this if you want to use an image other than the default one.
* The default is R.drawable.customactivityoncrash_error_image (a cute upside-down bug).
*/
@NonNull
public Builder errorDrawable(@Nullable @DrawableRes Integer errorDrawable) {
config.errorDrawable = errorDrawable;
return this;
}
/**
* Sets the error activity class to launch when a crash occurs.
* If null, the default error activity will be used.
*/
@NonNull
public Builder errorActivity(@Nullable Class<? extends Activity> errorActivityClass) {
config.errorActivityClass = errorActivityClass;
return this;
}
/**
* Sets the main activity class that the error activity must launch when a crash occurs.
* If not set or set to null, the default launch activity will be used.
* If your app has no launch activities and this is not set, the default error activity will close instead.
*/
@NonNull
public Builder restartActivity(@Nullable Class<? extends Activity> restartActivityClass) {
config.restartActivityClass = restartActivityClass;
return this;
}
/**
* Sets an event listener to be called when events occur, so they can be reported
* by the app as, for example, Google Analytics events.
* If not set or set to null, no events will be reported.
*
* @param eventListener The event listener.
* @throws IllegalArgumentException if the eventListener is an inner or anonymous class
*/
@NonNull
public Builder eventListener(@Nullable CrashReporter.EventListener eventListener) {
if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) {
throw new IllegalArgumentException("The event listener cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.");
} else {
config.eventListener = eventListener;
}
return this;
}
/**
* Sets the custom data collector class to invoke when a crash occurs.
* If not set or set to null, no custom data will be collected.
*
* @param collector The custom data collector.
* @throws IllegalArgumentException if the collector is an inner or anonymous class
*/
@NonNull
public Builder customCrashDataCollector(@Nullable CrashReporter.CustomCrashDataCollector collector) {
if (collector != null && collector.getClass().getEnclosingClass() != null && !Modifier.isStatic(collector.getClass().getModifiers())) {
throw new IllegalArgumentException("The custom data collector cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.");
} else {
config.customCrashDataCollector = collector;
}
return this;
}
@NonNull
public CrashReporterConfig get() {
return config;
}
public void apply() {
CrashReporter.setConfig(config);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.tungsten.fcllibrary.component.view.FCLTitleView
android:id="@+id/title"
app:title="@string/crash_reporter_title"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0"
app:layout_constraintWidth_percent="0.5"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintWidth_percent="0.9"
app:layout_constraintHeight_percent="0.7"
app:layout_constraintBottom_toTopOf="@id/copy"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title">
<com.tungsten.fcllibrary.component.view.FCLTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/error"/>
</ScrollView>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/restart"
android:text="@string/crash_reporter_restart"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.2"
app:layout_constraintVertical_bias="1"
app:layout_constraintWidth_percent="0.15"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/close"
android:text="@string/crash_reporter_close"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.4"
app:layout_constraintVertical_bias="1"
app:layout_constraintWidth_percent="0.15"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/copy"
android:text="@string/crash_reporter_copy"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.6"
app:layout_constraintVertical_bias="1"
app:layout_constraintWidth_percent="0.15"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.tungsten.fcllibrary.component.view.FCLButton
android:id="@+id/share"
android:text="@string/crash_reporter_share"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintHorizontal_bias="0.8"
app:layout_constraintVertical_bias="1"
app:layout_constraintWidth_percent="0.15"
app:layout_constraintHeight_percent="0.12"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FCLButton">
<attr name="shape" format="integer"/>
</declare-styleable>
<declare-styleable name="FCLImageButton">
<attr name="auto_tint" format="boolean"/>
</declare-styleable>
<declare-styleable name="FCLTextView">
<attr name="auto_text_tint" format="boolean"/>
</declare-styleable>
<declare-styleable name="FCLTitleView">
<attr name="title" format="string" localization="suggested" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="file_browser_provider" translatable="false">com.tungsten.fcl.provider</string>
<string name="crash_reporter_title">Crash Reporter</string>
<string name="crash_reporter_hint">Fold Craft Launcher has encountered an unresolvable error, please seek help from the developer or others.</string>
<string name="crash_reporter_restart">Restart</string>
<string name="crash_reporter_close">Close</string>
<string name="crash_reporter_copy">Copy</string>
<string name="crash_reporter_share">Share</string>
<string name="crash_reporter_toast">Copy successfully!</string>
</resources>

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@ -1,4 +1,4 @@
package com.tungsten.fclcore.constant;
package com.tungsten.fclauncher;
import android.content.Context;

View File

@ -19,3 +19,4 @@ rootProject.name = "Fold Craft Launcher"
include ':FCL'
include ':FCLCore'
include ':FCLauncher'
include ':FCLLibrary'