add a crash reporter & add some custom theme view & upload assets
This commit is contained in:
parent
78effa6543
commit
18aa63e017
|
@ -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'
|
||||
|
|
|
@ -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>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package com.tungsten.fcl.activity;
|
||||
|
||||
public class SplashActivity {
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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'
|
||||
}
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.tungsten.fclcore.constant;
|
||||
package com.tungsten.fclauncher;
|
||||
|
||||
import android.content.Context;
|
||||
|
|
@ -19,3 +19,4 @@ rootProject.name = "Fold Craft Launcher"
|
|||
include ':FCL'
|
||||
include ':FCLCore'
|
||||
include ':FCLauncher'
|
||||
include ':FCLLibrary'
|
||||
|
|
Loading…
Reference in New Issue