134 Commits
1.0.1 ... 1.2

Author SHA1 Message Date
82946e55fe Update version to 1.2 2023-01-23 12:19:06 +08:00
910b3523a3 Update copyright date to 2023 for all existing files 2023-01-23 12:13:50 +08:00
a6fd0b699c Modify remove scrollbar and reset scroll view when view changed in AppErrorsDetailActivity, activity_app_errors_detail 2023-01-23 11:59:35 +08:00
6ddbfb5444 Added check for updates feature from GitHub Release 2023-01-23 11:43:55 +08:00
fe4f91291e Added i18n strings 2023-01-23 11:42:04 +08:00
5e0336a434 Added disable automatic wrapping error stack trace contents function in ConfigData, AppErrorsDetailActivity, activity_app_errors_detail 2023-01-22 15:51:31 +08:00
55a4d68a18 Added i18n strings 2023-01-22 15:49:31 +08:00
358ac848e2 Modify remove screenOrientation parameter for AppErrorsDetailActivity in AndroidManifest 2023-01-22 15:32:18 +08:00
567d0a7b6c Modify add cpuAbi, versionName, versionCode default value for legacy data transfer of AppErrorsInfoBean in AppErrorsRecordData 2023-01-22 14:55:16 +08:00
4d5dadae8b Modify make crashed apps info persistence and add more info to stackOutputShareContent, stackOutputFileContent in AppErrorsInfoBean, FrameworkHooker, AppErrorsDetailActivity 2023-01-22 14:51:15 +08:00
9dce54f326 Added appVersionNameOf, appVersionCodeOf functions and merge to appVersionBrandOf function in FunctionFactory 2023-01-22 14:23:41 +08:00
37af877a7d Modify change the judgment logic when fetched installed packages list is empty in FrameworkHooker 2023-01-22 14:11:05 +08:00
59f5b27d19 Modify change view id "app_api" to "app_cpu_abi" in AppErrorsDetailActivity, activity_app_errors_detail 2023-01-22 14:05:08 +08:00
1299779868 Modify change apps config filters function with 3 types such as user apps, system apps and all apps 2023-01-22 13:05:05 +08:00
27c5e92879 Added i18n strings and remove some unused translations 2023-01-22 13:03:02 +08:00
d22745f2ea Modify make radio button singleLine in dia_app_config 2023-01-22 12:59:38 +08:00
dfcee71168 Modify add @throws code note in putAppShowingType function in AppErrorsConfigData 2023-01-22 11:49:37 +08:00
5b30499cc3 Update YukiHookAPI 2023-01-21 00:53:45 +08:00
1889b848b0 Update Android Gradle Plugin to 7.4.0 2023-01-21 00:49:40 +08:00
c349ee5dc7 Added global app config template function and remove old implementations 2023-01-20 03:09:25 +08:00
72e99bd4f2 Added i18n strings and fix some translations 2023-01-20 02:59:15 +08:00
2d9550d5b9 Modify remove fetch installed packages list log and only print for empty list in FrameworkHooker 2023-01-20 02:33:25 +08:00
27e068898e Fix translations for i18n strings 2023-01-19 23:59:28 +08:00
92a0591d5b Update Gradle dependencies 2023-01-19 22:29:12 +08:00
2a97dcbc06 Added access root failed tips dialog in FrameworkTool 2023-01-19 21:53:58 +08:00
0dc12ae3e6 Added i18n strings 2023-01-19 21:53:51 +08:00
075850d239 Modify remove @Keep in all beans and add @SerializedName in AppErrorsInfoBean 2023-01-19 13:07:26 +08:00
c48a23f07d Modify change empty mark for AppErrorsInfoBean in AppErrorsInfoBean, FrameworkHooker 2023-01-19 12:57:02 +08:00
cf1796b7cb Modify optimize code in FrameworkHooker 2023-01-17 13:45:16 +08:00
5bb1f38146 Modify ignored "android" package name when getting app list data in FrameworkHooker 2023-01-17 13:13:21 +08:00
a055d7f53d Modify rename ui/view to ui/widget 2023-01-17 11:15:48 +08:00
2eae45a640 Modify change the way of getting app list data in FrameworkHooker 2023-01-17 05:12:23 +08:00
fd168f8810 Modify change multi-user app display name with suffix its user id in FrameworkHooker 2023-01-17 05:02:15 +08:00
37580519db Modify change appNameOf function returned default blank content to "unknown" in FunctionFactory 2023-01-17 04:56:37 +08:00
5144494f82 Modify make app errors record files sorted by last modified date in AppErrorsRecordData 2023-01-17 04:35:47 +08:00
01a6c1ffa5 Fix the previous fix caused the text could not be selected in AppErrorsDetailActivity, activity_app_errors_detail 2023-01-17 03:59:48 +08:00
2aaa422a56 Modify change AppErrorsData to AppErrorsProcessData in FrameworkHooker 2023-01-17 03:22:37 +08:00
7c0c1754e9 Modify merge to new way to save and read the app errors record data 2023-01-17 03:17:37 +08:00
587c718d0a Added List.toArrayList function in FunctionFactory 2023-01-17 03:03:27 +08:00
6b2b538047 Added Any?.toJsonOrNull, String.toEntityOrNull functions in GsonFormatFactory 2023-01-17 01:37:08 +08:00
6a5fee830c Fix code style in build.gradle 2023-01-16 23:36:20 +08:00
1d0cc1d24f Fix the central color problem of views such as CheckBox 2023-01-16 22:40:26 +08:00
eeaf386635 Fix the interface automatically slides up problem on Android versions lower than 10 in activity_app_errors_detail 2023-01-16 22:21:21 +08:00
a15b5b008d Added "Go It Now" button in app errors dialog for unable get app errors record in AppErrorsDetailActivity 2023-01-16 22:20:02 +08:00
37962fa12f Modify merge app errors functions implementation code to AppErrorsData in FrameworkHooker 2023-01-15 14:35:06 +08:00
3798479c23 Fix crashed apps user id mismatch problem in FrameworkHooker, AppErrorsInfoBean 2023-01-15 04:12:50 +08:00
a214f5773f Fix the app first crash report not responded problem in some customize ROMs in FrameworkHooker 2023-01-15 03:57:41 +08:00
aeda0f183e Modify support Android 7.0 2023-01-15 02:39:03 +08:00
c6d5f07b8c Fix PackageList class not exist problem in Android 8.1 and fix app errors dialog no show problem in FrameworkHooker 2023-01-15 02:37:16 +08:00
5991d976b9 Modify optimize code format in MainActivity 2023-01-15 01:35:02 +08:00
3fb4e4f375 Fix no onCreate method in AppErrorDialog class problem in Android 10 in FrameworkHooker 2023-01-14 01:55:52 +08:00
8c4a1ea5f0 Modify make HookEntry singleton 2023-01-14 01:55:29 +08:00
7749cb9aeb Modify merge to YukiHookAPI new usage 2023-01-14 01:55:22 +08:00
9d9cb473e8 Update Gradle & Kotlin
- Update Kotlin version to 1.7.22
- Update Gradle version to 7.6
- Update Gradle dependencies
2023-01-14 01:28:08 +08:00
afeb16e69d Update YukiHookAPI 2023-01-14 01:24:19 +08:00
211343a6e7 Modify remove "contains", "replace" method's param name statement 2023-01-14 01:22:16 +08:00
2c0cfb6863 Modify add release channel description, release status description in README 2022-11-26 00:12:48 +08:00
15293950a9 Modify change action file name for ci 2022-11-25 23:32:43 +08:00
88fe23ab9a Modify change action name for ci 2022-11-25 23:29:03 +08:00
Fankesyooni
150d2e8aa5 Merge pull request #24 from KitsunePie/ci
Upgrade ci deps
2022-11-14 13:13:57 +08:00
Howard Wu
dd6e971a34 gradlew chmod +x 2022-11-14 12:13:50 +08:00
Howard Wu
051da4df5f Update push_ci.yml 2022-11-14 12:10:41 +08:00
Howard Wu
368a4b347f Upgrade ci deps 2022-11-14 12:08:46 +08:00
30b92770e9 Update Gradle & PlatformSDK
- Update Android Gradle Plugin version to 7.3.1
- Update Kotlin version to 1.7.20
- Update YukiHookAPI version to 1.1.4 in demo-app
2022-10-20 00:18:46 +08:00
Fankesyooni
2491547e3e Merge pull request #10 from cracky5322/master
Update Traditional Chinese
2022-10-12 12:00:25 +08:00
Jia-Bin
182a65255f Update Traditional Chinese
Better quality and beautiful localization translation optimization
2022-10-12 11:42:17 +08:00
aef2e0814a Fix file naming bug 2022-10-05 09:57:39 +08:00
e5062d3947 Update version to 1.1 2022-10-05 07:03:16 +08:00
1a1856ce9e Added Microsoft App Center analytics 2022-10-05 06:45:49 +08:00
4ae7fff484 Added i18n strings 2022-10-05 06:44:33 +08:00
1f29ac1bba Update .gitignore 2022-10-05 05:51:46 +08:00
92f6837c30 Added debug log viewing function 2022-10-05 04:36:55 +08:00
a44d29e102 Added i18n strings 2022-10-05 04:35:52 +08:00
6fbaf6d7a2 Modify add debug log and change crash log in FrameworkHooker 2022-10-05 04:32:58 +08:00
5320ce1b0e Modify change ListView padding bottom in activity_app_errors_muted, activity_app_errors_record, activity_config 2022-10-05 03:00:56 +08:00
2b317070a2 Fix when "proc" field got null System Framework maybe crashed in FrameworkHooker 2022-10-05 02:22:36 +08:00
f7784b393d Update YukiHookAPI 2022-10-04 07:31:50 +08:00
8eb814a345 Modify change app errors log's "App" to "Application" in FrameworkHooker 2022-10-04 03:47:52 +08:00
d350944f0d Modify change Context.openApp function command "am start ..." to system startActivityAsUser function in FunctionFactory 2022-10-04 03:03:52 +08:00
56b3656bf2 Fix code style in FunctionFactory 2022-10-04 02:38:50 +08:00
332cf0d3c0 Fix tip text not full width in activity_main in demo-app 2022-10-04 02:04:58 +08:00
0974c38d76 Fix destroyed Activity reading list data maybe out of bounds or called adapter's data confusion error in ConfigureActivity 2022-10-04 00:49:56 +08:00
a35dcfed66 Added system version text click notice dialog in MainActivity 2022-10-03 22:25:29 +08:00
5944842c8e Fix English translation for i18n strings 2022-10-03 22:21:01 +08:00
0b39bd9865 Modify move unable read errors data on-time tip to AppErrorsDetailActivity 2022-10-03 21:29:09 +08:00
d194da21ca Update i18n strings 2022-10-03 21:28:07 +08:00
777af500d4 Modify change missing output log's timestamp file name to UTC time file name 2022-10-03 08:04:13 +08:00
2472f8b7e5 Fix some custom system can't read application crash info on-time will get wrong errors data problem 2022-10-03 07:19:13 +08:00
1153494dfa Added pid showing for log in FrameworkHooker 2022-10-03 07:05:02 +08:00
95ee9de477 Added i18n strings 2022-10-03 06:40:11 +08:00
fe94441e9a Modify allowed multi-user app errors dialog's "Reopen App" option to start Activity with correct user id 2022-10-03 06:15:42 +08:00
3156a1721e Modify change Context.openApp function can open multi-user's Activity 2022-10-03 06:11:49 +08:00
9948a3fbc1 Fix UTC time displayed directly on UI 2022-10-03 05:44:34 +08:00
2481263c00 Added multi-user display app's user id feature 2022-10-03 05:43:49 +08:00
a6bf4e8a80 Added i18n strings 2022-10-03 05:33:29 +08:00
64e54348f4 Added INTERACT_ACROSS_USERS permission in AndroidManifest 2022-10-03 04:41:17 +08:00
33ee056ed9 Fix app errors record's current Context may not has INTERACT_ACROSS_USERS permission problem 2022-10-03 04:40:46 +08:00
1fc9f07b9f Added Material 3 dynamic colors theme for app errors dialog 2022-10-03 04:09:54 +08:00
499e7d9296 Added Resources.colorOf function in FunctionFactory 2022-10-03 04:02:53 +08:00
f2311f31cb Added i18n strings 2022-10-03 03:39:18 +08:00
b9f52b4e67 Added dynamic colors Material 3 theme for Translucent 2022-10-03 03:17:42 +08:00
c272dbc109 Modify code notes in ConfigData 2022-10-03 03:15:10 +08:00
4c47cbb271 Added isDisableMaterial3 function in DialogBuilderFactory 2022-10-03 03:08:14 +08:00
3de258f95d Update i18n strings 2022-10-03 02:48:06 +08:00
b173d3bfed Modify make app errors records data to persistent storage 2022-10-03 02:41:32 +08:00
310bc2c9dc Added Gson format function 2022-10-03 02:37:08 +08:00
af0d29a8f4 Modify merge thread to thread pool in ConfigureActivity, AppErrorsRecordActivity 2022-10-03 02:35:04 +08:00
4db70d02ec Added thread pool function 2022-10-03 02:34:16 +08:00
167199ba34 Added getResolverString / putResolverString function in ConfigData 2022-10-03 02:19:35 +08:00
98f22d6bca Update proguard-rules.pro 2022-10-03 01:32:27 +08:00
8f09a32d22 Added Gson in Gradle dependencies 2022-10-03 01:31:32 +08:00
f14b7d8f20 Added @Keep to data beans for R8 2022-10-03 01:29:06 +08:00
d38474e082 Modify merge all png elements to svg elements 2022-10-03 01:17:10 +08:00
5c1b8d3d6a Modify change icon to svg in activity_main 2022-10-02 23:11:07 +08:00
68dff21c42 Modify format code style in FrameworkHooker 2022-10-02 02:19:17 +08:00
1014f42584 Modify change code style in AppErrorsInfoBean 2022-10-02 00:52:58 +08:00
1a57a331b9 Modify change timestamp displayed text to UTC time in AppErrorsInfoBean, AppErrorsRecordActivity 2022-10-02 00:50:15 +08:00
bfb5e037e5 Added Long.toUtcTime function in FunctionFactory 2022-10-02 00:50:15 +08:00
1862faf637 Modify merge DataConst to ConfigData and move DataFactory to data/factory 2022-10-02 00:25:16 +08:00
abf6103b9d Added system locale display info on stack output content in AppErrorsInfoBean 2022-10-01 03:57:18 +08:00
c47d356e4c Modify replace app errors info's display text "null" to "unknown" in AppErrorsInfoBean 2022-10-01 03:57:18 +08:00
e8990f50f9 Fix add a scroll view to resolve app errors dialog bottom occlusion problem 2022-10-01 03:48:14 +08:00
a1d2d7ddf0 Added scroll view in MainActivity and remove rotation lock on demo-app 2022-10-01 03:40:12 +08:00
ec829a85af Added fast restart problem dialog in FrameworkTool 2022-10-01 03:32:52 +08:00
ce7a483106 Added i18n strings 2022-10-01 03:32:45 +08:00
8d64f9c989 Modify merge YukiHookAPI new usage and compatible with API 33 2022-10-01 03:32:23 +08:00
1973c1acaf Update Gradle & PlatformSDK 2022-10-01 00:18:09 +08:00
936a5b6802 Update YukiHookAPI 2022-10-01 00:15:14 +08:00
c5e9df257d Update .idea 2022-10-01 00:13:05 +08:00
57836f4a7f Added Project icon 2022-09-30 22:25:44 +08:00
2e09ab584d Update .gitignore 2022-09-30 22:25:05 +08:00
a9a7b5bf26 Added readme document language isolation 2022-07-26 23:50:05 +08:00
981e0ba746 Update Gradle & Kotlin & PlatformSDK
- Update Kotlin version to 1.7.10
- Update Gradle version
2022-07-20 23:13:53 +08:00
fed9fd3ae8 Merge dependencies 2022-07-20 02:14:39 +08:00
133 changed files with 3956 additions and 2776 deletions

View File

@@ -1,4 +1,4 @@
name: main
name: Automatic Build on Commit
on:
workflow_dispatch:
@@ -15,18 +15,20 @@ jobs:
name: Build CI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.12
uses: jwlawson/actions-setup-cmake@v1
with:
cmake-version: '3.22.1'
- name: Prepare Java 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
java-package: jdk
distribution: 'temurin'
cache: 'gradle'
- name: Cache Gradle Dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -36,15 +38,13 @@ jobs:
restore-keys: |
gradle-deps
- name: Cache Gradle Build
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.gradle/caches/build-cache-*
key: gradle-builds-core-${{ github.sha }}
restore-keys: |
gradle-builds
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: |
./gradlew :app:assembleRelease
@@ -52,12 +52,12 @@ jobs:
echo "APK_FILE=$(find app/build/outputs/apk/release -name '*.apk')" >> $GITHUB_ENV
echo "DEMO_APK_FILE=$(find demo-app/build/outputs/apk/release -name '*.apk')" >> $GITHUB_ENV
- name: Upload Artifacts(module)
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: ${{ env.APK_FILE }}
name: module-release
- name: Upload Artifacts(demo-app)
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: ${{ env.DEMO_APK_FILE }}
name: demo-release

View File

@@ -1,4 +1,4 @@
name: main
name: Pull Request Checker
on:
pull_request:
@@ -14,18 +14,20 @@ jobs:
name: Pull request check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.12
uses: jwlawson/actions-setup-cmake@v1
with:
cmake-version: '3.22.1'
- name: Prepare Java 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
java-package: jdk
distribution: 'temurin'
cache: 'gradle'
- name: Cache Gradle Dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@@ -35,15 +37,13 @@ jobs:
restore-keys: |
gradle-deps
- name: Cache Gradle Build
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.gradle/caches/build-cache-*
key: gradle-builds-core-${{ github.sha }}
restore-keys: |
gradle-builds
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: |
./gradlew :app:assembleRelease
@@ -51,12 +51,12 @@ jobs:
echo "APK_FILE=$(find app/build/outputs/apk/release -name '*.apk')" >> $GITHUB_ENV
echo "DEMO_APK_FILE=$(find demo-app/build/outputs/apk/release -name '*.apk')" >> $GITHUB_ENV
- name: Upload Artifacts(module)
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: ${{ env.APK_FILE }}
name: module-release
- name: Upload Artifacts(demo-app)
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: ${{ env.DEMO_APK_FILE }}
name: demo-release

10
.gitignore vendored
View File

@@ -1,7 +1,14 @@
# Project exclude paths
*.iml
.gradle
.secret
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
@@ -10,5 +17,4 @@
local.properties
/app/releaseHasController/
/app/debug/
/app/release/
/.idea/
/app/release/

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_29.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-05-19T02:00:42.222889Z" />
</component>
</project>

21
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/demo-app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

BIN
.idea/icon.png generated Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

48
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/drawable-night/bg_black_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable-night/bg_dark_round.xml" value="0.256" />
<entry key="app/src/main/res/drawable-night/bg_green_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable-night/bg_orange_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable-night/bg_yellow_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable/bg_black_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable/bg_dark_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable/bg_green_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable/bg_orange_round.xml" value="0.2425" />
<entry key="app/src/main/res/drawable/ic_debug.xml" value="0.2555" />
<entry key="app/src/main/res/drawable/ic_exception.xml" value="0.256" />
<entry key="app/src/main/res/drawable/ic_filter.xml" value="0.256" />
<entry key="app/src/main/res/drawable/ic_share.xml" value="0.241" />
<entry key="app/src/main/res/drawable/ic_statistics.xml" value="0.256" />
<entry key="app/src/main/res/layout/activity_app_errors_detail.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/activity_app_errors_display.xml" value="0.4359375" />
<entry key="app/src/main/res/layout/activity_app_errors_ignored.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/activity_app_errors_muted.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/activity_app_errors_record.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/activity_config.xml" value="0.4359375" />
<entry key="app/src/main/res/layout/activity_main.xml" value="0.4359375" />
<entry key="app/src/main/res/layout/adapter_app_errors_ignored.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/adapter_app_errors_muted.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/adapter_app_errors_record.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/adapter_app_info.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/dia_app_config.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/dia_app_errors_display.xml" value="0.4359375" />
<entry key="app/src/main/res/layout/dia_app_errors_statistics.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/layout/dia_apps_filter.xml" value="0.43697916666666664" />
<entry key="app/src/main/res/menu/menu_list_detail_action.xml" value="0.43697916666666664" />
<entry key="demo-app/src/main/res/layout/activity_main.xml" value="0.4083333333333333" />
<entry key="demo-app/src/main/res/layout/activity_multi_process.xml" value="0.4359375" />
</map>
</option>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

106
README-zh-CN.md Normal file
View File

@@ -0,0 +1,106 @@
# AppErrorsTracking
[![Blank](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/KitsunePie/AppErrorsTracking)
[![Blank](https://img.shields.io/badge/license-AGPL3.0-blue)](https://github.com/KitsunePie/AppErrorsTracking/blob/master/LICENSE)
[![Blank](https://img.shields.io/badge/version-v1.2-green)](https://github.com/KitsunePie/AppErrorsTracking/releases)
[![Blank](https://img.shields.io/github/downloads/KitsunePie/AppErrorsTracking/total?label=Release)](https://github.com/KitsunePie/AppErrorsTracking/releases)
[![Blank](https://img.shields.io/github/downloads/Xposed-Modules-Repo/com.fankes.apperrorstracking/total?label=LSPosed%20Repo&logo=Android&style=flat&labelColor=F48FB1&logoColor=ffffff)](https://github.com/Xposed-Modules-Repo/com.fankes.apperrorstracking/releases)
<br/><br/>
[English](https://github.com/KitsunePie/AppErrorsTracking/blob/master/README.md) | 简体中文
为原生 FC 对话框增加更多功能并修复国内定制 ROM 删除 FC 对话框的问题,给 Android 开发者带来更好的体验。
此项目为 Xposed 模块,可用在任何 Android 系统中,目前仅在 **LSPosed** 中测试通过。
此模块专为 Android 开发者而打造。
在可能的无法连接电脑,不能进行 ADB 调试的时候,可通过此模块来快速捕获任意已安装应用的任意异常,以便快速定位问题。
应用发生崩溃的错误日志对开发者来说是无价的财富,若你不是开发者,你依然可以安装此模块,以便给开发者提供更多异常信息快速解决问题。
> 最低支持 Android 7.0
## 项目缘由
我实在是不能理解,国内 ROM 除了 MIUI(稳定版除外) 都选择了删除应用程序崩溃的对话框(FC 对话框),我曾以为这一直是一个特性,直到我去反编译了系统框架,才确认确实是被删掉了。
难道产品经理认为,让用户看不到错误,应用直接闪退,逃避就是最好的解决方案吗,还是说**另有隐情**呢?
## 工作原理
不同于 `Thread.UncaughtExceptionHandler`,我们通过注入系统框架,使用原生方式全方位捕获应用异常,不会产生额外的注册监听,在性能上相比原始的异常监听会更好。
同时系统级别的异常捕获还可捕获原生层的 `stack trace`
## 注意事项
系统原生方式捕获的异常只能为 APP 自身未进行处理的异常,若 APP 自身拥有自定义的 `Thread.UncaughtExceptionHandler`
类似 **Bugly** 这样的自动收集异常功能,系统就无法获取到 APP 是否真正发生异常而闪退(FC),例如 **QQ**、**TIM**。
## 功能列表
- 完全取代系统的应用错误对话框
- 记录每个应用的异常,直到重新启动前持续保留
- 复制、分享、导出异常堆栈功能
- 异常历史记录功能,可通过通知栏磁贴“异常历史记录”进入和模块主界面进入
- 应用异常统计功能
- 多进程应用的异常显示功能
## 翻译贡献
欢迎为此项目做出贡献,将其翻译为您国家的语言。
## 发行渠道说明
- [Automatic Build on Commit](https://github.com/KitsunePie/AppErrorsTracking/actions/workflows/commit_ci.yml)
上述更新为代码 `commit` 后自动触发,具体更新内容可点击上方的文字前往 **Github Actions** 进行查看,本更新由开源的流程自动编译发布,**不保证其稳定性**,所发布的版本**仅供测试**,且不会特殊说明甚至可能会变更版本号或保持与当前稳定版相同的版本号。
- [Release](https://github.com/KitsunePie/AppErrorsTracking/releases)
- [Xposed-Modules-Repo](https://github.com/Xposed-Modules-Repo/com.fankes.apperrorstracking/releases)
上述更新为手动发布的稳定版,具体更新内容可点击上方的文字前往指定的发布页面查看,稳定版的更新将会同时发布到上述地址中,同步更新。
## 发行状态说明
![Blank](https://img.shields.io/badge/build-passing-brightgreen)
上述状态为当前稳定版与自动构建版本一致或当前代码改动与稳定版无功能差异。
![Blank](https://img.shields.io/badge/build-pending-dbab09)
上述状态为存在自动构建版本和新功能的更新但当前并未发布稳定版,处于预发行状态。
![Blank](https://img.shields.io/badge/build-problem-red)
上述状态为当前发行的稳定版可能存在严重问题但并未及时进行修复且并未发布稳定版。
## 许可证
- [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html)
```
Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
```
Powered by [YukiHookAPI](https://github.com/fankes/YukiHookAPI)
版权所有 © 2017-2023 Fankes Studio(qzmmcn@163.com)

View File

@@ -2,37 +2,25 @@
[![Blank](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/KitsunePie/AppErrorsTracking)
[![Blank](https://img.shields.io/badge/license-AGPL3.0-blue)](https://github.com/KitsunePie/AppErrorsTracking/blob/master/LICENSE)
[![Blank](https://img.shields.io/badge/version-v1.0.1-green)](https://github.com/KitsunePie/AppErrorsTracking/releases)
[![Blank](https://img.shields.io/badge/version-v1.2-green)](https://github.com/KitsunePie/AppErrorsTracking/releases)
[![Blank](https://img.shields.io/github/downloads/KitsunePie/AppErrorsTracking/total?label=Release)](https://github.com/KitsunePie/AppErrorsTracking/releases)
[![Blank](https://img.shields.io/github/downloads/Xposed-Modules-Repo/com.fankes.apperrorstracking/total?label=LSPosed%20Repo&logo=Android&style=flat&labelColor=F48FB1&logoColor=ffffff)](https://github.com/Xposed-Modules-Repo/com.fankes.apperrorstracking/releases)
<br/><br/>
English | [简体中文](https://github.com/KitsunePie/AppErrorsTracking/blob/master/README-zh-CN.md)
Added more features to app's errors dialog, fixed custom rom deleted dialog, the best experience to Android developer.
This project is an Xposed module that can be used in any Android system, currently only tested in **LSPosed**.
This module is specially designed for Android developers.
When it is possible that the computer cannot be connected and ADB cannot be performed, this module can be used to quickly capture any exception
of any installed apps, so as to quickly locate the problem.
When it is possible that the computer cannot be connected and ADB cannot be performed, this module can be used to quickly capture any errors of
any installed apps, so as to quickly locate the problem.
The error log of apps crashing is an invaluable asset for developers. If you are not a developer, you can still install this module to provide
developers with more exception information to quickly solve problems.
> Minimum support Android 8.1
**应用异常跟踪**
为原生 FC 对话框增加更多功能并修复国内定制 ROM 删除 FC 对话框的问题,给 Android 开发者带来更好的体验。
此项目为 Xposed 模块,可用在任何 Android 系统中,目前仅在 **LSPosed** 中测试通过。
此模块专为 Android 开发者而打造。
在可能的无法连接电脑,不能进行 ADB 调试的时候,可通过此模块来快速捕获任意已安装应用的任意异常,以便快速定位问题。
应用发生崩溃的错误日志对开发者来说是无价的财富,若你不是开发者,你依然可以安装此模块,以便给开发者提供更多异常信息快速解决问题。
> 最低支持 Android 8.1
> Minimum support Android 7.0
## Project Reason
@@ -42,12 +30,6 @@ dialog) of apps crashes. I thought this was always a feature until I decompiled
Does the product manager think that it is the best solution to let the user not see the error, and the apps will crash and exit directly, or is
there another **hidden secret**?
**项目缘由**
我实在是不能理解,国内 ROM 除了 MIUI(稳定版除外) 都选择了删除应用程序崩溃的对话框(FC 对话框),我曾以为这一直是一个特性,直到我去反编译了系统框架,才确认确实是被删掉了。
难道产品经理认为,让用户看不到错误,应用直接闪退,逃避就是最好的解决方案吗,还是说**另有隐情**呢?
## Woking Principle
Unlike `Thread.UncaughtExceptionHandler`, we use the native method to capture apps errors in all directions by injecting the system framework,
@@ -55,13 +37,13 @@ without generating additional registration monitoring, which is better than the
At the same time, the system-level exception capture can also capture the `stack trace` of the native platform.
**工作原理**
## Precautions
不同于 `Thread.UncaughtExceptionHandler`,我们通过注入系统框架,使用原生方式全方位捕获应用异常,不会产生额外的注册监听,在性能上相比原始的异常监听会更好。
The errors captured by the system natively can only be errors that are not handled by the apps itself. If the apps itself has a
custom `Thread.UncaughtExceptionHandler`
Similar to **Bugly** to automatically collect errors, the system cannot obtain whether the apps actually crashes **(FC)**.
同时系统级别的异常捕获还可捕获原生层的 `stack trace`
## Feature
## Features List
- Completely replaces the system's apps errors dialog
@@ -76,34 +58,53 @@ At the same time, the system-level exception capture can also capture the `stack
- Errors display function for multi-process apps
**功能**
- 完全取代系统的应用错误对话框
- 记录每个应用的异常,直到重新启动前持续保留
- 复制、分享、导出异常堆栈功能
- 异常历史记录功能,可通过通知栏磁贴“异常历史记录”进入和模块主界面进入
- 应用异常统计功能
- 多进程应用的异常显示功能
## Translation contribution
## Translation Contribution
Contributions to this project are welcome to translate it into your country's language.
**翻译贡献**
## Release Channel Description
欢迎为此项目做出贡献,将其翻译为您国家的语言。
- [Automatic Build on Commit](https://github.com/KitsunePie/AppErrorsTracking/actions/workflows/commit_ci.yml)
The above update is automatically triggered after the code `commit`.
The specific update content can be viewed by clicking the text above and going to **Github Actions**.
This update is automatically compiled and released by the open source process, **no guarantee of its stability**, so the released version is
**for testing only**, and there is no special explanation or even the version may change or remain the same as the current stable version.
- [Release](https://github.com/KitsunePie/AppErrorsTracking/releases)
- [Xposed-Modules-Repo](https://github.com/Xposed-Modules-Repo/com.fankes.apperrorstracking/releases)
The above update is a manually released stable version.
For the specific update content, you can click the text above to go to the designated release page to view.
The update of the stable version will be released to the above address at the same time and updated synchronously.
## Release Status Description
![Blank](https://img.shields.io/badge/build-passing-brightgreen)
The above status is that the current stable version is consistent with the automatic build version or the current code changes and the stable
version have no functional difference.
![Blank](https://img.shields.io/badge/build-pending-dbab09)
The above state is that there are automatic build versions and updates with new features but no stable version is currently released, and it is
in a pre-release state.
![Blank](https://img.shields.io/badge/build-problem-red)
The above status is that the currently released stable version may have serious problems but have not been fixed in time and the stable version
has not been released.
## License
- [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html)
```
Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@@ -121,4 +122,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
Powered by [YukiHookAPI](https://github.com/fankes/YukiHookAPI)
版权所有 © 2019-2022 Fankes Studio(qzmmcn@163.com)
Copyright © 2017-2023 Fankes Studio(qzmmcn@163.com)

View File

@@ -1,12 +1,12 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp' version '1.7.0-1.0.6'
id 'com.google.devtools.ksp' version '1.7.22-1.0.8'
}
android {
namespace 'com.fankes.apperrorstracking'
compileSdk 32
compileSdk 33
signingConfigs {
debug {
@@ -21,12 +21,15 @@ android {
defaultConfig {
applicationId "com.fankes.apperrorstracking"
minSdk 27
targetSdk 32
minSdk 24
targetSdk 33
versionCode rootProject.ext.appVersionCode
versionName rootProject.ext.appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
/** 添加 App Center Secret 到 BuildConfig */
buildConfigField("String", "APP_CENTER_SECRET", "\"${getAppCenterSecret()}\"")
}
buildTypes {
@@ -58,15 +61,31 @@ android {
}
}
/**
* 获取 App Center Secret
* @return [String]
*/
String getAppCenterSecret() {
def fileName = '../.secret/APP_CENTER_SECRET'
def content = ''
if (file(fileName).exists()) file(fileName).eachLine { content = it }
return content
}
dependencies {
compileOnly 'de.robv.android.xposed:api:82'
implementation 'com.highcapable.yukihookapi:api:1.0.92'
ksp 'com.highcapable.yukihookapi:ksp-xposed:1.0.92'
implementation "com.github.topjohnwu.libsu:core:3.1.2"
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.highcapable.yukihookapi:api:1.1.6'
ksp 'com.highcapable.yukihookapi:ksp-xposed:1.1.6'
implementation 'com.microsoft.appcenter:appcenter-analytics:4.4.5'
implementation 'com.microsoft.appcenter:appcenter-crashes:4.4.5'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.7'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.github.duanhong169:drawabletoolbox:1.0.7'
implementation 'com.github.topjohnwu.libsu:core:3.1.2'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.7.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View File

@@ -34,6 +34,31 @@
-renamesourcefileattribute P
-keepattributes SourceFile,LineNumberTable
## ---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
# Gson specific classes
-dontwarn sun.misc**
-keep class com.google.gson.stream**{*;}
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model** { <fields>; }
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
## ---------------End: proguard configuration for Gson ----------
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static *** throwUninitializedProperty(...);
public static *** throwUninitializedPropertyAccessException(...);

View File

@@ -6,6 +6,10 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.INTERACT_ACROSS_USERS"
tools:ignore="ProtectedPermissions" />
<application
android:name=".application.AppErrorsApplication"
android:allowBackup="true"
@@ -23,7 +27,10 @@
android:value="@string/xposed_desc" />
<meta-data
android:name="xposedminversion"
android:value="93" />
android:value="89" />
<meta-data
android:name="xposedsharedprefs"
android:value="true" />
<meta-data
android:name="xposedscope"
android:resource="@array/module_scope" />
@@ -59,6 +66,11 @@
android:exported="false"
android:screenOrientation="behind" />
<activity
android:name=".ui.activity.debug.LoggerActivity"
android:exported="false"
android:screenOrientation="behind" />
<activity
android:name=".ui.activity.errors.AppErrorsRecordActivity"
android:exported="true"
@@ -83,7 +95,6 @@
android:name=".ui.activity.errors.AppErrorsDetailActivity"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="behind"
android:taskAffinity=":detail" />
<service

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -22,7 +22,9 @@
package com.fankes.apperrorstracking.application
import androidx.appcompat.app.AppCompatDelegate
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.utils.tool.AppAnalyticsTool
import com.highcapable.yukihookapi.hook.xposed.application.ModuleApplication
class AppErrorsApplication : ModuleApplication() {
@@ -33,5 +35,9 @@ class AppErrorsApplication : ModuleApplication() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
/** 绑定 I18n */
LocaleString.bind(instance = this)
/** 装载存储控制类 */
ConfigData.init(instance = this)
/** 装载 App Center */
AppAnalyticsTool.init(instance = this)
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -25,6 +25,8 @@ import java.io.Serializable
/**
* 应用异常信息显示 bean
* @param pid APP 进程 ID
* @param userId APP 用户 ID
* @param packageName APP 包名
* @param processName APP 进程名
* @param appName APP 名称
@@ -34,6 +36,8 @@ import java.io.Serializable
* @param isShowReopenButton 是否显示重新打开按钮
*/
data class AppErrorsDisplayBean(
var pid: Int,
var userId: Int,
var packageName: String,
var processName: String,
var appName: String,

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -22,16 +22,23 @@
package com.fankes.apperrorstracking.bean
import android.app.ApplicationErrorReport
import android.content.Context
import android.os.Build
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.utils.factory.difference
import com.fankes.apperrorstracking.utils.factory.*
import com.google.gson.annotations.SerializedName
import java.io.Serializable
import java.text.SimpleDateFormat
import java.util.*
/**
* 应用异常信息 bean
* @param pid 进程 ID
* @param userId 用户 ID
* @param cpuAbi CPU 架构类型
* @param packageName 包名
* @param versionName 版本名称
* @param versionCode 版本号
* @param isNativeCrash 是否为原生层异常
* @param exceptionClassName 异常类名
* @param exceptionMessage 异常信息
@@ -43,48 +50,100 @@ import java.util.*
* @param timestamp 记录时间戳
*/
data class AppErrorsInfoBean(
var packageName: String,
var isNativeCrash: Boolean,
var exceptionClassName: String,
var exceptionMessage: String,
var throwFileName: String,
var throwClassName: String,
var throwMethodName: String,
var throwLineNumber: Int,
var stackTrace: String,
var timestamp: Long,
@SerializedName("pid")
var pid: Int = -1,
@SerializedName("userId")
var userId: Int = -1,
@SerializedName("cpuAbi")
var cpuAbi: String = "",
@SerializedName("packageName")
var packageName: String = "",
@SerializedName("versionName")
var versionName: String = "",
@SerializedName("versionCode")
var versionCode: Long = -1L,
@SerializedName("isNativeCrash")
var isNativeCrash: Boolean = false,
@SerializedName("exceptionClassName")
var exceptionClassName: String = "",
@SerializedName("exceptionMessage")
var exceptionMessage: String = "",
@SerializedName("throwFileName")
var throwFileName: String = "",
@SerializedName("throwClassName")
var throwClassName: String = "",
@SerializedName("throwMethodName")
var throwMethodName: String = "",
@SerializedName("throwLineNumber")
var throwLineNumber: Int = -1,
@SerializedName("stackTrace")
var stackTrace: String = "",
@SerializedName("timestamp")
var timestamp: Long = -1L
) : Serializable {
companion object {
/**
* 从 [ApplicationErrorReport.CrashInfo] 克隆
* @param context 当前实例
* @param pid APP 进程 ID
* @param userId APP 用户 ID
* @param packageName APP 包名
* @param crashInfo [ApplicationErrorReport.CrashInfo]
* @return [AppErrorsInfoBean]
*/
fun clone(packageName: String?, crashInfo: ApplicationErrorReport.CrashInfo?) =
fun clone(context: Context, pid: Int, userId: Int, packageName: String?, crashInfo: ApplicationErrorReport.CrashInfo?) =
(crashInfo?.exceptionClassName?.lowercase() == "native crash").let { isNativeCrash ->
AppErrorsInfoBean(
packageName = packageName ?: "null",
pid = pid,
userId = userId,
cpuAbi = packageName?.let { context.appCpuAbiOf(it) } ?: "",
packageName = packageName ?: "unknown",
versionName = packageName?.let { context.appVersionNameOf(it) } ?: "",
versionCode = packageName?.let { context.appVersionCodeOf(it) } ?: -1L,
isNativeCrash = isNativeCrash,
exceptionClassName = crashInfo?.exceptionClassName ?: "null",
exceptionClassName = crashInfo?.exceptionClassName ?: "unknown",
exceptionMessage = if (isNativeCrash) crashInfo?.stackTrace.let {
if (it?.contains(other = "Abort message: '") == true)
if (it?.contains("Abort message: '") == true)
runCatching { it.split("Abort message: '")[1].split("'")[0] }.getOrNull()
?: crashInfo?.exceptionMessage ?: "null"
else crashInfo?.exceptionMessage ?: "null"
} else crashInfo?.exceptionMessage ?: "null",
throwFileName = crashInfo?.throwFileName ?: "null",
throwClassName = crashInfo?.throwClassName ?: "null",
throwMethodName = crashInfo?.throwMethodName ?: "null",
?: crashInfo?.exceptionMessage ?: "unknown"
else crashInfo?.exceptionMessage ?: "unknown"
} else crashInfo?.exceptionMessage ?: "unknown",
throwFileName = crashInfo?.throwFileName ?: "unknown",
throwClassName = crashInfo?.throwClassName ?: "unknown",
throwMethodName = crashInfo?.throwMethodName ?: "unknown",
throwLineNumber = crashInfo?.throwLineNumber ?: -1,
stackTrace = crashInfo?.stackTrace?.trim() ?: "null",
stackTrace = crashInfo?.stackTrace?.trim() ?: "unknown",
timestamp = System.currentTimeMillis()
)
}
}
/**
* 获取当前内容是否为空
* @return [Boolean]
*/
val isEmpty get() = pid == -1 && userId == -1 && timestamp == -1L
/**
* 获取生成的 Json 文件名
* @return [String]
*/
val jsonFileName get() = "${packageName}_${pid}_${timestamp}.json"
/**
* 获取 APP 版本信息与版本号
* @return [String]
*/
val versionBrand get() = if (versionName.isBlank()) "unknown" else "$versionName($versionCode)"
/**
* 获取异常本地化 UTC 时间
* @return [String]
*/
val utcTime get() = timestamp.toUtcTime()
/**
* 获取异常本地化经过时间
* @return [String]
@@ -101,10 +160,10 @@ data class AppErrorsInfoBean(
)
/**
* 获取异常本地化量化时间
* 获取异常本地化时间
* @return [String]
*/
val dateTime get() = SimpleDateFormat.getDateTimeInstance().format(Date(timestamp)) ?: "DateTime not found"
val dateTime get() = SimpleDateFormat.getDateTimeInstance().format(Date(timestamp)) ?: utcTime
/**
* 获取异常堆栈分享模板
@@ -113,17 +172,7 @@ data class AppErrorsInfoBean(
val stackOutputShareContent
get() = "Generated by AppErrorsTracking\n" +
"Project Url: https://github.com/KitsunePie/AppErrorsTracking\n" +
"===============\n" +
"[Device Brand]: ${Build.BRAND}\n" +
"[Device Model]: ${Build.MODEL}\n" +
"[Display]: ${Build.DISPLAY}\n" +
"[Android Version]: ${Build.VERSION.RELEASE}\n" +
"[API Version]: ${Build.VERSION.SDK_INT}\n" +
"[Package Name]: $packageName\n" +
"[Error Type]: ${if (isNativeCrash) "Native" else "Jvm"}\n" +
"[Crash Time]: $dateTime\n" +
"[Stack Trace]:\n" +
stackTrace
"===============\n$environmentInfo"
/**
* 获取异常堆栈文件模板
@@ -134,14 +183,26 @@ data class AppErrorsInfoBean(
" Generated by AppErrorsTracking\n" +
" Project Url: https://github.com/KitsunePie/AppErrorsTracking\n" +
"================================================================\n" +
"[Device Brand]: ${Build.BRAND}\n" +
environmentInfo
/**
* 获取运行环境信息
* @return [String]
*/
private val environmentInfo
get() = "[Device Brand]: ${Build.BRAND}\n" +
"[Device Model]: ${Build.MODEL}\n" +
"[Display]: ${Build.DISPLAY}\n" +
"[Android Version]: ${Build.VERSION.RELEASE}\n" +
"[API Version]: ${Build.VERSION.SDK_INT}\n" +
"[Android API Level]: ${Build.VERSION.SDK_INT}\n" +
"[System Locale]: ${Locale.getDefault()}\n" +
"[Process ID]: $pid\n" +
(if (userId > 0) "[User Id]: $userId\n" else "") +
"[CPU ABI]: ${cpuAbi.ifBlank { "none" }}\n" +
"[Package Name]: $packageName\n" +
"[Error Type]: ${if (isNativeCrash) "Native" else "Jvm"}\n" +
"[Crash Time]: $dateTime\n" +
"[Stack Trace]:\n" +
stackTrace
"[Version Name]: ${versionName.ifBlank { "unknown" }}\n" +
"[Version Code]: ${versionCode.takeIf { it != -1L } ?: "unknown"}\n" +
"[Error Type]: ${if (isNativeCrash) "Native" else "JVM"}\n" +
"[Crash Time]: $utcTime\n" +
"[Stack Trace]:\n" + stackTrace
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -21,11 +21,15 @@
*/
package com.fankes.apperrorstracking.bean
import com.fankes.apperrorstracking.bean.enum.AppFiltersType
import java.io.Serializable
/**
* 应用过滤条件 bean
* @param name 名称或包名
* @param isContainsSystem 是否包含系统应用
* @param type 过滤条件类型
*/
data class AppFiltersBean(var name: String = "", var isContainsSystem: Boolean = false) : Serializable
data class AppFiltersBean(
var name: String = "",
var type: AppFiltersType = AppFiltersType.USER
) : Serializable

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -30,4 +30,8 @@ import java.io.Serializable
* @param name APP 名称
* @param packageName APP 包名
*/
data class AppInfoBean(var icon: Drawable? = null, var name: String, var packageName: String) : Serializable
data class AppInfoBean(
var icon: Drawable? = null,
var name: String,
var packageName: String
) : Serializable

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -28,7 +28,10 @@ import java.io.Serializable
* @param type 类型
* @param packageName 包名
*/
data class MutedErrorsAppBean(var type: MuteType, var packageName: String) : Serializable {
data class MutedErrorsAppBean(
var type: MuteType,
var packageName: String
) : Serializable {
/**
* 已忽略的异常类型

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -17,13 +17,20 @@
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
* This file is Created by fankes on 2023/1/22.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
package com.fankes.apperrorstracking.bean.enum
class Constants {
/**
* 应用过滤条件类型定义类
*/
enum class AppFiltersType {
/** 用户 */
USER,
companion object {
const val DEFAULT_COLOR = 0xFFBA68C8.toInt()
}
/** 系统 */
SYSTEM,
/** 全部 */
ALL
}

View File

@@ -0,0 +1,139 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2023/1/20.
*/
package com.fankes.apperrorstracking.data
import com.fankes.apperrorstracking.data.enum.AppErrorsConfigType
import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData
/**
* 应用配置模版存储控制类
*/
object AppErrorsConfigData {
/** 显示错误对话框键值名称 */
private const val SHOW_ERRORS_DIALOG_APPS = "_show_errors_dialog_apps"
/** 推送错误通知键值名称 */
private const val SHOW_ERRORS_NOTIFY_APPS = "_show_errors_notify_apps"
/** 显示错误 Toast 键值名称 */
private const val SHOW_ERRORS_TOAST_APPS = "_show_errors_toast_apps"
/** 什么也不显示键值名称 */
private const val SHOW_ERRORS_NOTHING_APPS = "_show_errors_nothing_apps"
/** 全局错误显示类型 */
private val GLOBAL_SHOW_ERRORS_TYPE = PrefsData("_global_show_errors_type", AppErrorsConfigType.DIALOG.ordinal)
/** 显示错误对话框的 APP 包名数组 */
private var showDialogApps = HashSet<String>()
/** 推送错误通知的 APP 包名数组 */
private var showNotifyApps = HashSet<String>()
/** 显示错误 Toast 的 APP 包名数组 */
private var showToastApps = HashSet<String>()
/** 什么也不显示的 APP 包名数组 */
private var showNothingApps = HashSet<String>()
/** 刷新存储控制类 */
fun refresh() {
showDialogApps = ConfigData.getStringSet(SHOW_ERRORS_DIALOG_APPS).toHashSet()
showNotifyApps = ConfigData.getStringSet(SHOW_ERRORS_NOTIFY_APPS).toHashSet()
showToastApps = ConfigData.getStringSet(SHOW_ERRORS_TOAST_APPS).toHashSet()
showNothingApps = ConfigData.getStringSet(SHOW_ERRORS_NOTHING_APPS).toHashSet()
}
/**
* 获取当前 APP 显示错误的类型是否为 [type]
* @param type 当前类型
* @param packageName 当前 APP 包名 - 不填为全局配置
* @return [Boolean]
*/
fun isAppShowingType(type: AppErrorsConfigType, packageName: String = "") =
if (packageName.isNotBlank()) when (type) {
AppErrorsConfigType.GLOBAL ->
showDialogApps.contains(packageName).not() &&
showNotifyApps.contains(packageName).not() &&
showToastApps.contains(packageName).not() &&
showNothingApps.contains(packageName).not()
AppErrorsConfigType.DIALOG -> showDialogApps.contains(packageName)
AppErrorsConfigType.NOTIFY -> showNotifyApps.contains(packageName)
AppErrorsConfigType.TOAST -> showToastApps.contains(packageName)
AppErrorsConfigType.NOTHING -> showNothingApps.contains(packageName)
} else ConfigData.getInt(GLOBAL_SHOW_ERRORS_TYPE) == type.ordinal
/**
* 写入当前 APP 显示错误的类型
* @param type 当前类型
* @param packageName 当前 APP 包名 - 不填为全局配置
* @throws IllegalStateException 如果 [packageName] 为空 [type] 为 [AppErrorsConfigType.GLOBAL]
*/
fun putAppShowingType(type: AppErrorsConfigType, packageName: String = "") {
if (packageName.isBlank() && type == AppErrorsConfigType.GLOBAL)
error("You can't still specify the \"follow global config\" type when saving the global config")
fun saveAllData() {
ConfigData.putStringSet(SHOW_ERRORS_DIALOG_APPS, showDialogApps)
ConfigData.putStringSet(SHOW_ERRORS_NOTIFY_APPS, showNotifyApps)
ConfigData.putStringSet(SHOW_ERRORS_TOAST_APPS, showToastApps)
ConfigData.putStringSet(SHOW_ERRORS_NOTHING_APPS, showNothingApps)
}
if (packageName.isNotBlank()) when (type) {
AppErrorsConfigType.GLOBAL -> {
showDialogApps.remove(packageName)
showNotifyApps.remove(packageName)
showToastApps.remove(packageName)
showNothingApps.remove(packageName)
saveAllData()
}
AppErrorsConfigType.DIALOG -> {
showDialogApps.add(packageName)
showNotifyApps.remove(packageName)
showToastApps.remove(packageName)
showNothingApps.remove(packageName)
saveAllData()
}
AppErrorsConfigType.NOTIFY -> {
showDialogApps.remove(packageName)
showNotifyApps.add(packageName)
showToastApps.remove(packageName)
showNothingApps.remove(packageName)
saveAllData()
}
AppErrorsConfigType.TOAST -> {
showDialogApps.remove(packageName)
showNotifyApps.remove(packageName)
showToastApps.add(packageName)
showNothingApps.remove(packageName)
saveAllData()
}
AppErrorsConfigType.NOTHING -> {
showDialogApps.remove(packageName)
showNotifyApps.remove(packageName)
showToastApps.remove(packageName)
showNothingApps.add(packageName)
saveAllData()
}
} else ConfigData.putInt(GLOBAL_SHOW_ERRORS_TYPE, type.ordinal)
}
}

View File

@@ -0,0 +1,134 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2023/1/17.
*/
@file:Suppress("StaticFieldLeak")
package com.fankes.apperrorstracking.data
import android.content.Context
import android.provider.Settings
import com.fankes.apperrorstracking.bean.AppErrorsInfoBean
import com.fankes.apperrorstracking.utils.factory.*
import com.highcapable.yukihookapi.hook.log.loggerE
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList
/**
* [AppErrorsInfoBean] 存储控制类
*/
object AppErrorsRecordData {
/** 异常记录数据文件目录路径 */
private const val FOLDER_PATH = "/data/misc/app_errors_records/"
/** 当前实例 */
private var context: Context? = null
/**
* 获取当前异常记录数据目录
* @return [File]
*/
private val errorsInfoDataFolder by lazy { File(FOLDER_PATH) }
/**
* 获取当前全部异常记录数据文件
* @return [List]<[File]>
*/
private val errorsInfoDataFiles get() = errorsInfoDataFolder.listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
/** 已记录的全部 APP 异常信息数组 */
var allData = CopyOnWriteArrayList<AppErrorsInfoBean>()
/**
* 初始化存储控制类
* @param context 实例
*/
fun init(context: Context) {
this.context = context
initializeDataDirectory()
allData = readAllDataFromFiles()
}
/** 初始化异常记录数据目录 */
private fun initializeDataDirectory() {
runCatching {
errorsInfoDataFolder.also { if (it.exists().not() || it.isFile) it.apply { delete(); mkdirs() } }
}.onFailure {
loggerE(msg = "Can't create directory \"$FOLDER_PATH\", there will be problems with the app errors records function", e = it)
}
}
/**
* 获取旧版异常记录数据并自动转换到新版
* @return [ArrayList]<[AppErrorsInfoBean]> or null
*/
private fun copyOldDataFromResolverString() = context?.let {
val keyName = "app_errors_data"
runCatching {
Settings.Secure.getString(it.contentResolver, keyName)
?.toEntityOrNull<CopyOnWriteArrayList<AppErrorsInfoBean>>()
?.onEach { e ->
e.cpuAbi = it.appCpuAbiOf(e.packageName)
e.versionName = it.appVersionNameOf(e.packageName)
e.versionCode = it.appVersionCodeOf(e.packageName)
e.toJsonOrNull()?.also { json -> File(errorsInfoDataFolder.absolutePath, e.jsonFileName).writeText(json) }
}.let { result ->
if (result != null) {
Settings.Secure.putString(it.contentResolver, keyName, "")
result
} else null
}
}.getOrNull()
}
/**
* 从文件获取全部异常记录数据
* @return [ArrayList]<[AppErrorsInfoBean]>
*/
private fun readAllDataFromFiles() = copyOldDataFromResolverString() ?: CopyOnWriteArrayList<AppErrorsInfoBean>().apply {
errorsInfoDataFiles.takeIf { it.isNotEmpty() }?.forEach { it.readText().toEntityOrNull<AppErrorsInfoBean>()?.let { e -> add(e) } }
}
/**
* 添加新的异常记录数据
* @param bean [AppErrorsInfoBean] 实例
*/
fun add(bean: AppErrorsInfoBean) {
allData.add(0, bean)
bean.toJsonOrNull()?.runCatching { File(errorsInfoDataFolder.absolutePath, bean.jsonFileName).writeText(this) }
}
/**
* 移除指定的异常记录数据
* @param bean [AppErrorsInfoBean] 实例
*/
fun remove(bean: AppErrorsInfoBean) {
allData.remove(bean)
runCatching { File(errorsInfoDataFolder.absolutePath, bean.jsonFileName).delete() }
}
/** 清除全部异常记录数据 */
fun clearAll() {
allData.clear()
runCatching { errorsInfoDataFolder.deleteRecursively() }
initializeDataDirectory()
}
}

View File

@@ -0,0 +1,222 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/10/1.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package com.fankes.apperrorstracking.data
import android.content.Context
import android.os.Build
import android.widget.CompoundButton
import com.highcapable.yukihookapi.hook.factory.modulePrefs
import com.highcapable.yukihookapi.hook.log.loggerW
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData
/**
* 全局配置存储控制类
*/
object ConfigData {
/** 显示开发者提示 */
val SHOW_DEVELOPER_NOTICE = PrefsData("_show_developer_notice", true)
/** 启用 Material 3 风格的错误对话框 */
val ENABLE_MATERIAL3_STYLE_APP_ERRORS_DIALOG = PrefsData("_enable_material3_style_dialog", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
/** 仅对前台应用显示错误对话框 */
val ENABLE_ONLY_SHOW_ERRORS_IN_FRONT = PrefsData("_enable_only_show_errors_in_front", false)
/** 仅对应用主进程显示错误对话框 */
val ENABLE_ONLY_SHOW_ERRORS_IN_MAIN = PrefsData("_enable_only_show_errors_in_main", false)
/** 错误对话框始终显示“重新打开”选项 */
val ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS = PrefsData("_enable_always_shows_reopen_app_options", false)
/** 启用应用配置模板 */
val ENABLE_APP_CONFIG_TEMPLATE = PrefsData("_enable_app_config_template", false)
/** 禁止异常堆栈内容自动换行 */
val DISABLE_AUTO_WRAP_ERROR_STACK_TRACE = PrefsData("_disable_auto_wrap_error_stack_trace", false)
/** 当前实例 - [Context] or [PackageParam] */
private var instance: Any? = null
/**
* 初始化存储控制类
* @param instance 实例 - 只能是 [Context] or [PackageParam]
* @throws IllegalStateException 如果类型错误
*/
fun init(instance: Any) {
when (instance) {
is Context, is PackageParam -> this.instance = instance
else -> error("Unknown type for init ConfigData")
}
AppErrorsConfigData.refresh()
}
/**
* 读取 [Set]<[String]> 数据
* @param key 键值名称
* @return [Set]<[String]>
*/
internal fun getStringSet(key: String) = when (instance) {
is Context -> (instance as Context).modulePrefs.getStringSet(key, setOf())
is PackageParam -> (instance as PackageParam).prefs.getStringSet(key, setOf())
else -> error("Unknown type for get prefs data")
}
/**
* 存入 [Set]<[String]> 数据
* @param key 键值名称
* @param value 键值内容
*/
internal fun putStringSet(key: String, value: Set<String>) {
when (instance) {
is Context -> (instance as Context).modulePrefs.putStringSet(key, value)
is PackageParam -> loggerW(msg = "Not support for this method")
else -> error("Unknown type for put prefs data")
}
}
/**
* 读取 [Int] 数据
* @param data 键值数据模板
* @return [Int]
*/
internal fun getInt(data: PrefsData<Int>) = when (instance) {
is Context -> (instance as Context).modulePrefs.get(data)
is PackageParam -> (instance as PackageParam).prefs.get(data)
else -> error("Unknown type for get prefs data")
}
/**
* 存入 [Int] 数据
* @param data 键值数据模板
* @param value 键值内容
*/
internal fun putInt(data: PrefsData<Int>, value: Int) {
when (instance) {
is Context -> (instance as Context).modulePrefs.put(data, value)
is PackageParam -> loggerW(msg = "Not support for this method")
else -> error("Unknown type for put prefs data")
}
}
/**
* 读取 [Boolean] 数据
* @param data 键值数据模板
* @return [Boolean]
*/
private fun getBoolean(data: PrefsData<Boolean>) = when (instance) {
is Context -> (instance as Context).modulePrefs.get(data)
is PackageParam -> (instance as PackageParam).prefs.get(data)
else -> error("Unknown type for get prefs data")
}
/**
* 存入 [Boolean] 数据
* @param data 键值数据模板
* @param value 键值内容
*/
private fun putBoolean(data: PrefsData<Boolean>, value: Boolean) {
when (instance) {
is Context -> (instance as Context).modulePrefs.put(data, value)
is PackageParam -> loggerW(msg = "Not support for this method")
else -> error("Unknown type for put prefs data")
}
}
/**
* 绑定到 [CompoundButton] 自动设置选中状态
* @param data 键值数据模板
* @param onChange 当改变时回调
*/
fun CompoundButton.bind(data: PrefsData<Boolean>, onChange: (Boolean) -> Unit = {}) {
isChecked = getBoolean(data).also(onChange)
setOnCheckedChangeListener { button, isChecked ->
if (button.isPressed) {
putBoolean(data, isChecked)
onChange(isChecked)
}
}
}
/**
* 是否显示开发者提示
* @return [Boolean]
*/
var isShowDeveloperNotice
get() = getBoolean(SHOW_DEVELOPER_NOTICE)
set(value) {
putBoolean(SHOW_DEVELOPER_NOTICE, value)
}
/**
* 是否仅对前台应用显示错误对话框
* @return [Boolean]
*/
var isEnableOnlyShowErrorsInFront
get() = getBoolean(ENABLE_ONLY_SHOW_ERRORS_IN_FRONT)
set(value) {
putBoolean(ENABLE_ONLY_SHOW_ERRORS_IN_FRONT, value)
}
/**
* 是否仅对应用主进程显示错误对话框
* @return [Boolean]
*/
var isEnableOnlyShowErrorsInMain
get() = getBoolean(ENABLE_ONLY_SHOW_ERRORS_IN_MAIN)
set(value) {
putBoolean(ENABLE_ONLY_SHOW_ERRORS_IN_MAIN, value)
}
/**
* 错误对话框是否始终显示“重新打开”选项
* @return [Boolean]
*/
var isEnableAlwaysShowsReopenAppOptions
get() = getBoolean(ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS)
set(value) {
putBoolean(ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS, value)
}
/**
* 是否启用应用配置模板
* @return [Boolean]
*/
var isEnableAppConfigTemplate
get() = getBoolean(ENABLE_APP_CONFIG_TEMPLATE)
set(value) {
putBoolean(ENABLE_APP_CONFIG_TEMPLATE, value)
}
/**
* 是否启用 Material 3 风格的错误对话框
* @return [Boolean]
*/
var isEnableMaterial3StyleAppErrorsDialog
get() = getBoolean(ENABLE_MATERIAL3_STYLE_APP_ERRORS_DIALOG)
set(value) {
putBoolean(ENABLE_MATERIAL3_STYLE_APP_ERRORS_DIALOG, value)
}
}

View File

@@ -1,35 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/14.
*/
package com.fankes.apperrorstracking.data
import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData
object DataConst {
val SHOW_DEVELOPER_NOTICE = PrefsData("_show_developer_notice", true)
val ENABLE_HIDE_ICON = PrefsData("_hide_icon", false)
val ENABLE_ONLY_SHOW_ERRORS_IN_FRONT = PrefsData("_enable_only_show_errors_in_front", false)
val ENABLE_ONLY_SHOW_ERRORS_IN_MAIN = PrefsData("_enable_only_show_errors_in_main", false)
val ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS = PrefsData("_enable_always_shows_reopen_app_options", false)
val ENABLE_APP_CONFIG_TEMPLATE = PrefsData("_enable_app_config_template", false)
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -17,18 +17,26 @@
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
* This file is Created by fankes on 2023/1/20.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
package com.fankes.apperrorstracking.data.enum
import android.graphics.drawable.Drawable
/**
* 应用配置模版类型定义类
*/
enum class AppErrorsConfigType {
/** 跟随全局配置 */
GLOBAL,
abstract class DrawableWrapperBuilder<T : DrawableWrapperBuilder<T>> {
/** 对话框 */
DIALOG,
protected var drawable: Drawable? = null
/** 通知 */
NOTIFY,
@Suppress("UNCHECKED_CAST")
fun drawable(drawable: Drawable): T = apply { this.drawable = drawable } as T
/** Toast */
TOAST,
abstract fun build(): Drawable
/** 什么也不显示 */
NOTHING
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -21,6 +21,7 @@
*/
package com.fankes.apperrorstracking.hook
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.hook.entity.FrameworkHooker
import com.fankes.apperrorstracking.locale.LocaleString
import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed
@@ -29,10 +30,13 @@ import com.highcapable.yukihookapi.hook.factory.encase
import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit
@InjectYukiHookWithXposed(entryClassName = "AppErrorsTracking", isUsingResourcesHook = false)
class HookEntry : IYukiHookXposedInit {
object HookEntry : IYukiHookXposedInit {
override fun onInit() = configs {
debugTag = "AppErrorsTracking"
debugLog {
tag = "AppErrorsTracking"
isRecord = true
}
isDebug = false
isEnableModulePrefsCache = false
}
@@ -40,6 +44,7 @@ class HookEntry : IYukiHookXposedInit {
override fun onHook() = encase {
loadSystem {
LocaleString.bind(instance = this)
ConfigData.init(instance = this)
loadHooker(FrameworkHooker)
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -19,17 +19,18 @@
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("UseCompatLoadingForDrawables")
package com.fankes.apperrorstracking.hook.entity
import android.app.ApplicationErrorReport
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageInfo
import android.os.Build
import android.os.Message
import android.os.SystemClock
import android.util.ArrayMap
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import com.fankes.apperrorstracking.BuildConfig
@@ -38,10 +39,11 @@ import com.fankes.apperrorstracking.bean.AppErrorsDisplayBean
import com.fankes.apperrorstracking.bean.AppErrorsInfoBean
import com.fankes.apperrorstracking.bean.AppInfoBean
import com.fankes.apperrorstracking.bean.MutedErrorsAppBean
import com.fankes.apperrorstracking.data.DataConst
import com.fankes.apperrorstracking.hook.factory.isAppShowErrorsNotify
import com.fankes.apperrorstracking.hook.factory.isAppShowErrorsToast
import com.fankes.apperrorstracking.hook.factory.isAppShowNothing
import com.fankes.apperrorstracking.bean.enum.AppFiltersType
import com.fankes.apperrorstracking.data.AppErrorsConfigData
import com.fankes.apperrorstracking.data.AppErrorsRecordData
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.data.enum.AppErrorsConfigType
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsDisplayActivity
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsRecordActivity
@@ -53,8 +55,11 @@ import com.highcapable.yukihookapi.hook.factory.field
import com.highcapable.yukihookapi.hook.factory.hasMethod
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.log.loggerE
import com.highcapable.yukihookapi.hook.log.loggerI
import com.highcapable.yukihookapi.hook.log.loggerW
import com.highcapable.yukihookapi.hook.type.android.BundleClass
import com.highcapable.yukihookapi.hook.type.android.MessageClass
import com.highcapable.yukihookapi.hook.type.java.BooleanType
object FrameworkHooker : YukiBaseHooker() {
@@ -70,7 +75,6 @@ object FrameworkHooker : YukiBaseHooker() {
"com.android.server.am.ProcessRecord\$PackageList",
"com.android.server.am.PackageList"
)
private val ErrorDialogControllerClass = VariousClass(
"com.android.server.am.ProcessRecord\$ErrorDialogController",
"com.android.server.am.ErrorDialogController"
@@ -82,24 +86,133 @@ object FrameworkHooker : YukiBaseHooker() {
/** 已忽略错误的 APP 数组 - 直到重新启动 */
private var mutedErrorsIfRestartApps = HashSet<String>()
/** 已记录的 APP 异常信息数组 - 直到重新启动 */
private val appErrorsRecords = ArrayList<AppErrorsInfoBean>()
/**
* APP 进程异常数据定义类
* @param errors [AppErrorsClass] 实例
* @param proc [ProcessRecordClass] 实例
* @param resultData [AppErrorDialog_DataClass] 实例 - 默认空
*/
private class AppErrorsProcessData(errors: Any?, proc: Any?, resultData: Any? = null) {
/** 注册 */
private fun register() {
/**
* 获取当前包列表实例
* @return [Any] or null
*/
private val pkgList = if (ProcessRecordClass.toClass().hasMethod { name = "getPkgList"; emptyParam() })
ProcessRecordClass.toClass().method { name = "getPkgList"; emptyParam() }.get(proc).call()
else ProcessRecordClass.toClass().field { name = "pkgList" }.get(proc).any()
/**
* 获取当前包列表数组大小
* @return [Int]
*/
private val pkgListSize = PackageListClass.toClassOrNull()?.method { name = "size"; emptyParam() }?.get(pkgList)?.int()
?: ProcessRecordClass.toClass().field { name = "pkgList" }.get(proc).cast<ArrayMap<*, *>>()?.size ?: -1
/**
* 获取当前 pid 信息
* @return [Int]
*/
val pid = ProcessRecordClass.toClass().field { name { it == "mPid" || it == "pid" } }.get(proc).int()
/**
* 获取当前用户 ID 信息
* @return [Int]
*/
val userId = ProcessRecordClass.toClass().field { name = "userId" }.get(proc).int()
/**
* 获取当前 APP 信息
* @return [ApplicationInfo] or null
*/
val appInfo = ProcessRecordClass.toClass().field { name = "info" }.get(proc).cast<ApplicationInfo>()
/**
* 获取当前进程名称
* @return [String]
*/
val processName = ProcessRecordClass.toClass().field { name = "processName" }.get(proc).string()
/**
* 获取当前 APP、进程 包名
* @return [String]
*/
val packageName = appInfo?.packageName ?: processName
/**
* 获取当前进程是否为可被启动的 APP - 非框架 APP
* @return [Boolean]
*/
val isActualApp = pkgListSize == 1 && appInfo != null
/**
* 获取当前进程是否为主进程
* @return [Boolean]
*/
val isMainProcess = packageName == processName
/**
* 获取当前进程是否为后台进程
* @return [Boolean]
*/
val isBackgroundProcess = UserControllerClass.toClass()
.method { name { it == "getCurrentProfileIds" || it == "getCurrentProfileIdsLocked" } }
.get(ActivityManagerServiceClass.toClass().field { name = "mUserController" }
.get(AppErrorsClass.toClass().field { name = "mService" }.get(errors).any()).any())
.invoke<IntArray>()?.takeIf { it.isNotEmpty() }?.any { it != userId } ?: false
/**
* 获取当前进程是否短时内重复崩溃
* @return [Boolean]
*/
val isRepeatingCrash = resultData?.let { AppErrorDialog_DataClass.toClass().field { name = "repeating" }.get(it).boolean() } ?: false
}
/** 注册生命周期 */
private fun registerLifecycle() {
onAppLifecycle {
/** 解锁后清空已记录的忽略错误 APP */
registerReceiver(Intent.ACTION_USER_PRESENT) { _, _ -> mutedErrorsIfUnlockApps.clear() }
/** 刷新模块 Resources 缓存 */
registerReceiver(Intent.ACTION_LOCALE_CHANGED) { _, _ -> refreshModuleAppResources() }
/** 启动时从本地获取异常记录数据 */
onCreate { AppErrorsRecordData.init(context = this) }
}
FrameworkTool.Host.with(instance = this) {
onOpenAppUsedFramework { appContext.openApp(it) }
onPushAppErrorsInfoData { appErrorsRecords }
onRemoveAppErrorsInfoData { appErrorsRecords.remove(it) }
onClearAppErrorsInfoData { appErrorsRecords.clear() }
onMutedErrorsIfUnlock { mutedErrorsIfUnlockApps.add(it) }
onMutedErrorsIfRestart { mutedErrorsIfRestartApps.add(it) }
onRefreshFrameworkPrefsData {
/** 必要的延迟防止 Sp 存储不刷新 */
SystemClock.sleep(100)
/** 刷新存储类 */
AppErrorsConfigData.refresh()
if (prefs.isPreferencesAvailable.not()) loggerW(msg = "Cannot refreshing app errors config data, preferences is not available")
}
onOpenAppUsedFramework {
appContext?.openApp(it.first, it.second)
loggerI(msg = "Opened \"${it.first}\"${it.second.takeIf { e -> e > 0 }?.let { e -> " --user $e" } ?: ""}")
}
onPushAppErrorInfoData {
AppErrorsRecordData.allData.firstOrNull { e -> e.pid == it } ?: run {
loggerW(msg = "Cannot received crash application data --pid $it")
AppErrorsInfoBean()
}
}
onPushAppErrorsInfoData { AppErrorsRecordData.allData.toArrayList() }
onRemoveAppErrorsInfoData {
loggerI(msg = "Removed app errors info data for package \"${it.packageName}\"")
AppErrorsRecordData.remove(it)
}
onClearAppErrorsInfoData {
loggerI(msg = "Cleared all app errors info data, size ${AppErrorsRecordData.allData.size}")
AppErrorsRecordData.clearAll()
}
onMutedErrorsIfUnlock {
mutedErrorsIfUnlockApps.add(it)
loggerI(msg = "Muted \"$it\" until unlocks")
}
onMutedErrorsIfRestart {
mutedErrorsIfRestartApps.add(it)
loggerI(msg = "Muted \"$it\" until restarts")
}
onPushMutedErrorsAppsData {
arrayListOf<MutedErrorsAppBean>().apply {
mutedErrorsIfUnlockApps.takeIf { it.isNotEmpty() }
@@ -110,35 +223,144 @@ object FrameworkHooker : YukiBaseHooker() {
}
onUnmuteErrorsApp {
when (it.type) {
MutedErrorsAppBean.MuteType.UNTIL_UNLOCKS -> mutedErrorsIfUnlockApps.remove(it.packageName)
MutedErrorsAppBean.MuteType.UNTIL_REBOOTS -> mutedErrorsIfRestartApps.remove(it.packageName)
MutedErrorsAppBean.MuteType.UNTIL_UNLOCKS -> {
loggerI(msg = "Unmuted if unlocks errors app \"${it.packageName}\"")
mutedErrorsIfUnlockApps.remove(it.packageName)
}
MutedErrorsAppBean.MuteType.UNTIL_REBOOTS -> {
loggerI(msg = "Unmuted if restarts errors app \"${it.packageName}\"")
mutedErrorsIfRestartApps.remove(it.packageName)
}
}
}
onUnmuteAllErrorsApps {
loggerI(msg = "Unmute all errors apps --unlocks ${mutedErrorsIfUnlockApps.size} --restarts ${mutedErrorsIfRestartApps.size}")
mutedErrorsIfUnlockApps.clear()
mutedErrorsIfRestartApps.clear()
}
onPushAppListData { filters ->
arrayListOf<AppInfoBean>().apply {
appContext.packageManager.getInstalledPackages(PackageManager.GET_CONFIGURATIONS).also { info ->
(if (filters.name.isNotBlank())
info.filter { it.packageName.contains(filters.name) || appContext.appName(it.packageName).contains(filters.name) }
else info).let { result ->
if (filters.isContainsSystem.not()) result.filter { (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 0 }
else result
}.sortedByDescending { it.lastUpdateTime }
.forEach { add(AppInfoBean(name = appContext.appName(it.packageName), packageName = it.packageName)) }
/** 移除模块自身 */
removeIf { it.packageName == BuildConfig.APPLICATION_ID }
}
}
appContext?.let { context ->
context.listOfPackages()
.filter { it.packageName.let { e -> e != "android" && e != BuildConfig.APPLICATION_ID } }
.let { info ->
arrayListOf<AppInfoBean>().apply {
if (info.isNotEmpty())
(if (filters.name.isNotBlank()) info.filter {
it.packageName.contains(filters.name) || context.appNameOf(it.packageName).contains(filters.name)
} else info).let { result ->
/**
* 是否为系统应用
* @return [Boolean]
*/
fun PackageInfo.isSystemApp() = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
when (filters.type) {
AppFiltersType.USER -> result.filter { it.isSystemApp().not() }
AppFiltersType.SYSTEM -> result.filter { it.isSystemApp() }
AppFiltersType.ALL -> result
}
}.sortedByDescending { it.lastUpdateTime }
.forEach { add(AppInfoBean(name = context.appNameOf(it.packageName), packageName = it.packageName)) }
else loggerW(msg = "Fetched installed packages but got empty list")
}
}
} ?: arrayListOf()
}
}
}
/**
* 处理 APP 进程异常信息展示
* @param context 当前实例
*/
private fun AppErrorsProcessData.handleShowAppErrorUi(context: Context) {
/** 当前 APP 名称 */
val appName = appInfo?.let { context.appNameOf(it.packageName) } ?: packageName
/** 当前 APP 名称 (包含用户 ID) */
val appNameWithUserId = if (userId != 0) "$appName (${LocaleString.userId(userId)})" else appName
/** 崩溃标题 */
val errorTitle = if (isRepeatingCrash) LocaleString.aerrRepeatedTitle(appNameWithUserId) else LocaleString.aerrTitle(appNameWithUserId)
/** 使用通知推送异常信息 */
fun showAppErrorsWithNotify() =
context.pushNotify(
channelId = "APPS_ERRORS",
channelName = LocaleString.appName,
title = errorTitle,
content = LocaleString.appErrorsTip,
icon = IconCompat.createWithBitmap(moduleAppResources.drawableOf(R.drawable.ic_notify).toBitmap()),
color = 0xFFFF6200.toInt(),
intent = AppErrorsRecordActivity.intent()
)
/** 使用 Toast 展示异常信息 */
fun showAppErrorsWithToast() = context.toast(errorTitle)
/** 使用对话框展示异常信息 */
fun showAppErrorsWithDialog() =
AppErrorsDisplayActivity.start(
context, AppErrorsDisplayBean(
pid = pid,
userId = userId,
packageName = packageName,
processName = processName,
appName = appName,
title = errorTitle,
isShowAppInfoButton = isActualApp,
isShowReopenButton = isActualApp &&
(isRepeatingCrash.not() || ConfigData.isEnableAlwaysShowsReopenAppOptions) &&
context.isAppCanOpened(packageName) &&
isMainProcess,
isShowCloseAppButton = isActualApp
)
)
/** 判断是否为已忽略的 APP */
if (mutedErrorsIfUnlockApps.contains(packageName) || mutedErrorsIfRestartApps.contains(packageName)) return
/** 判断是否为后台进程 */
if ((isBackgroundProcess || context.isAppCanOpened(packageName).not()) && ConfigData.isEnableOnlyShowErrorsInFront) return
/** 判断是否为主进程 */
if (isMainProcess.not() && ConfigData.isEnableOnlyShowErrorsInMain) return
when {
packageName == BuildConfig.APPLICATION_ID -> {
context.toast(msg = "AppErrorsTracking has crashed, please see the log in console")
loggerE(msg = "AppErrorsTracking has crashed itself, please see the Android Runtime Exception in console")
}
ConfigData.isEnableAppConfigTemplate -> when {
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.GLOBAL, packageName) -> when {
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.DIALOG) -> showAppErrorsWithDialog()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTIFY) -> showAppErrorsWithNotify()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.TOAST) -> showAppErrorsWithToast()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTHING) -> {}
}
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.DIALOG, packageName) -> showAppErrorsWithDialog()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTIFY, packageName) -> showAppErrorsWithNotify()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.TOAST, packageName) -> showAppErrorsWithToast()
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTHING, packageName) -> {}
}
else -> showAppErrorsWithDialog()
}
/** 打印错误日志 */
if (isActualApp) loggerE(
msg = "Application \"$packageName\" ${if (isRepeatingCrash) "keeps stopping" else "has stopped"}" +
(if (packageName != processName) " --process \"$processName\"" else "") +
"${if (userId != 0) " --user $userId" else ""} --pid $pid"
) else loggerE(msg = "Process \"$processName\" ${if (isRepeatingCrash) "keeps stopping" else "has stopped"} --pid $pid")
}
/**
* 处理 APP 进程异常数据
* @param context 当前实例
* @param info 系统错误报告数据实例
*/
private fun AppErrorsProcessData.handleAppErrorsInfo(context: Context, info: ApplicationErrorReport.CrashInfo?) {
AppErrorsRecordData.add(AppErrorsInfoBean.clone(context, pid, userId, appInfo?.packageName, info))
loggerI(msg = "Received crash application data${if (userId != 0) " --user $userId" else ""} --pid $pid")
}
override fun onHook() {
/** 注册 */
register()
/** 注册生命周期 */
registerLifecycle()
/** 干掉原生错误对话框 - 如果有 */
ErrorDialogControllerClass.hook {
injectMember {
@@ -157,15 +379,26 @@ object FrameworkHooker : YukiBaseHooker() {
}
}.ignoredHookClassNotFoundFailure()
/** 干掉原生错误对话框 - API 30 以下 */
ActivityTaskManagerService_LocalServiceClass.hook {
injectMember {
method {
name = "canShowErrorDialogs"
emptyParam()
}
replaceToFalse()
}
}.by { Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q }
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
ActivityTaskManagerService_LocalServiceClass.hook {
injectMember {
method {
name = "canShowErrorDialogs"
emptyParam()
}
replaceToFalse()
}.ignoredNoSuchMemberFailure()
}.ignoredHookClassNotFoundFailure()
ActivityManagerServiceClass.hook {
injectMember {
method {
name = "canShowErrorDialogs"
emptyParam()
}
replaceToFalse()
}.ignoredNoSuchMemberFailure()
}.ignoredHookClassNotFoundFailure()
}
/** 干掉原生错误对话框 - 如果上述方法全部失效则直接结束对话框 */
AppErrorDialogClass.hook {
injectMember {
@@ -174,7 +407,14 @@ object FrameworkHooker : YukiBaseHooker() {
param(BundleClass)
}
afterHook { instance<Dialog>().cancel() }
}
}.ignoredNoSuchMemberFailure()
injectMember {
method {
name = "onStart"
emptyParam()
}
afterHook { instance<Dialog>().cancel() }
}.ignoredNoSuchMemberFailure()
}
/** 注入自定义错误对话框 */
AppErrorsClass.hook {
@@ -185,125 +425,32 @@ object FrameworkHooker : YukiBaseHooker() {
}
afterHook {
/** 当前实例 */
val context = field { name = "mContext" }.get(instance).cast<Context>() ?: return@afterHook
val context = appContext ?: field { name = "mContext" }.get(instance).cast<Context>() ?: return@afterHook
/** 错误数据 */
val errData = args().first().cast<Message>()?.obj
/** 当前错误数据 */
val resultData = args().first().cast<Message>()?.obj
/** 当前进程信息 */
val proc = AppErrorDialog_DataClass.clazz.field { name = "proc" }.get(errData).any()
/** 当前 UserId 信息 */
val userId = ProcessRecordClass.clazz.field { name = "userId" }.get(proc).int()
/** 当前 APP 信息 */
val appInfo = ProcessRecordClass.clazz.field { name = "info" }.get(proc).cast<ApplicationInfo>()
/** 当前进程名称 */
val processName = ProcessRecordClass.clazz.field { name = "processName" }.get(proc).string()
/** 当前 APP、进程 包名 */
val packageName = appInfo?.packageName ?: processName
/** 当前 APP 名称 */
val appName = appInfo?.let { context.appName(it.packageName) } ?: packageName
/** 是否为 APP */
val isApp = (PackageListClass.clazz.method {
name = "size"
emptyParam()
}.get(if (ProcessRecordClass.clazz.hasMethod {
name = "getPkgList"
emptyParam()
}) ProcessRecordClass.clazz.method {
name = "getPkgList"
emptyParam()
}.get(proc).call() else ProcessRecordClass.clazz.field {
name = "pkgList"
}.get(proc).self).int() == 1 && appInfo != null)
/** 是否为主进程 */
val isMainProcess = packageName == processName
/** 是否为后台进程 */
val isBackgroundProcess = UserControllerClass.clazz.method { name = "getCurrentProfileIds" }
.get(ActivityManagerServiceClass.clazz.field { name = "mUserController" }
.get(field { name = "mService" }.get(instance).any()).any())
.invoke<IntArray>()?.takeIf { it.isNotEmpty() }?.any { it != userId } ?: false
/** 是否短时内重复错误 */
val isRepeating = AppErrorDialog_DataClass.clazz.field { name = "repeating" }.get(errData).boolean()
/** 崩溃标题 */
val errorTitle = if (isRepeating) LocaleString.aerrRepeatedTitle(appName) else LocaleString.aerrTitle(appName)
/** 是否始终显示重新打开按钮 */
val isAlwaysShowsReopenApp = prefs.get(DataConst.ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS)
/** 打印错误日志 */
if (isApp) loggerE(
msg = "App \"$packageName\"${if (packageName != processName) " --process \"$processName\"" else ""}" +
" has crashed${if (isRepeating) " again" else ""}"
) else loggerE(msg = "Process \"$processName\" has crashed${if (isRepeating) " again" else ""}")
/** 判断是否为模块自身崩溃 */
if (packageName == BuildConfig.APPLICATION_ID) {
context.toast(msg = "AppErrorsTracking has crashed, please see the log in console")
return@afterHook
}
/** 判断是否为已忽略的 APP */
if (mutedErrorsIfUnlockApps.contains(packageName) || mutedErrorsIfRestartApps.contains(packageName)) return@afterHook
/** 判断配置模块启用状态 */
if (prefs.get(DataConst.ENABLE_APP_CONFIG_TEMPLATE)) {
if (isAppShowNothing(packageName)) return@afterHook
if (isAppShowErrorsNotify(packageName)) {
context.pushNotify(
channelId = "APPS_ERRORS",
channelName = LocaleString.appName,
title = errorTitle,
content = LocaleString.appErrorsTip,
icon = IconCompat.createWithBitmap(R.mipmap.ic_notify.drawableOf(moduleAppResources).toBitmap()),
color = 0xFFFF6200.toInt(),
intent = AppErrorsRecordActivity.intent()
)
return@afterHook
}
if (isAppShowErrorsToast(packageName)) {
context.toast(errorTitle)
return@afterHook
}
}
/** 判断是否为后台进程 */
if ((isBackgroundProcess || context.isAppCanOpened(packageName).not())
&& prefs.get(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_FRONT)
) return@afterHook
/** 判断是否为主进程 */
if (isMainProcess.not() && prefs.get(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_MAIN)) return@afterHook
/** 启动错误对话框显示窗口 */
AppErrorsDisplayActivity.start(
context, AppErrorsDisplayBean(
packageName = packageName,
processName = processName,
appName = appName,
title = errorTitle,
isShowAppInfoButton = isApp,
isShowReopenButton = isApp && (isRepeating.not() || isAlwaysShowsReopenApp)
&& context.isAppCanOpened(packageName) && isMainProcess,
isShowCloseAppButton = isApp
)
)
val proc = AppErrorDialog_DataClass.toClass().field { name = "proc" }.get(resultData).any()
/** 创建 APP 进程异常数据类 */
AppErrorsProcessData(instance, proc, resultData).handleShowAppErrorUi(context)
}
}
injectMember {
method {
name = "crashApplication"
paramCount = 2
name = "handleAppCrashInActivityController"
returnType = BooleanType
}
afterHook {
/** 当前 APP 信息 */
val appInfo = ProcessRecordClass.clazz.field { name = "info" }.get(args().first().any()).cast<ApplicationInfo>()
/** 添加当前异常信息到第一位 */
appErrorsRecords.add(0, AppErrorsInfoBean.clone(appInfo?.packageName, args().last().cast()))
/** 当前实例 */
val context = appContext ?: field { name = "mContext" }.get(instance).cast<Context>() ?: return@afterHook
/** 当前进程信息 */
val proc = args().first().any() ?: return@afterHook loggerW(msg = "Received but got null ProcessRecord")
/** 创建 APP 进程异常数据类 */
AppErrorsProcessData(instance, proc).handleAppErrorsInfo(context, args(index = 1).cast())
}
}
}
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/6/8.
*/
@file:Suppress("unused")
package com.fankes.apperrorstracking.hook.factory
import android.content.Context
import com.highcapable.yukihookapi.hook.factory.modulePrefs
import com.highcapable.yukihookapi.hook.param.PackageParam
/**
* 获取此 APP 是否配置显示错误对话框
* @param packageName APP 包名
*/
fun PackageParam.isAppShowErrorsDialog(packageName: String) = prefs.getBoolean("${packageName}_show_errors_dialog", true)
/**
* 获取此 APP 是否配置显示错误通知推送
* @param packageName APP 包名
*/
fun PackageParam.isAppShowErrorsNotify(packageName: String) = prefs.getBoolean("${packageName}_show_errors_notify", false)
/**
* 获取此 APP 是否配置显示错误 Toast 提示
* @param packageName APP 包名
*/
fun PackageParam.isAppShowErrorsToast(packageName: String) = prefs.getBoolean("${packageName}_show_errors_toast", false)
/**
* 获取此 APP 是否配置不显示任何提示
* @param packageName APP 包名
*/
fun PackageParam.isAppShowNothing(packageName: String) = prefs.getBoolean("${packageName}_show_nothing", false)
/**
* 获取此 APP 是否配置显示错误对话框
* @param packageName APP 包名
*/
fun Context.isAppShowErrorsDialog(packageName: String) = modulePrefs.getBoolean("${packageName}_show_errors_dialog", true)
/**
* 获取此 APP 是否配置显示错误通知推送
* @param packageName APP 包名
*/
fun Context.isAppShowErrorsNotify(packageName: String) = modulePrefs.getBoolean("${packageName}_show_errors_notify", false)
/**
* 获取此 APP 是否配置显示错误 Toast 提示
* @param packageName APP 包名
*/
fun Context.isAppShowErrorsToast(packageName: String) = modulePrefs.getBoolean("${packageName}_show_errors_toast", false)
/**
* 获取此 APP 是否配置不显示任何提示
* @param packageName APP 包名
*/
fun Context.isAppShowNothing(packageName: String) = modulePrefs.getBoolean("${packageName}_show_nothing", false)
/**
* 设置此 APP 是否配置显示错误对话框
* @param packageName APP 包名
* @param isApply 是否设置
*/
fun Context.putAppShowErrorsDialog(packageName: String, isApply: Boolean) = modulePrefs.putBoolean("${packageName}_show_errors_dialog", isApply)
/**
* 设置此 APP 是否配置显示错误通知推送
* @param packageName APP 包名
* @param isApply 是否设置
*/
fun Context.putAppShowErrorsNotify(packageName: String, isApply: Boolean) = modulePrefs.putBoolean("${packageName}_show_errors_notify", isApply)
/**
* 设置此 APP 是否配置显示错误 Toast 提示
* @param packageName APP 包名
* @param isApply 是否设置
*/
fun Context.putAppShowErrorsToast(packageName: String, isApply: Boolean) = modulePrefs.putBoolean("${packageName}_show_errors_toast", isApply)
/**
* 设置此 APP 是否配置不显示任何提示
* @param packageName APP 包名
* @param isApply 是否设置
*/
fun Context.putAppShowNothing(packageName: String, isApply: Boolean) = modulePrefs.putBoolean("${packageName}_show_nothing", isApply)

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -167,6 +167,12 @@ object LocaleString {
/** @string Automatic generated */
fun muteIfRestartTip(vararg objArrs: Any) = R.string.mute_if_restart_tip.bind(*objArrs)
/** @string Automatic generated */
val noListData get() = noListData()
/** @string Automatic generated */
fun noListData(vararg objArrs: Any) = R.string.no_list_data.bind(*objArrs)
/** @string Automatic generated */
val confirm get() = confirm()
@@ -191,6 +197,12 @@ object LocaleString {
/** @string Automatic generated */
fun notice(vararg objArrs: Any) = R.string.notice.bind(*objArrs)
/** @string Automatic generated */
val warning get() = warning()
/** @string Automatic generated */
fun warning(vararg objArrs: Any) = R.string.warning.bind(*objArrs)
/** @string Automatic generated */
val areYouSureClearErrors get() = areYouSureClearErrors()
@@ -347,6 +359,12 @@ object LocaleString {
/** @string Automatic generated */
fun areYouSureUnmuteAll(vararg objArrs: Any) = R.string.are_you_sure_unmute_all.bind(*objArrs)
/** @string Automatic generated */
val noListResult get() = noListResult()
/** @string Automatic generated */
fun noListResult(vararg objArrs: Any) = R.string.no_list_result.bind(*objArrs)
/** @string Automatic generated */
val filterByCondition get() = filterByCondition()
@@ -454,4 +472,88 @@ object LocaleString {
/** @string Automatic generated */
fun developerNotice(vararg objArrs: Any) = R.string.developer_notice.bind(*objArrs)
/** @string Automatic generated */
val fastRestartProblem get() = fastRestartProblem()
/** @string Automatic generated */
fun fastRestartProblem(vararg objArrs: Any) = R.string.fast_restart_problem.bind(*objArrs)
/** @string Automatic generated */
val userId get() = userId()
/** @string Automatic generated */
fun userId(vararg objArrs: Any) = R.string.user_id.bind(*objArrs)
/** @string Automatic generated */
val unableGetAppErrorsRecordTip get() = unableGetAppErrorsRecordTip()
/** @string Automatic generated */
fun unableGetAppErrorsRecordTip(vararg objArrs: Any) = R.string.unable_get_app_errors_record_tip.bind(*objArrs)
/** @string Automatic generated */
val exportAllLogsSuccess get() = exportAllLogsSuccess()
/** @string Automatic generated */
fun exportAllLogsSuccess(vararg objArrs: Any) = R.string.export_all_logs_success.bind(*objArrs)
/** @string Automatic generated */
val exportAllLogsFail get() = exportAllLogsFail()
/** @string Automatic generated */
fun exportAllLogsFail(vararg objArrs: Any) = R.string.export_all_logs_fail.bind(*objArrs)
/** @string Automatic generated */
val goItNow get() = goItNow()
/** @string Automatic generated */
fun goItNow(vararg objArrs: Any) = R.string.go_it_now.bind(*objArrs)
/** @string Automatic generated */
val accessRootFailTip get() = accessRootFailTip()
/** @string Automatic generated */
fun accessRootFailTip(vararg objArrs: Any) = R.string.access_root_fail_tip.bind(*objArrs)
/** @string Automatic generated */
val globalConfig get() = globalConfig()
/** @string Automatic generated */
fun globalConfig(vararg objArrs: Any) = R.string.global_config.bind(*objArrs)
/** @string Automatic generated */
val followGlobalConfig get() = followGlobalConfig()
/** @string Automatic generated */
fun followGlobalConfig(vararg objArrs: Any) = R.string.follow_global_config.bind(*objArrs)
/** @string Automatic generated */
val batchOperationsNumber get() = batchOperationsNumber()
/** @string Automatic generated */
fun batchOperationsNumber(vararg objArrs: Any) = R.string.batch_operations_number.bind(*objArrs)
/** @string Automatic generated */
val clickToUpdate get() = clickToUpdate()
/** @string Automatic generated */
fun clickToUpdate(vararg objArrs: Any) = R.string.click_to_update.bind(*objArrs)
/** @string Automatic generated */
val latestVersion get() = latestVersion()
/** @string Automatic generated */
fun latestVersion(vararg objArrs: Any) = R.string.latest_version.bind(*objArrs)
/** @string Automatic generated */
val latestVersionTip get() = latestVersionTip()
/** @string Automatic generated */
fun latestVersionTip(vararg objArrs: Any) = R.string.latest_version_tip.bind(*objArrs)
/** @string Automatic generated */
val updateNow get() = updateNow()
/** @string Automatic generated */
fun updateNow(vararg objArrs: Any) = R.string.update_now.bind(*objArrs)
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -23,6 +23,8 @@
package com.fankes.apperrorstracking.ui.activity.base
import android.app.ActivityManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
@@ -32,9 +34,9 @@ import androidx.viewbinding.ViewBinding
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.utils.factory.isNotSystemInDarkMode
import com.fankes.apperrorstracking.utils.factory.toast
import com.highcapable.yukihookapi.hook.factory.current
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.type.android.LayoutInflaterClass
import java.lang.reflect.ParameterizedType
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
@@ -43,15 +45,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
javaClass.genericSuperclass.also { type ->
if (type is ParameterizedType) {
binding = (type.actualTypeArguments[0] as Class<VB>).method {
name = "inflate"
param(LayoutInflaterClass)
}.get().invoke<VB>(layoutInflater) ?: error("binding failed")
setContentView(binding.root)
} else error("binding but got wrong type")
}
binding = current().generic()?.argument()?.method {
name = "inflate"
param(LayoutInflaterClass)
}?.get()?.invoke<VB>(layoutInflater) ?: error("binding failed")
setContentView(binding.root)
/** 隐藏系统的标题栏 */
supportActionBar?.hide()
/** 初始化沉浸状态栏 */
@@ -71,6 +69,24 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
/** 回调 [onCreate] 方法 */
abstract fun onCreate()
/**
* 在旧版的 Android 系统中使用了 activity-alias 标签从启动器启动 Activity 会造成其组件名称 (完整类名) 为代理名称
*
* 为了获取真实的顶层 Activity 组件名称 (完整类名) - 如果名称不正确将自动执行一次结束并重新打开当前 Activity
*/
fun checkingTopComponentName() {
/** 当前顶层的 Activity 组件名称 (完整类名) */
val topComponentName = runCatching {
@Suppress("DEPRECATION")
(getSystemService(ACTIVITY_SERVICE) as? ActivityManager?)
?.getRunningTasks(9999)?.firstOrNull()?.topActivity?.className ?: ""
}.getOrNull() ?: ""
if (topComponentName.isNotBlank() && topComponentName != javaClass.name) {
finish()
startActivity(Intent(this, javaClass))
}
}
/**
* 弹出提示并退出
* @param name 名称

View File

@@ -0,0 +1,186 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/10/4.
*/
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
package com.fankes.apperrorstracking.ui.activity.debug
import android.app.Activity
import android.content.Intent
import android.view.ContextMenu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.databinding.ActivitiyLoggerBinding
import com.fankes.apperrorstracking.databinding.AdapterLoggerBinding
import com.fankes.apperrorstracking.databinding.DiaLoggerFilterBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.utils.factory.*
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
import com.highcapable.yukihookapi.hook.factory.dataChannel
import com.highcapable.yukihookapi.hook.log.YukiHookLogger
import com.highcapable.yukihookapi.hook.log.YukiLoggerData
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.text.SimpleDateFormat
import java.util.*
class LoggerActivity : BaseActivity<ActivitiyLoggerBinding>() {
companion object {
/** 请求保存文件回调标识 */
private const val WRITE_REQUEST_CODE = 0
}
/** 回调适配器改变 */
private var onChanged: (() -> Unit)? = null
/** 过滤条件 */
private var filters = arrayListOf("D", "I", "W", "E")
/** 全部的调试日志数据 */
private val listData = ArrayList<YukiLoggerData>()
override fun onCreate() {
binding.titleBackIcon.setOnClickListener { finish() }
binding.refreshIcon.setOnClickListener { refreshData() }
binding.filterIcon.setOnClickListener {
showDialog<DiaLoggerFilterBinding> {
title = LocaleString.filterByCondition
binding.configCheck0.isChecked = filters.any { it == "D" }
binding.configCheck1.isChecked = filters.any { it == "I" }
binding.configCheck2.isChecked = filters.any { it == "W" }
binding.configCheck3.isChecked = filters.any { it == "E" }
confirmButton {
filters.clear()
if (binding.configCheck0.isChecked) filters.add("D")
if (binding.configCheck1.isChecked) filters.add("I")
if (binding.configCheck2.isChecked) filters.add("W")
if (binding.configCheck3.isChecked) filters.add("E")
refreshData()
}
cancelButton()
}
}
binding.exportAllIcon.setOnClickListener {
runCatching {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/application"
putExtra(Intent.EXTRA_TITLE, "app_errors_tracking_${System.currentTimeMillis().toUtcTime()}.log")
}, WRITE_REQUEST_CODE)
}.onFailure { toast(msg = "Start Android SAF failed") }
}
/** 设置列表元素和 Adapter */
binding.listView.apply {
bindAdapter {
onBindDatas { listData }
onBindViews<AdapterLoggerBinding> { binding, position ->
listData[position].also { bean ->
binding.priorityText.apply {
text = bean.priority
setBackgroundResource(
when (bean.priority) {
"D" -> R.drawable.bg_logger_d_round
"I" -> R.drawable.bg_logger_i_round
"W" -> R.drawable.bg_logger_w_round
"E" -> R.drawable.bg_logger_e_round
else -> R.drawable.bg_logger_d_round
}
)
}
binding.messageText.text = bean.msg.format()
binding.timeText.text = bean.timestamp.format()
binding.throwableText.isVisible = bean.throwable != null
binding.throwableText.text = bean.throwable?.toStackTrace() ?: ""
}
}
}.apply { onChanged = { notifyDataSetChanged() } }
registerForContextMenu(this)
}
}
/** 更新列表数据 */
private fun refreshData() {
dataChannel(FrameworkTool.SYSTEM_FRAMEWORK_NAME).obtainLoggerInMemoryData {
listData.clear()
it.takeIf { e -> e.isNotEmpty() }?.reversed()?.filter { filters.any { e -> it.priority == e } }?.forEach { e -> listData.add(e) }
onChanged?.invoke()
binding.listView.post { binding.listView.setSelection(0) }
binding.exportAllIcon.isVisible = listData.isNotEmpty()
binding.listView.isVisible = listData.isNotEmpty()
binding.listNoDataView.isVisible = listData.isEmpty()
binding.listNoDataView.text = if (filters.size < 4) LocaleString.noListResult else LocaleString.noListData
}
}
/**
* 格式化为本地时间格式
* @return [String]
*/
private fun Long.format() = SimpleDateFormat.getDateTimeInstance().format(Date(this))
/**
* 格式化消息字符串样式
* @return [String]
*/
private fun String.format() = replace("--", "\n--")
/**
* 获取完整的异常堆栈内容
* @return [String]
*/
private fun Throwable.toStackTrace() = ByteArrayOutputStream().also { printStackTrace(PrintStream(it)) }.toString().trim()
override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
menuInflater.inflate(R.menu.menu_logger_action, menu)
super.onCreateContextMenu(menu, v, menuInfo)
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (item.menuInfo is AdapterView.AdapterContextMenuInfo)
(item.menuInfo as? AdapterView.AdapterContextMenuInfo?)?.also {
if (item.itemId == R.id.logger_copy)
copyToClipboard(listData[it.position].let { e -> e.toString() + (e.throwable?.toStackTrace()?.let { t -> "\n$t" } ?: "") })
}
return super.onContextItemSelected(item)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) runCatching {
data?.data?.let {
contentResolver?.openOutputStream(it)?.apply { write(YukiHookLogger.contents(listData).toByteArray()) }?.close()
toast(LocaleString.exportAllLogsSuccess)
} ?: toast(LocaleString.exportAllLogsFail)
}.onFailure { toast(LocaleString.exportAllLogsFail) }
}
override fun onResume() {
super.onResume()
/** 执行更新 */
refreshData()
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -26,9 +26,13 @@ package com.fankes.apperrorstracking.ui.activity.errors
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.bean.AppErrorsInfoBean
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.data.ConfigData.bind
import com.fankes.apperrorstracking.databinding.ActivityAppErrorsDetailBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
@@ -58,8 +62,26 @@ class AppErrorsDetailActivity : BaseActivity<ActivityAppErrorsDetailBinding>() {
private var stackTrace = ""
override fun onCreate() {
val appErrorsInfo = runCatching { intent?.getSerializableExtra(EXTRA_APP_ERRORS_INFO) as? AppErrorsInfoBean }.getOrNull()
val appErrorsInfo = runCatching { intent?.getSerializableExtraCompat<AppErrorsInfoBean>(EXTRA_APP_ERRORS_INFO) }.getOrNull()
?: return toastAndFinish(name = "AppErrorsInfo")
if (appErrorsInfo.isEmpty) {
binding.appPanelScrollView.isVisible = false
showDialog {
title = LocaleString.notice
msg = LocaleString.unableGetAppErrorsRecordTip
confirmButton(LocaleString.gotIt) {
cancel()
finish()
}
cancelButton(LocaleString.goItNow) {
cancel()
finish()
navigate<AppErrorsRecordActivity>()
}
noCancelable()
}
return
}
binding.appInfoItem.setOnClickListener { openSelfSetting(appErrorsInfo.packageName) }
binding.titleBackIcon.setOnClickListener { onBackPressed() }
binding.printIcon.setOnClickListener {
@@ -73,7 +95,7 @@ class AppErrorsDetailActivity : BaseActivity<ActivityAppErrorsDetailBinding>() {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/application"
putExtra(Intent.EXTRA_TITLE, "${appErrorsInfo.packageName}_${appErrorsInfo.timestamp}.log")
putExtra(Intent.EXTRA_TITLE, "${appErrorsInfo.packageName}_${appErrorsInfo.utcTime}.log")
}, WRITE_REQUEST_CODE)
}.onFailure { toast(msg = "Start Android SAF failed") }
}
@@ -83,10 +105,12 @@ class AppErrorsDetailActivity : BaseActivity<ActivityAppErrorsDetailBinding>() {
putExtra(Intent.EXTRA_TEXT, appErrorsInfo.stackOutputShareContent)
}, LocaleString.shareErrorStack))
}
binding.appIcon.setImageDrawable(appIcon(appErrorsInfo.packageName))
binding.appNameText.text = appName(appErrorsInfo.packageName)
binding.appVersionText.text = appVersion(appErrorsInfo.packageName)
binding.appAbiText.text = appCpuAbi(appErrorsInfo.packageName).ifBlank { LocaleString.noCpuAbi }
binding.appIcon.setImageDrawable(appIconOf(appErrorsInfo.packageName))
binding.appNameText.text = appNameOf(appErrorsInfo.packageName)
binding.appVersionText.text = appErrorsInfo.versionBrand
binding.appUserIdText.isVisible = appErrorsInfo.userId > 0
binding.appUserIdText.text = LocaleString.userId(appErrorsInfo.userId)
binding.appCpuAbiText.text = appErrorsInfo.cpuAbi.ifBlank { LocaleString.noCpuAbi }
binding.jvmErrorPanel.isGone = appErrorsInfo.isNativeCrash
binding.errorTypeIcon.setImageResource(if (appErrorsInfo.isNativeCrash) R.drawable.ic_cpp else R.drawable.ic_java)
binding.errorInfoText.text = appErrorsInfo.exceptionMessage
@@ -96,11 +120,26 @@ class AppErrorsDetailActivity : BaseActivity<ActivityAppErrorsDetailBinding>() {
binding.errorThrowMethodText.text = appErrorsInfo.throwMethodName
binding.errorLineNumberText.text = appErrorsInfo.throwLineNumber.toString()
binding.errorRecordTimeText.text = appErrorsInfo.dateTime
binding.errorStackText.text = appErrorsInfo.stackTrace
binding.errorStackTraceMovableText.text = appErrorsInfo.stackTrace
binding.errorStackTraceFixedText.text = appErrorsInfo.stackTrace
binding.disableAutoWrapErrorStackTraceSwitch.bind(ConfigData.DISABLE_AUTO_WRAP_ERROR_STACK_TRACE) {
binding.errorStackTraceScrollView.isVisible = it
binding.errorStackTraceFixedText.isGone = it
resetScrollView()
}
binding.appPanelScrollView.setOnScrollChangeListener { _, _, y, _, _ ->
binding.detailTitleText.text = if (y >= 30.dp(context = this)) appName(appErrorsInfo.packageName) else LocaleString.appName
binding.detailTitleText.text = if (y >= 30.dp(context = this)) appNameOf(appErrorsInfo.packageName) else LocaleString.appName
}
binding.detailTitleText.setOnClickListener { binding.appPanelScrollView.smoothScrollTo(0, 0) }
resetScrollView()
}
/** 修复在一些小屏设备上设置了 [TextView.setTextIsSelectable] 后布局自动上滑问题 */
private fun resetScrollView() {
binding.rootView.post {
binding.appPanelScrollView.scrollTo(0, 0)
binding.errorStackTraceScrollView.scrollTo(0, 0)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -22,16 +22,16 @@
package com.fankes.apperrorstracking.ui.activity.errors
import android.content.Context
import android.os.Build
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.bean.AppErrorsDisplayBean
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.databinding.ActivityAppErrorsDisplayBinding
import com.fankes.apperrorstracking.databinding.DiaAppErrorsDisplayBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.utils.factory.navigate
import com.fankes.apperrorstracking.utils.factory.openSelfSetting
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.factory.toast
import com.fankes.apperrorstracking.utils.factory.*
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
class AppErrorsDisplayActivity : BaseActivity<ActivityAppErrorsDisplayBinding>() {
@@ -56,11 +56,19 @@ class AppErrorsDisplayActivity : BaseActivity<ActivityAppErrorsDisplayBinding>()
override fun onCreate() {
instance?.finish()
instance = this
val appErrorsDisplay = runCatching { intent?.getSerializableExtra(EXTRA_APP_ERRORS_DISPLAY) as? AppErrorsDisplayBean }.getOrNull()
val appErrorsDisplay = runCatching { intent?.getSerializableExtraCompat<AppErrorsDisplayBean>(EXTRA_APP_ERRORS_DISPLAY) }.getOrNull()
?: return toastAndFinish(name = "AppErrorsDisplay")
/** 设置 Material 3 动态颜色主题 */
if (ConfigData.isEnableMaterial3StyleAppErrorsDialog) setTheme(R.style.Theme_AppErrorsTracking_Translucent_Material3)
/** 显示对话框 */
showDialog<DiaAppErrorsDisplayBinding> {
showDialog<DiaAppErrorsDisplayBinding>(ConfigData.isEnableMaterial3StyleAppErrorsDialog.not()) {
title = appErrorsDisplay.title
if (ConfigData.isEnableMaterial3StyleAppErrorsDialog && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
arrayOf(
binding.appInfoIcon, binding.closeAppIcon,
binding.reopenAppIcon, binding.errorDetailIcon,
binding.mutedIfUnlockIcon, binding.mutedIfRestartIcon
).forEach { it.setColorFilter(resources.colorOf(android.R.color.system_accent1_600)) }
binding.processNameText.isVisible = appErrorsDisplay.packageName != appErrorsDisplay.processName
binding.appInfoItem.isVisible = appErrorsDisplay.isShowAppInfoButton
binding.closeAppItem.isVisible = appErrorsDisplay.isShowReopenButton.not() && appErrorsDisplay.isShowCloseAppButton
@@ -72,17 +80,13 @@ class AppErrorsDisplayActivity : BaseActivity<ActivityAppErrorsDisplayBinding>()
}
binding.closeAppItem.setOnClickListener { cancel() }
binding.reopenAppItem.setOnClickListener {
FrameworkTool.openAppUsedFramework(context, appErrorsDisplay.packageName)
FrameworkTool.openAppUsedFramework(context, appErrorsDisplay.packageName, appErrorsDisplay.userId)
cancel()
}
binding.errorDetailItem.setOnClickListener {
FrameworkTool.fetchAppErrorsInfoData(context) { appErrorsInfos ->
appErrorsInfos.takeIf { it.isNotEmpty() }
?.filter { it.packageName == appErrorsDisplay.packageName }
?.takeIf { it.isNotEmpty() }?.get(0)?.let {
AppErrorsDetailActivity.start(context, it)
cancel()
} ?: toast(msg = "No errors founded")
FrameworkTool.fetchAppErrorInfoData(context, appErrorsDisplay.pid) { appErrorsInfo ->
AppErrorsDetailActivity.start(context, appErrorsInfo)
cancel()
}
}
binding.mutedIfUnlockItem.setOnClickListener {

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -29,8 +29,8 @@ import com.fankes.apperrorstracking.databinding.ActivityAppErrorsMutedBinding
import com.fankes.apperrorstracking.databinding.AdapterAppErrorsMutedBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.utils.factory.appIcon
import com.fankes.apperrorstracking.utils.factory.appName
import com.fankes.apperrorstracking.utils.factory.appIconOf
import com.fankes.apperrorstracking.utils.factory.appNameOf
import com.fankes.apperrorstracking.utils.factory.bindAdapter
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
@@ -58,8 +58,8 @@ class AppErrorsMutedActivity : BaseActivity<ActivityAppErrorsMutedBinding>() {
onBindDatas { listData }
onBindViews<AdapterAppErrorsMutedBinding> { binding, position ->
listData[position].also { bean ->
binding.appIcon.setImageDrawable(appIcon(bean.packageName))
binding.appNameText.text = appName(bean.packageName)
binding.appIcon.setImageDrawable(appIconOf(bean.packageName))
binding.appNameText.text = appNameOf(bean.packageName)
binding.muteTypeText.text = when (bean.type) {
MutedErrorsAppBean.MuteType.UNTIL_UNLOCKS -> LocaleString.muteIfUnlock
MutedErrorsAppBean.MuteType.UNTIL_REBOOTS -> LocaleString.muteIfRestart

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -35,6 +35,7 @@ import com.fankes.apperrorstracking.BuildConfig
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.bean.AppErrorsInfoBean
import com.fankes.apperrorstracking.bean.AppFiltersBean
import com.fankes.apperrorstracking.bean.enum.AppFiltersType
import com.fankes.apperrorstracking.databinding.ActivityAppErrorsRecordBinding
import com.fankes.apperrorstracking.databinding.AdapterAppErrorsRecordBinding
import com.fankes.apperrorstracking.databinding.DiaAppErrorsStatisticsBinding
@@ -76,8 +77,8 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
title = LocaleString.notice
progressContent = LocaleString.generatingStatistics
noCancelable()
FrameworkTool.fetchAppListData(context, AppFiltersBean(isContainsSystem = true)) {
Thread {
FrameworkTool.fetchAppListData(context, AppFiltersBean(type = AppFiltersType.ALL)) {
newThread {
val errorsApps = listData.groupBy { it.packageName }
.map { it.key to it.value.size }
.sortedByDescending { it.second }
@@ -94,14 +95,14 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
title = LocaleString.appErrorsStatistics
binding.totalErrorsUnitText.text = LocaleString.totalErrorsUnit(listData.size)
binding.totalAppsUnitText.text = LocaleString.totalAppsUnit(it.size)
binding.mostErrorsAppIcon.setImageDrawable(appIcon(mostAppPackageName))
binding.mostErrorsAppText.text = appName(mostAppPackageName)
binding.mostErrorsAppIcon.setImageDrawable(appIconOf(mostAppPackageName))
binding.mostErrorsAppText.text = appNameOf(mostAppPackageName)
binding.mostErrorsTypeText.text = mostErrorsType
binding.totalPptOfErrorsText.text = "$pptCount%"
confirmButton(LocaleString.gotIt)
}
}
}.start()
}
}
}
}
@@ -132,8 +133,10 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
onBindDatas { listData }
onBindViews<AdapterAppErrorsRecordBinding> { binding, position ->
listData[position].also { bean ->
binding.appIcon.setImageDrawable(appIcon(bean.packageName))
binding.appNameText.text = appName(bean.packageName)
binding.appIcon.setImageDrawable(appIconOf(bean.packageName))
binding.appNameText.text = appNameOf(bean.packageName)
binding.appUserIdText.isVisible = bean.userId > 0
binding.appUserIdText.text = LocaleString.userId(bean.userId)
binding.errorsTimeText.text = bean.crossTime
binding.errorTypeIcon.setImageResource(if (bean.isNativeCrash) R.drawable.ic_cpp else R.drawable.ic_java)
binding.errorTypeText.text = if (bean.isNativeCrash) "Native crash" else bean.exceptionClassName.simpleThwName()
@@ -166,7 +169,7 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
("${cacheDir.absolutePath}/temp").also { path ->
File(path).mkdirs()
listData.takeIf { it.isNotEmpty() }?.forEach {
File("$path/${it.packageName}_${it.timestamp}.log").writeText(it.stackOutputFileContent)
File("$path/${it.packageName}_${it.utcTime}.log").writeText(it.stackOutputFileContent)
}
outPutFilePath = "${cacheDir.absolutePath}/temp_${System.currentTimeMillis()}.zip"
ZipFileTool.zipMultiFile(path, outPutFilePath)
@@ -174,7 +177,7 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/application"
putExtra(Intent.EXTRA_TITLE, "app_errors_info_${System.currentTimeMillis()}.zip")
putExtra(Intent.EXTRA_TITLE, "app_errors_info_${System.currentTimeMillis().toUtcTime()}.zip")
}, WRITE_REQUEST_CODE)
}.onFailure { toast(msg = "Start Android SAF failed") }
}
@@ -191,7 +194,7 @@ class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
* @return [String]
*/
private fun String.simpleThwName() =
let { text -> if (text.contains(other = ".")) text.split(".").let { e -> e[e.lastIndex] } else text }
let { text -> if (text.contains(".")) text.split(".").let { e -> e[e.lastIndex] } else text }
override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
menuInflater.inflate(R.menu.menu_list_detail_action, menu)

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -24,15 +24,18 @@ package com.fankes.apperrorstracking.ui.activity.main
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.bean.AppFiltersBean
import com.fankes.apperrorstracking.bean.AppInfoBean
import com.fankes.apperrorstracking.bean.enum.AppFiltersType
import com.fankes.apperrorstracking.data.AppErrorsConfigData
import com.fankes.apperrorstracking.data.enum.AppErrorsConfigType
import com.fankes.apperrorstracking.databinding.ActivityConfigBinding
import com.fankes.apperrorstracking.databinding.AdapterAppInfoBinding
import com.fankes.apperrorstracking.databinding.DiaAppConfigBinding
import com.fankes.apperrorstracking.databinding.DiaAppsFilterBinding
import com.fankes.apperrorstracking.hook.factory.*
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.utils.factory.appIcon
import com.fankes.apperrorstracking.utils.factory.appIconOf
import com.fankes.apperrorstracking.utils.factory.bindAdapter
import com.fankes.apperrorstracking.utils.factory.newThread
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
@@ -48,37 +51,32 @@ class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
private val listData = ArrayList<AppInfoBean>()
override fun onCreate() {
binding.titleBackIcon.setOnClickListener { onBackPressed() }
binding.titleBackIcon.setOnClickListener { finish() }
binding.globalIcon.setOnClickListener {
showAppConfigDialog(LocaleString.globalConfig, isShowGlobalConfig = false) { type ->
AppErrorsConfigData.putAppShowingType(type)
onChanged?.invoke()
}
}
binding.batchIcon.setOnClickListener {
showDialog<DiaAppConfigBinding> {
title = LocaleString.batchOperations
confirmButton {
val config0 = binding.configRadio0.isChecked
val config1 = binding.configRadio1.isChecked
val config2 = binding.configRadio2.isChecked
val config3 = binding.configRadio3.isChecked
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureApplySiteApps(listData.size)
confirmButton {
listData.takeIf { it.isNotEmpty() }?.forEach {
putAppShowErrorsDialog(it.packageName, config0)
putAppShowErrorsNotify(it.packageName, config1)
putAppShowErrorsToast(it.packageName, config2)
putAppShowNothing(it.packageName, config3)
}
onChanged?.invoke()
}
cancelButton()
showAppConfigDialog(LocaleString.batchOperationsNumber(listData.size), isNotSetDefaultValue = true) { type ->
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureApplySiteApps(listData.size)
confirmButton {
listData.takeIf { it.isNotEmpty() }?.forEach { AppErrorsConfigData.putAppShowingType(type, it.packageName) }
onChanged?.invoke()
}
cancelButton()
}
cancelButton()
}
}
binding.filterIcon.setOnClickListener {
showDialog<DiaAppsFilterBinding> {
title = LocaleString.filterByCondition
binding.containsSystemSwitch.isChecked = appFilters.isContainsSystem
binding.filtersRadioUser.isChecked = appFilters.type == AppFiltersType.USER
binding.filtersRadioSystem.isChecked = appFilters.type == AppFiltersType.SYSTEM
binding.filtersRadioAll.isChecked = appFilters.type == AppFiltersType.ALL
binding.appFiltersEdit.apply {
requestFocus()
invalidate()
@@ -87,15 +85,24 @@ class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
setSelection(appFilters.name.length)
}
}
/** 设置 [AppFiltersBean.type] */
fun setAppFiltersType() {
appFilters.type = when {
binding.filtersRadioUser.isChecked -> AppFiltersType.USER
binding.filtersRadioSystem.isChecked -> AppFiltersType.SYSTEM
binding.filtersRadioAll.isChecked -> AppFiltersType.ALL
else -> error("Invalid app filters type")
}
}
confirmButton {
appFilters.isContainsSystem = binding.containsSystemSwitch.isChecked
setAppFiltersType()
appFilters.name = binding.appFiltersEdit.text.toString().trim()
refreshData()
}
cancelButton()
if (appFilters.name.isNotBlank())
neutralButton(LocaleString.clearFilters) {
appFilters.isContainsSystem = binding.containsSystemSwitch.isChecked
setAppFiltersType()
appFilters.name = ""
refreshData()
}
@@ -109,10 +116,11 @@ class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
binding.appIcon.setImageDrawable(bean.icon)
binding.appNameText.text = bean.name
binding.configTypeText.text = when {
isAppShowErrorsDialog(bean.packageName) -> LocaleString.showErrorsDialog
isAppShowErrorsNotify(bean.packageName) -> LocaleString.showErrorsNotify
isAppShowErrorsToast(bean.packageName) -> LocaleString.showErrorsToast
isAppShowNothing(bean.packageName) -> LocaleString.showNothing
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.GLOBAL, bean.packageName) -> LocaleString.followGlobalConfig
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.DIALOG, bean.packageName) -> LocaleString.showErrorsDialog
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTIFY, bean.packageName) -> LocaleString.showErrorsNotify
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.TOAST, bean.packageName) -> LocaleString.showErrorsToast
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTHING, bean.packageName) -> LocaleString.showNothing
else -> "Unknown type"
}
}
@@ -120,20 +128,9 @@ class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
}.apply { onChanged = { notifyDataSetChanged() } }
setOnItemClickListener { _, _, p, _ ->
listData[p].also { bean ->
showDialog<DiaAppConfigBinding> {
title = bean.name
binding.configRadio0.isChecked = isAppShowErrorsDialog(bean.packageName)
binding.configRadio1.isChecked = isAppShowErrorsNotify(bean.packageName)
binding.configRadio2.isChecked = isAppShowErrorsToast(bean.packageName)
binding.configRadio3.isChecked = isAppShowNothing(bean.packageName)
confirmButton {
putAppShowErrorsDialog(bean.packageName, binding.configRadio0.isChecked)
putAppShowErrorsNotify(bean.packageName, binding.configRadio1.isChecked)
putAppShowErrorsToast(bean.packageName, binding.configRadio2.isChecked)
putAppShowNothing(bean.packageName, binding.configRadio3.isChecked)
onChanged?.invoke()
}
cancelButton()
showAppConfigDialog(bean.name, bean.packageName) { type ->
AppErrorsConfigData.putAppShowingType(type, bean.packageName)
onChanged?.invoke()
}
}
}
@@ -151,32 +148,82 @@ class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
refreshData()
}
/**
* 显示应用配置对话框
* @param title 对话框标题
* @param packageName APP 包名 - 默认空 (空时使用全局配置的默认值)
* @param isNotSetDefaultValue 是否不设置选项的默认值 - 默认否
* @param isShowGlobalConfig 是否显示跟随全局配置选项 - 默认是
* @param result 回调类型结果
*/
private fun showAppConfigDialog(
title: String,
packageName: String = "",
isNotSetDefaultValue: Boolean = false,
isShowGlobalConfig: Boolean = true,
result: (AppErrorsConfigType) -> Unit
) {
showDialog<DiaAppConfigBinding> {
this.title = title
binding.configRadio0.isVisible = isShowGlobalConfig
if (isNotSetDefaultValue.not()) {
if (isShowGlobalConfig) binding.configRadio0.isChecked =
AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.GLOBAL, packageName)
binding.configRadio1.isChecked = AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.DIALOG, packageName)
binding.configRadio2.isChecked = AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTIFY, packageName)
binding.configRadio3.isChecked = AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.TOAST, packageName)
binding.configRadio4.isChecked = AppErrorsConfigData.isAppShowingType(AppErrorsConfigType.NOTHING, packageName)
}
confirmButton {
result(
when {
binding.configRadio0.isChecked -> AppErrorsConfigType.GLOBAL
binding.configRadio1.isChecked -> AppErrorsConfigType.DIALOG
binding.configRadio2.isChecked -> AppErrorsConfigType.NOTIFY
binding.configRadio3.isChecked -> AppErrorsConfigType.TOAST
binding.configRadio4.isChecked -> AppErrorsConfigType.NOTHING
else -> error("Invalid config type")
}
)
FrameworkTool.refreshFrameworkPrefsData(context)
}
cancelButton()
}
}
/** 刷新列表数据 */
private fun refreshData() {
binding.listProgressView.isVisible = true
binding.globalIcon.isVisible = false
binding.batchIcon.isVisible = false
binding.filterIcon.isVisible = false
binding.listView.isVisible = false
binding.listNoDataView.isVisible = false
binding.titleCountText.text = LocaleString.loading
FrameworkTool.fetchAppListData(context = this, appFilters) {
listData.clear()
Thread {
it.takeIf { e -> e.isNotEmpty() }?.forEach { e ->
listData.add(e)
e.icon = appIcon(e.packageName)
/** 设置一个临时变量用于更新列表数据 */
val tempsData = ArrayList<AppInfoBean>()
newThread {
runCatching {
it.takeIf { e -> e.isNotEmpty() }?.forEach { e ->
tempsData.add(e)
e.icon = appIconOf(e.packageName)
}
}
runOnUiThread {
if (isDestroyed.not()) runOnUiThread {
listData.clear()
listData.addAll(tempsData)
onChanged?.invoke()
binding.listView.post { binding.listView.setSelection(0) }
binding.listProgressView.isVisible = false
binding.globalIcon.isVisible = true
binding.batchIcon.isVisible = listData.isNotEmpty()
binding.filterIcon.isVisible = true
binding.listView.isVisible = listData.isNotEmpty()
binding.listNoDataView.isVisible = listData.isEmpty()
binding.titleCountText.text = LocaleString.resultCount(listData.size)
}
}.start()
} else tempsData.clear()
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -23,85 +23,87 @@
package com.fankes.apperrorstracking.ui.activity.main
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.BuildConfig
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.data.DataConst
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.data.ConfigData.bind
import com.fankes.apperrorstracking.databinding.ActivityMainBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.ui.activity.debug.LoggerActivity
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsMutedActivity
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsRecordActivity
import com.fankes.apperrorstracking.utils.factory.navigate
import com.fankes.apperrorstracking.utils.factory.openBrowser
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.factory.toast
import com.fankes.apperrorstracking.utils.factory.*
import com.fankes.apperrorstracking.utils.tool.AppAnalyticsTool.bindAppAnalytics
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
import com.fankes.apperrorstracking.utils.tool.GithubReleaseTool
import com.highcapable.yukihookapi.YukiHookAPI
import com.highcapable.yukihookapi.hook.factory.modulePrefs
class MainActivity : BaseActivity<ActivityMainBinding>() {
companion object {
/** 系统版本 */
private val systemVersion = "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) ${Build.DISPLAY}"
/** 模块是否有效 */
var isModuleValied = false
}
override fun onCreate() {
checkingTopComponentName()
/** 检查更新 */
GithubReleaseTool.checkingForUpdate(context = this, BuildConfig.VERSION_NAME) { version, function ->
binding.mainTextReleaseVersion.apply {
text = LocaleString.clickToUpdate(version)
isVisible = true
setOnClickListener { function() }
}
}
binding.mainTextVersion.text = LocaleString.moduleVersion(BuildConfig.VERSION_NAME)
binding.mainTextSystemVersion.text =
LocaleString.systemVersion("${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) ${Build.DISPLAY}")
binding.onlyShowErrorsInFrontSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_FRONT)
binding.onlyShowErrorsInMainProcessSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_MAIN)
binding.alwaysShowsReopenAppOptionsSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS)
binding.enableAppsConfigsTemplateSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_APP_CONFIG_TEMPLATE)
binding.hideIconInLauncherSwitch.isChecked = modulePrefs.get(DataConst.ENABLE_HIDE_ICON)
binding.mgrAppsConfigsTemplateButton.isVisible = modulePrefs.get(DataConst.ENABLE_APP_CONFIG_TEMPLATE)
binding.mainTextSystemVersion.text = LocaleString.systemVersion(systemVersion)
binding.onlyShowErrorsInFrontSwitch.bind(ConfigData.ENABLE_ONLY_SHOW_ERRORS_IN_FRONT)
binding.onlyShowErrorsInMainProcessSwitch.bind(ConfigData.ENABLE_ONLY_SHOW_ERRORS_IN_MAIN)
binding.alwaysShowsReopenAppOptionsSwitch.bind(ConfigData.ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS)
binding.enableAppsConfigsTemplateSwitch.bind(ConfigData.ENABLE_APP_CONFIG_TEMPLATE) {
binding.mgrAppsConfigsTemplateButton.isVisible = it
}
binding.enableMaterial3AppErrorsDialogSwitch.bind(ConfigData.ENABLE_MATERIAL3_STYLE_APP_ERRORS_DIALOG)
/** 设置桌面图标显示隐藏 */
binding.hideIconInLauncherSwitch.isChecked = isLauncherIconShowing.not()
binding.hideIconInLauncherSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
modulePrefs.put(DataConst.ENABLE_HIDE_ICON, b)
packageManager.setComponentEnabledSetting(
ComponentName(packageName, "${BuildConfig.APPLICATION_ID}.Home"),
if (b) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
hideOrShowLauncherIcon(b)
}
binding.onlyShowErrorsInFrontSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
modulePrefs.put(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_FRONT, b)
}
binding.onlyShowErrorsInMainProcessSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
modulePrefs.put(DataConst.ENABLE_ONLY_SHOW_ERRORS_IN_MAIN, b)
}
binding.alwaysShowsReopenAppOptionsSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
modulePrefs.put(DataConst.ENABLE_ALWAYS_SHOWS_REOPEN_APP_OPTIONS, b)
}
binding.enableAppsConfigsTemplateSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
binding.mgrAppsConfigsTemplateButton.isVisible = b
modulePrefs.put(DataConst.ENABLE_APP_CONFIG_TEMPLATE, b)
/** 设置匿名统计 */
binding.enableAnonymousStatisticsSwitch.bindAppAnalytics()
/** 系统版本点击事件 */
binding.mainTextSystemVersion.setOnClickListener {
showDialog {
title = LocaleString.notice
msg = systemVersion
confirmButton(LocaleString.gotIt)
}
}
/** 管理应用配置模板按钮点击事件 */
binding.mgrAppsConfigsTemplateButton.setOnClickListener { whenActivated { navigate<ConfigureActivity>() } }
/** 功能管理按钮点击事件 */
binding.viewErrorsRecordButton.setOnClickListener { whenActivated { navigate<AppErrorsRecordActivity>() } }
binding.viewMutedErrorsAppsButton.setOnClickListener { whenActivated { navigate<AppErrorsMutedActivity>() } }
/** 调试日志按钮点击事件 */
binding.titleLoggerIcon.setOnClickListener { navigate<LoggerActivity>() }
/** 重启按钮点击事件 */
binding.titleRestartIcon.setOnClickListener { FrameworkTool.restartSystem(context = this) }
/** 项目地址按钮点击事件 */
binding.titleGithubIcon.setOnClickListener { openBrowser(url = "https://github.com/KitsunePie/AppErrorsTracking") }
/** 显示开发者提示 */
if (modulePrefs.get(DataConst.SHOW_DEVELOPER_NOTICE))
if (ConfigData.isShowDeveloperNotice)
showDialog {
title = LocaleString.developerNotice
msg = LocaleString.developerNoticeTip
confirmButton(LocaleString.gotIt) { modulePrefs.put(DataConst.SHOW_DEVELOPER_NOTICE, value = false) }
confirmButton(LocaleString.gotIt) { ConfigData.isShowDeveloperNotice = false }
noCancelable()
}
}
@@ -117,18 +119,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
)
binding.mainImgStatus.setImageResource(
when {
YukiHookAPI.Status.isXposedModuleActive -> R.mipmap.ic_success
else -> R.mipmap.ic_warn
YukiHookAPI.Status.isXposedModuleActive -> R.drawable.ic_success
else -> R.drawable.ic_warn
}
)
binding.mainTextStatus.text =
when {
YukiHookAPI.Status.isXposedModuleActive && isModuleValied.not() -> LocaleString.moduleNotFullyActivated
YukiHookAPI.Status.isXposedModuleActive -> LocaleString.moduleIsActivated
else -> LocaleString.moduleNotActivated
}
binding.mainTextStatus.text = when {
YukiHookAPI.Status.isXposedModuleActive && isModuleValied.not() -> LocaleString.moduleNotFullyActivated
YukiHookAPI.Status.isXposedModuleActive -> LocaleString.moduleIsActivated
else -> LocaleString.moduleNotActivated
}
binding.mainTextApiWay.isVisible = YukiHookAPI.Status.isXposedModuleActive
binding.mainTextApiWay.text = "Activated by ${YukiHookAPI.Status.executorName} API ${YukiHookAPI.Status.executorVersion}"
binding.mainTextApiWay.text = "Activated by ${YukiHookAPI.Status.Executor.name} API ${YukiHookAPI.Status.Executor.apiLevel}"
}
/**

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -19,14 +19,14 @@
*
* This file is Created by fankes on 2022/6/1.
*/
package com.fankes.apperrorstracking.ui.view
package com.fankes.apperrorstracking.ui.widget
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.widget.LinearLayout
import com.fankes.apperrorstracking.utils.drawable.drawabletoolbox.DrawableBuilder
import com.fankes.apperrorstracking.utils.factory.dp
import top.defaults.drawabletoolbox.DrawableBuilder
class ItemLinearLayout(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -21,7 +21,7 @@
*/
@file:Suppress("SameParameterValue")
package com.fankes.apperrorstracking.ui.view
package com.fankes.apperrorstracking.ui.widget
import android.content.Context
import android.content.res.ColorStateList
@@ -29,9 +29,9 @@ import android.graphics.Color
import android.text.TextUtils
import android.util.AttributeSet
import androidx.appcompat.widget.SwitchCompat
import com.fankes.apperrorstracking.utils.drawable.drawabletoolbox.DrawableBuilder
import com.fankes.apperrorstracking.utils.factory.dp
import com.fankes.apperrorstracking.utils.factory.isSystemInDarkMode
import top.defaults.drawabletoolbox.DrawableBuilder
class MaterialSwitch(context: Context, attrs: AttributeSet?) : SwitchCompat(context, attrs) {

View File

@@ -1,292 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("SameParameterValue")
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.RotateDrawable
import android.os.Build
import java.lang.reflect.Field
import java.lang.reflect.Method
private val gradientState = resolveGradientState()
private fun resolveGradientState(): Class<*> {
val classes = GradientDrawable::class.java.declaredClasses
for (singleClass in classes) {
if (singleClass.simpleName == "GradientState") return singleClass
}
throw RuntimeException("GradientState could not be found in thisAny GradientDrawable implementation")
}
private val rotateState = resolveRotateState()
private fun resolveRotateState(): Class<*> {
val classes = RotateDrawable::class.java.declaredClasses
for (singleClass in classes) {
if (singleClass.simpleName == "RotateState") return singleClass
}
throw RuntimeException("RotateState could not be found in thisAny RotateDrawable implementation")
}
@Throws(SecurityException::class, NoSuchFieldException::class)
private fun resolveField(source: Class<*>, fieldName: String): Field {
val field = source.getDeclaredField(fieldName)
field.isAccessible = true
return field
}
@Throws(SecurityException::class, NoSuchMethodException::class)
private fun resolveMethod(
source: Class<*>,
methodName: String,
vararg parameterTypes: Class<*>
): Method {
val method = source.getDeclaredMethod(methodName, *parameterTypes)
method.isAccessible = true
return method
}
fun setInnerRadius(drawable: GradientDrawable, value: Int) {
try {
val innerRadius = resolveField(gradientState, "mInnerRadius")
innerRadius.setInt(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setInnerRadiusRatio(drawable: GradientDrawable, value: Float) {
try {
val innerRadius = resolveField(gradientState, "mInnerRadiusRatio")
innerRadius.setFloat(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setThickness(drawable: GradientDrawable, value: Int) {
try {
val innerRadius = resolveField(gradientState, "mThickness")
innerRadius.setInt(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setThicknessRatio(drawable: GradientDrawable, value: Float) {
try {
val innerRadius = resolveField(gradientState, "mThicknessRatio")
innerRadius.setFloat(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setUseLevelForShape(drawable: GradientDrawable, value: Boolean) {
try {
val useLevelForShape = resolveField(gradientState, "mUseLevelForShape")
useLevelForShape.setBoolean(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
@SuppressLint("ObsoleteSdkInt")
fun setOrientation(drawable: GradientDrawable, value: GradientDrawable.Orientation) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
drawable.orientation = value
} else {
try {
val orientation = resolveField(gradientState, "mOrientation")
orientation.set(drawable.constantState, value)
val rectIdDirty = resolveField(GradientDrawable::class.java, "mRectIsDirty")
rectIdDirty.setBoolean(drawable, true)
drawable.invalidateSelf()
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
@SuppressLint("ObsoleteSdkInt")
fun setColors(drawable: GradientDrawable, value: IntArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
drawable.colors = value
} else {
try {
val colors = resolveField(gradientState, "mColors")
colors.set(drawable.constantState, value)
drawable.invalidateSelf()
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setGradientRadiusType(drawable: GradientDrawable, value: Int) {
try {
val type = resolveField(gradientState, "mGradientRadiusType")
type.setInt(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setGradientRadius(drawable: GradientDrawable, value: Float) {
try {
val gradientRadius = resolveField(gradientState, "mGradientRadius")
gradientRadius.setFloat(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setStrokeColor(drawable: GradientDrawable, value: Int) {
try {
val type = resolveField(gradientState, "mStrokeColor")
type.setInt(drawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
fun setDrawable(rotateDrawable: RotateDrawable, drawable: Drawable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
rotateDrawable.drawable = drawable
} else {
try {
val drawableField = resolveField(rotateState, "mDrawable")
val stateField = resolveField(RotateDrawable::class.java, "mState")
drawableField.set(stateField.get(rotateDrawable), drawable)
drawable.callback = rotateDrawable
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setPivotX(rotateDrawable: RotateDrawable, value: Float) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
rotateDrawable.pivotX = value
} else {
try {
val pivotXField = resolveField(rotateState, "mPivotX")
pivotXField.setFloat(rotateDrawable.constantState, value)
val pivotXRelField = resolveField(rotateState, "mPivotXRel")
pivotXRelField.setBoolean(rotateDrawable.constantState, true)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setPivotY(rotateDrawable: RotateDrawable, value: Float) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
rotateDrawable.pivotY = value
} else {
try {
val pivotYField = resolveField(rotateState, "mPivotY")
pivotYField.setFloat(rotateDrawable.constantState, value)
val pivotYRelField = resolveField(rotateState, "mPivotYRel")
pivotYRelField.setBoolean(rotateDrawable.constantState, true)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setFromDegrees(rotateDrawable: RotateDrawable, value: Float) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
rotateDrawable.fromDegrees = value
} else {
try {
val fromDegreesField = resolveField(rotateState, "mFromDegrees")
fromDegreesField.setFloat(rotateDrawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setToDegrees(rotateDrawable: RotateDrawable, value: Float) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
rotateDrawable.toDegrees = value
} else {
try {
val toDegreesField = resolveField(rotateState, "mToDegrees")
toDegreesField.setFloat(rotateDrawable.constantState, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
fun setRadius(rippleDrawable: RippleDrawable, value: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rippleDrawable.radius = value
} else {
try {
val setRadiusMethod =
resolveMethod(RippleDrawable::class.java, "setMaxRadius", Int::class.java)
setRadiusMethod.invoke(rippleDrawable, value)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}

View File

@@ -1,489 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.util.StateSet
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
class DrawableBuilder {
private var properties = DrawableProperties()
private var order: AtomicInteger = AtomicInteger(1)
private var transformsMap = TreeMap<Int, (Drawable) -> Drawable>()
private var baseDrawable: Drawable? = null
fun batch(properties: DrawableProperties) = apply { this.properties = properties.copy() }
fun baseDrawable(drawable: Drawable) = apply { baseDrawable = drawable }
// <shape>
fun shape(shape: Int) = apply { properties.shape = shape }
fun rectangle() = apply { shape(GradientDrawable.RECTANGLE) }
fun oval() = apply { shape(GradientDrawable.OVAL) }
fun line() = apply { shape(GradientDrawable.LINE) }
fun ring() = apply { shape(GradientDrawable.RING) }
fun innerRadius(innerRadius: Int) = apply { properties.innerRadius = innerRadius }
fun innerRadiusRatio(innerRadiusRatio: Float) =
apply { properties.innerRadiusRatio = innerRadiusRatio }
fun thickness(thickness: Int) = apply { properties.thickness = thickness }
fun thicknessRatio(thicknessRatio: Float) = apply { properties.thicknessRatio = thicknessRatio }
fun useLevelForRing(use: Boolean = true) = apply { properties.useLevelForRing = use }
// <corner>
fun cornerRadius(cornerRadius: Int) = apply { properties.cornerRadius = cornerRadius }
fun topLeftRadius(topLeftRadius: Int) = apply { properties.topLeftRadius = topLeftRadius }
fun topRightRadius(topRightRadius: Int) = apply { properties.topRightRadius = topRightRadius }
fun bottomRightRadius(bottomRightRadius: Int) =
apply { properties.bottomRightRadius = bottomRightRadius }
fun bottomLeftRadius(bottomLeftRadius: Int) =
apply { properties.bottomLeftRadius = bottomLeftRadius }
fun rounded() = apply { cornerRadius(Int.MAX_VALUE) }
fun cornerRadii(
topLeftRadius: Int,
topRightRadius: Int,
bottomRightRadius: Int,
bottomLeftRadius: Int
) = apply {
topLeftRadius(topLeftRadius); topRightRadius(topRightRadius); bottomRightRadius(
bottomRightRadius
); bottomLeftRadius(bottomLeftRadius)
}
// <gradient>
fun gradient(useGradient: Boolean = true) = apply { properties.useGradient = useGradient }
fun gradientType(type: Int) = apply { properties.type = type }
fun linearGradient() = apply { gradientType(GradientDrawable.LINEAR_GRADIENT) }
fun radialGradient() = apply { gradientType(GradientDrawable.RADIAL_GRADIENT) }
fun sweepGradient() = apply { gradientType(GradientDrawable.SWEEP_GRADIENT) }
fun angle(angle: Int) = apply { properties.angle = angle }
fun centerX(centerX: Float) = apply { properties.centerX = centerX }
fun centerY(centerY: Float) = apply { properties.centerY = centerY }
fun center(centerX: Float, centerY: Float) = apply { centerX(centerX); centerY(centerY) }
fun useCenterColor(useCenterColor: Boolean = true) =
apply { properties.useCenterColor = useCenterColor }
fun startColor(startColor: Int) = apply { properties.startColor = startColor }
fun centerColor(centerColor: Int) = apply {
properties.centerColor = centerColor
useCenterColor(true)
}
fun endColor(endColor: Int) = apply { properties.endColor = endColor }
fun gradientColors(startColor: Int, endColor: Int, centerColor: Int?) = apply {
startColor(startColor); endColor(endColor)
useCenterColor(centerColor != null)
centerColor?.let {
centerColor(it)
}
}
fun gradientRadiusType(gradientRadiusType: Int) =
apply { properties.gradientRadiusType = gradientRadiusType }
fun gradientRadius(gradientRadius: Float) = apply { properties.gradientRadius = gradientRadius }
fun gradientRadius(radius: Float, type: Int) =
apply { gradientRadius(radius); gradientRadiusType(type) }
fun gradientRadiusInPixel(radius: Float) =
apply { gradientRadius(radius); gradientRadiusType(DrawableProperties.RADIUS_TYPE_PIXELS) }
fun gradientRadiusInFraction(radius: Float) =
apply { gradientRadius(radius); gradientRadiusType(DrawableProperties.RADIUS_TYPE_FRACTION) }
fun useLevelForGradient(use: Boolean) = apply { properties.useLevelForGradient = use }
fun useLevelForGradient() = apply { useLevelForGradient(true) }
// <size>
fun width(width: Int) = apply { properties.width = width }
fun height(height: Int) = apply { properties.height = height }
fun size(width: Int, height: Int) = apply { width(width); height(height) }
fun size(size: Int) = apply { width(size).height(size) }
// <solid>
fun solidColor(solidColor: Int) = apply { properties.solidColor = solidColor }
private var solidColorPressed: Int? = null
fun solidColorPressed(color: Int?) = apply { solidColorPressed = color }
private var solidColorPressedWhenRippleUnsupported: Int? = null
fun solidColorPressedWhenRippleUnsupported(color: Int?) =
apply { solidColorPressedWhenRippleUnsupported = color }
private var solidColorDisabled: Int? = null
fun solidColorDisabled(color: Int?) = apply { solidColorDisabled = color }
private var solidColorSelected: Int? = null
fun solidColorSelected(color: Int?) = apply { solidColorSelected = color }
fun solidColorStateList(colorStateList: ColorStateList) =
apply { properties.solidColorStateList = colorStateList }
// <stroke>
fun strokeWidth(strokeWidth: Int) = apply { properties.strokeWidth = strokeWidth }
fun strokeColor(strokeColor: Int) = apply { properties.strokeColor = strokeColor }
private var strokeColorPressed: Int? = null
fun strokeColorPressed(color: Int?) = apply { strokeColorPressed = color }
private var strokeColorDisabled: Int? = null
fun strokeColorDisabled(color: Int?) = apply { strokeColorDisabled = color }
private var strokeColorSelected: Int? = null
fun strokeColorSelected(color: Int?) = apply { strokeColorSelected = color }
fun strokeColorStateList(colorStateList: ColorStateList) =
apply { properties.strokeColorStateList = colorStateList }
fun dashWidth(dashWidth: Int) = apply { properties.dashWidth = dashWidth }
fun dashGap(dashGap: Int) = apply { properties.dashGap = dashGap }
fun hairlineBordered() = apply { strokeWidth(1) }
fun shortDashed() = apply { dashWidth(12).dashGap(12) }
fun mediumDashed() = apply { dashWidth(24).dashGap(24) }
fun longDashed() = apply { dashWidth(36).dashGap(36) }
fun dashed() = apply { mediumDashed() }
// <rotate>
private var rotateOrder = 0
fun rotate(boolean: Boolean = true) = apply {
properties.useRotate = boolean
rotateOrder = if (boolean) {
order.getAndIncrement()
} else {
0
}
}
fun pivotX(pivotX: Float) = apply { properties.pivotX = pivotX }
fun pivotY(pivotY: Float) = apply { properties.pivotY = pivotY }
fun pivot(pivotX: Float, pivotY: Float) = apply { pivotX(pivotX).pivotY(pivotY) }
fun fromDegrees(degrees: Float) = apply { properties.fromDegrees = degrees }
fun toDegrees(degrees: Float) = apply { properties.toDegrees = degrees }
fun degrees(fromDegrees: Float, toDegrees: Float) =
apply { fromDegrees(fromDegrees).toDegrees(toDegrees) }
fun degrees(degrees: Float) = apply { fromDegrees(degrees).toDegrees(degrees) }
fun rotate(fromDegrees: Float, toDegrees: Float) =
apply { rotate().fromDegrees(fromDegrees).toDegrees(toDegrees) }
fun rotate(degrees: Float) = apply { rotate().degrees(degrees) }
// <scale>
private var scaleOrder = 0
fun scale(boolean: Boolean = true) = apply {
properties.useScale = boolean
scaleOrder = if (boolean) {
order.getAndIncrement()
} else {
0
}
}
fun scaleLevel(level: Int) = apply { properties.scaleLevel = level }
fun scaleGravity(gravity: Int) = apply { properties.scaleGravity = gravity }
fun scaleWidth(scale: Float) = apply { properties.scaleWidth = scale }
fun scaleHeight(scale: Float) = apply { properties.scaleHeight = scale }
fun scale(scale: Float) = apply { scale().scaleWidth(scale).scaleHeight(scale) }
fun scale(scaleWidth: Float, scaleHeight: Float) =
apply { scale().scaleWidth(scaleWidth).scaleHeight(scaleHeight) }
// flip
fun flip(boolean: Boolean = true) = apply { properties.useFlip = boolean }
fun orientation(orientation: Int) = apply { properties.orientation = orientation }
fun flipVertical() = apply { flip().orientation(FlipDrawable.ORIENTATION_VERTICAL) }
// <ripple>
fun ripple(boolean: Boolean = true) = apply { properties.useRipple = boolean }
fun rippleColor(color: Int) = apply { properties.rippleColor = color }
fun rippleColorStateList(colorStateList: ColorStateList) =
apply { properties.rippleColorStateList = colorStateList }
fun rippleRadius(radius: Int) = apply { properties.rippleRadius = radius }
fun build(): Drawable {
if (baseDrawable != null) {
return wrap(baseDrawable!!)
}
var drawable: Drawable
// fall back when ripple is unavailable on devices with API < 21
if (shouldFallbackRipple()) {
if (solidColorPressedWhenRippleUnsupported != null) {
solidColorPressed(solidColorPressedWhenRippleUnsupported)
} else {
solidColorPressed(properties.rippleColor)
}
}
if (needStateListDrawable()) {
drawable = StateListDrawableBuilder()
.pressed(buildPressedDrawable())
.disabled(buildDisabledDrawable())
.selected(buildSelectedDrawable())
.normal(buildNormalDrawable())
.build()
} else {
drawable = GradientDrawable()
setupGradientDrawable(drawable)
}
drawable = wrap(drawable)
return drawable
}
private fun getSolidColorStateList(): ColorStateList {
if (properties.solidColorStateList != null) {
return properties.solidColorStateList!!
}
val states = mutableListOf<IntArray>()
val colors = mutableListOf<Int>()
solidColorPressed?.let {
states.add(intArrayOf(android.R.attr.state_pressed))
colors.add(it)
}
solidColorDisabled?.let {
states.add(intArrayOf(-android.R.attr.state_enabled))
colors.add(it)
}
solidColorSelected?.let {
states.add(intArrayOf(android.R.attr.state_selected))
colors.add(it)
}
states.add(StateSet.WILD_CARD)
colors.add(properties.solidColor)
return ColorStateList(states.toTypedArray(), colors.toIntArray())
}
private fun getStrokeColorStateList(): ColorStateList {
if (properties.strokeColorStateList != null) {
return properties.strokeColorStateList!!
}
val states = mutableListOf<IntArray>()
val colors = mutableListOf<Int>()
strokeColorPressed?.let {
states.add(intArrayOf(android.R.attr.state_pressed))
colors.add(it)
}
strokeColorDisabled?.let {
states.add(intArrayOf(-android.R.attr.state_enabled))
colors.add(it)
}
strokeColorSelected?.let {
states.add(intArrayOf(android.R.attr.state_selected))
colors.add(it)
}
states.add(StateSet.WILD_CARD)
colors.add(properties.strokeColor)
return ColorStateList(states.toTypedArray(), colors.toIntArray())
}
private fun buildPressedDrawable(): Drawable? {
if (solidColorPressed == null && strokeColorPressed == null) return null
val pressedDrawable = GradientDrawable()
setupGradientDrawable(pressedDrawable)
solidColorPressed?.let {
pressedDrawable.setColor(it)
}
strokeColorPressed?.let {
setStrokeColor(pressedDrawable, it)
}
return pressedDrawable
}
private fun buildDisabledDrawable(): Drawable? {
if (solidColorDisabled == null && strokeColorDisabled == null) return null
val disabledDrawable = GradientDrawable()
setupGradientDrawable(disabledDrawable)
solidColorDisabled?.let {
disabledDrawable.setColor(it)
}
strokeColorDisabled?.let {
setStrokeColor(disabledDrawable, it)
}
return disabledDrawable
}
private fun buildSelectedDrawable(): Drawable? {
if (solidColorSelected == null && strokeColorSelected == null) return null
val selectedDrawable = GradientDrawable()
setupGradientDrawable(selectedDrawable)
solidColorSelected?.let {
selectedDrawable.setColor(it)
}
strokeColorSelected?.let {
setStrokeColor(selectedDrawable, it)
}
return selectedDrawable
}
private fun buildNormalDrawable(): Drawable {
val pressedDrawable = GradientDrawable()
setupGradientDrawable(pressedDrawable)
return pressedDrawable
}
private fun setupGradientDrawable(drawable: GradientDrawable) {
properties.apply {
drawable.shape = shape
if (shape == GradientDrawable.RING) {
setInnerRadius(drawable, innerRadius)
setInnerRadiusRatio(drawable, innerRadiusRatio)
setThickness(drawable, thickness)
setThicknessRatio(drawable, thicknessRatio)
setUseLevelForShape(drawable, useLevelForRing)
}
drawable.cornerRadii = getCornerRadii()
if (useGradient) {
drawable.gradientType = type
setGradientRadiusType(drawable, gradientRadiusType)
setGradientRadius(drawable, gradientRadius)
drawable.setGradientCenter(centerX, centerY)
setOrientation(drawable, getOrientation())
setColors(drawable, getColors())
drawable.useLevel = useLevelForGradient
} else {
drawable.color = getSolidColorStateList()
}
drawable.setSize(width, height)
drawable.setStroke(
strokeWidth,
getStrokeColorStateList(),
dashWidth.toFloat(),
dashGap.toFloat()
)
}
}
private fun needStateListDrawable(): Boolean {
return (hasStrokeColorStateList() || (!properties.useGradient && hasSolidColorStateList()))
}
private fun needRotateDrawable(): Boolean {
return properties.useRotate &&
!(properties.pivotX == 0.5f && properties.pivotY == 0.5f
&& properties.fromDegrees == 0f && properties.toDegrees == 0f)
}
private fun needScaleDrawable(): Boolean {
return properties.useScale
}
private fun wrap(drawable: Drawable): Drawable {
var wrappedDrawable = drawable
if (rotateOrder > 0) {
transformsMap[rotateOrder] = ::wrapRotateIfNeeded
}
if (scaleOrder > 0) {
transformsMap[scaleOrder] = ::wrapScaleIfNeeded
}
for (action in transformsMap.values) {
wrappedDrawable = action.invoke(wrappedDrawable)
}
if (properties.useFlip) {
wrappedDrawable = FlipDrawableBuilder()
.drawable(wrappedDrawable)
.orientation(properties.orientation)
.build()
}
if (isRippleSupported() && properties.useRipple) {
wrappedDrawable = RippleDrawableBuilder()
.drawable(wrappedDrawable)
.color(properties.rippleColor)
.colorStateList(properties.rippleColorStateList)
.radius(properties.rippleRadius)
.build()
}
return wrappedDrawable
}
private fun shouldFallbackRipple(): Boolean {
return properties.useRipple && !isRippleSupported()
}
private fun isRippleSupported() = true
private fun wrapRotateIfNeeded(drawable: Drawable): Drawable {
if (!needRotateDrawable()) return drawable
with(properties) {
return RotateDrawableBuilder()
.drawable(drawable)
.pivotX(pivotX)
.pivotY(pivotY)
.fromDegrees(fromDegrees)
.toDegrees(toDegrees)
.build()
}
}
private fun wrapScaleIfNeeded(drawable: Drawable): Drawable {
if (!needScaleDrawable()) return drawable
with(properties) {
return ScaleDrawableBuilder()
.drawable(drawable)
.level(scaleLevel)
.scaleGravity(scaleGravity)
.scaleWidth(scaleWidth)
.scaleHeight(scaleHeight)
.build()
}
}
private fun hasSolidColorStateList(): Boolean {
return solidColorPressed != null || solidColorDisabled != null || solidColorSelected != null
}
private fun hasStrokeColorStateList(): Boolean {
return strokeColorPressed != null || strokeColorDisabled != null || strokeColorSelected != null
}
}

View File

@@ -1,221 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("SetterBackingFieldAssignment", "unused")
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Parcel
import android.os.Parcelable
import android.view.Gravity
import java.io.Serializable
data class DrawableProperties(
// <shape>
@JvmField var shape: Int = GradientDrawable.RECTANGLE,
@JvmField var innerRadius: Int = -1,
@JvmField var innerRadiusRatio: Float = 9f,
@JvmField var thickness: Int = -1,
@JvmField var thicknessRatio: Float = 3f,
@JvmField var useLevelForRing: Boolean = false,
// <corner>
private var _cornerRadius: Int = 0,
@JvmField var topLeftRadius: Int = 0,
@JvmField var topRightRadius: Int = 0,
@JvmField var bottomRightRadius: Int = 0,
@JvmField var bottomLeftRadius: Int = 0,
// <gradient>
@JvmField var useGradient: Boolean = false,
@JvmField var type: Int = GradientDrawable.RADIAL_GRADIENT,
@JvmField var angle: Int = 0,
@JvmField var centerX: Float = 0.5f,
@JvmField var centerY: Float = 0.5f,
@JvmField var useCenterColor: Boolean = false,
@JvmField var startColor: Int = Constants.DEFAULT_COLOR,
@JvmField var centerColor: Int? = null,
@JvmField var endColor: Int = 0x7FFFFFFF,
@JvmField var gradientRadiusType: Int = RADIUS_TYPE_FRACTION,
@JvmField var gradientRadius: Float = 0.5f,
@JvmField var useLevelForGradient: Boolean = false,
// <size>
@JvmField var width: Int = -1,
@JvmField var height: Int = -1,
// <solid>
@JvmField var solidColor: Int = Color.TRANSPARENT,
@JvmField var solidColorStateList: ColorStateList? = null,
// <stroke>
@JvmField var strokeWidth: Int = 0,
@JvmField var strokeColor: Int = Color.DKGRAY,
@JvmField var strokeColorStateList: ColorStateList? = null,
@JvmField var dashWidth: Int = 0,
@JvmField var dashGap: Int = 0,
// <rotate>
@JvmField var useRotate: Boolean = false,
@JvmField var pivotX: Float = 0.5f,
@JvmField var pivotY: Float = 0.5f,
@JvmField var fromDegrees: Float = 0f,
@JvmField var toDegrees: Float = 0f,
// <scale>
@JvmField var useScale: Boolean = false,
@JvmField var scaleLevel: Int = 10000,
@JvmField var scaleGravity: Int = Gravity.CENTER,
@JvmField var scaleWidth: Float = 0f,
@JvmField var scaleHeight: Float = 0f,
// flip
@JvmField var useFlip: Boolean = false,
@JvmField var orientation: Int = FlipDrawable.ORIENTATION_HORIZONTAL,
// ripple
@JvmField var useRipple: Boolean = false,
@JvmField var rippleColor: Int = Constants.DEFAULT_COLOR,
@JvmField var rippleColorStateList: ColorStateList? = null,
@JvmField var rippleRadius: Int = -1
) : Serializable {
companion object {
const val RADIUS_TYPE_PIXELS = 0
const val RADIUS_TYPE_FRACTION = 1
@JvmField
val CREATOR = object : Parcelable.Creator<DrawableProperties> {
override fun createFromParcel(parcel: Parcel): DrawableProperties {
return DrawableProperties(parcel)
}
override fun newArray(size: Int): Array<DrawableProperties?> {
return arrayOfNulls(size)
}
}
}
var cornerRadius: Int = _cornerRadius
set(value) {
_cornerRadius = value
topLeftRadius = value
topRightRadius = value
bottomRightRadius = value
bottomLeftRadius = value
}
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readInt(),
parcel.readFloat(),
parcel.readInt(),
parcel.readFloat(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readInt(),
parcel.readFloat(),
parcel.readFloat(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readInt(),
parcel.readInt(),
parcel.readFloat(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readParcelable(ColorStateList::class.java.classLoader),
parcel.readInt(),
parcel.readInt(),
parcel.readParcelable(ColorStateList::class.java.classLoader),
parcel.readInt(),
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readFloat(),
parcel.readFloat(),
parcel.readFloat(),
parcel.readFloat(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readInt(),
parcel.readFloat(),
parcel.readFloat(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readInt(),
parcel.readParcelable(ColorStateList::class.java.classLoader),
parcel.readInt()
)
fun copy(): DrawableProperties {
val parcel = Parcel.obtain()
parcel.setDataPosition(0)
val properties = CREATOR.createFromParcel(parcel)
parcel.recycle()
return properties
}
fun getCornerRadii(): FloatArray {
return floatArrayOf(
topLeftRadius.toFloat(), topLeftRadius.toFloat(),
topRightRadius.toFloat(), topRightRadius.toFloat(),
bottomRightRadius.toFloat(), bottomRightRadius.toFloat(),
bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat()
)
}
fun getOrientation(): GradientDrawable.Orientation {
val orientation: GradientDrawable.Orientation = when (val angle = this.angle % 360) {
0 -> GradientDrawable.Orientation.LEFT_RIGHT
45 -> GradientDrawable.Orientation.BL_TR
90 -> GradientDrawable.Orientation.BOTTOM_TOP
135 -> GradientDrawable.Orientation.BR_TL
180 -> GradientDrawable.Orientation.RIGHT_LEFT
225 -> GradientDrawable.Orientation.TR_BL
270 -> GradientDrawable.Orientation.TOP_BOTTOM
315 -> GradientDrawable.Orientation.TL_BR
else -> throw IllegalArgumentException("Unsupported angle: $angle")
}
return orientation
}
fun getColors(): IntArray {
return if (useCenterColor && centerColor != null) {
intArrayOf(startColor, centerColor!!, endColor)
} else intArrayOf(startColor, endColor)
}
fun materialization(): Drawable = DrawableBuilder().batch(this).build()
}

View File

@@ -1,78 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("DEPRECATION", "CanvasSize", "OVERRIDE_DEPRECATION")
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Rect
import android.graphics.drawable.Drawable
class FlipDrawable(
private var drawable: Drawable,
private var orientation: Int = ORIENTATION_HORIZONTAL
) : Drawable() {
companion object {
const val ORIENTATION_HORIZONTAL = 0
const val ORIENTATION_VERTICAL = 1
}
override fun draw(canvas: Canvas) {
val saveCount = canvas.save()
if (orientation == ORIENTATION_VERTICAL) {
canvas.scale(1f, -1f, (canvas.width / 2).toFloat(), (canvas.height / 2).toFloat())
} else {
canvas.scale(-1f, 1f, (canvas.width / 2).toFloat(), (canvas.height / 2).toFloat())
}
drawable.bounds = Rect(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
canvas.restoreToCount(saveCount)
}
override fun onLevelChange(level: Int): Boolean {
drawable.level = level
invalidateSelf()
return true
}
override fun getIntrinsicWidth(): Int {
return drawable.intrinsicWidth
}
override fun getIntrinsicHeight(): Int {
return drawable.intrinsicHeight
}
override fun setAlpha(alpha: Int) {
drawable.alpha = alpha
}
override fun getOpacity(): Int {
return drawable.opacity
}
override fun setColorFilter(colorFilter: ColorFilter?) {
drawable.colorFilter = colorFilter
}
}

View File

@@ -1,181 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.view.Gravity
import androidx.annotation.RequiresApi
class LayerDrawableBuilder {
companion object {
const val DIMEN_UNDEFINED = Int.MIN_VALUE
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private var paddingMode = LayerDrawable.PADDING_MODE_NEST
private var paddingLeft = 0
private var paddingTop = 0
private var paddingRight = 0
private var paddingBottom = 0
private var paddingStart = 0
private var paddingEnd = 0
private val layers = ArrayList<Layer>()
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun paddingMode(mode: Int) = apply { paddingMode = mode }
fun paddingLeft(padding: Int) = apply { paddingLeft = padding }
fun paddingTop(padding: Int) = apply { paddingTop = padding }
fun paddingRight(padding: Int) = apply { paddingRight = padding }
fun paddingBottom(padding: Int) = apply { paddingBottom = padding }
@RequiresApi(Build.VERSION_CODES.M)
fun paddingStart(padding: Int) = apply { paddingStart = padding }
@RequiresApi(Build.VERSION_CODES.M)
fun paddingEnd(padding: Int) = apply { paddingEnd = padding }
fun padding(padding: Int) = apply {
paddingLeft(padding).paddingTop(padding).paddingRight(padding).paddingBottom(padding)
}
@RequiresApi(Build.VERSION_CODES.M)
fun paddingRelative(padding: Int) = apply {
paddingStart(padding).paddingTop(padding).paddingEnd(padding).paddingBottom(padding)
}
fun add(drawable: Drawable) = apply { layers.add(Layer(drawable)) }
fun width(width: Int) = apply { layers.last().width = width }
fun height(height: Int) = apply { layers.last().height = height }
fun insetLeft(inset: Int) = apply { layers.last().insetLeft = inset }
fun insetTop(inset: Int) = apply { layers.last().insetTop = inset }
fun insetRight(inset: Int) = apply { layers.last().insetRight = inset }
fun insetBottom(inset: Int) = apply { layers.last().insetBottom = inset }
@RequiresApi(Build.VERSION_CODES.M)
fun insetStart(inset: Int) = apply { layers.last().insetStart = inset }
@RequiresApi(Build.VERSION_CODES.M)
fun insetEnd(inset: Int) = apply { layers.last().insetEnd = inset }
fun inset(inset: Int) =
apply { insetLeft(inset).insetTop(inset).insetRight(inset).insetBottom(inset) }
fun inset(insetLeft: Int, insetTop: Int, insetRight: Int, insetBottom: Int) = apply {
insetLeft(insetLeft).insetTop(insetTop).insetRight(insetRight).insetBottom(insetBottom)
}
@RequiresApi(Build.VERSION_CODES.M)
fun insetRelative(inset: Int) =
apply { insetStart(inset).insetTop(inset).insetEnd(inset).insetBottom(inset) }
@RequiresApi(Build.VERSION_CODES.M)
fun insetRelative(insetStart: Int, insetTop: Int, insetEnd: Int, insetBottom: Int) = apply {
insetStart(insetStart).insetTop(insetTop).insetEnd(insetEnd).insetBottom(insetBottom)
}
@RequiresApi(Build.VERSION_CODES.M)
fun gravity(gravity: Int) = apply { layers.last().gravity = gravity }
fun modify(
index: Int, drawable: Drawable,
width: Int = DIMEN_UNDEFINED,
height: Int = DIMEN_UNDEFINED,
insetLeft: Int = DIMEN_UNDEFINED,
insetTop: Int = DIMEN_UNDEFINED,
insetRight: Int = DIMEN_UNDEFINED,
insetBottom: Int = DIMEN_UNDEFINED,
insetStart: Int = DIMEN_UNDEFINED,
insetEnd: Int = DIMEN_UNDEFINED
) =
apply {
val layer = layers[index]
layer.drawable = drawable
if (width != DIMEN_UNDEFINED) layer.width = width
if (height != DIMEN_UNDEFINED) layer.height = height
if (insetLeft != DIMEN_UNDEFINED) layer.insetLeft = insetLeft
if (insetTop != DIMEN_UNDEFINED) layer.insetTop = insetTop
if (insetRight != DIMEN_UNDEFINED) layer.insetRight = insetRight
if (insetBottom != DIMEN_UNDEFINED) layer.insetBottom = insetBottom
if (insetStart != DIMEN_UNDEFINED) layer.insetStart = insetStart
if (insetEnd != DIMEN_UNDEFINED) layer.insetEnd = insetEnd
}
fun build(): LayerDrawable {
val layerDrawable = LayerDrawable(layers.map { it -> it.drawable }.toTypedArray())
for (i in 0 until layers.size) {
val layer = layers[i]
layerDrawable.setLayerInset(
i,
layer.insetLeft,
layer.insetTop,
layer.insetRight,
layer.insetBottom
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (layer.insetStart != DIMEN_UNDEFINED || layer.insetEnd != DIMEN_UNDEFINED) {
layerDrawable.setLayerInsetRelative(
i,
layer.insetStart,
layer.insetTop,
layer.insetEnd,
layer.insetBottom
)
}
}
layerDrawable.setId(i, i)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
layerDrawable.setLayerGravity(i, layer.gravity)
layerDrawable.setLayerInsetStart(i, layer.insetStart)
layerDrawable.setLayerInsetEnd(i, layer.insetEnd)
}
}
layerDrawable.paddingMode = paddingMode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
layerDrawable.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
if (paddingStart != DIMEN_UNDEFINED || paddingEnd != DIMEN_UNDEFINED) {
layerDrawable.setPaddingRelative(
paddingStart,
paddingTop,
paddingEnd,
paddingBottom
)
}
}
return layerDrawable
}
internal class Layer(var drawable: Drawable) {
var gravity: Int = Gravity.NO_GRAVITY
var width: Int = -1
var height: Int = -1
var insetLeft: Int = 0
var insetTop: Int = 0
var insetRight: Int = 0
var insetBottom: Int = 0
var insetStart: Int = DIMEN_UNDEFINED
var insetEnd: Int = DIMEN_UNDEFINED
}
}

View File

@@ -1,61 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.Path
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.PathShape
class PathShapeDrawableBuilder {
private var path: Path? = null
private var pathStandardWidth: Float = 100f
private var pathStandardHeight: Float = 100f
private var width: Int = -1
private var height: Int = -1
fun path(path: Path, pathStandardWidth: Float, pathStandardHeight: Float) = apply {
this.path = path
this.pathStandardWidth = pathStandardWidth
this.pathStandardHeight = pathStandardHeight
}
fun width(width: Int) = apply { this.width = width }
fun height(height: Int) = apply { this.height = height }
fun size(size: Int) = apply { width(size).height(size) }
fun build(custom: ((shapeDrawable: ShapeDrawable) -> Unit)? = null): ShapeDrawable {
val shapeDrawable = ShapeDrawable()
if (path == null || width <= 0 || height <= 0) {
return shapeDrawable
}
val pathShape = PathShape(path!!, pathStandardWidth, pathStandardHeight)
shapeDrawable.shape = pathShape
shapeDrawable.intrinsicWidth = width
shapeDrawable.intrinsicHeight = height
if (custom != null) {
custom(shapeDrawable)
}
return shapeDrawable
}
}

View File

@@ -1,73 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.*
import android.util.StateSet
class RippleDrawableBuilder : DrawableWrapperBuilder<RippleDrawableBuilder>() {
private var color: Int = Constants.DEFAULT_COLOR
private var colorStateList: ColorStateList? = null
private var radius: Int = -1
fun color(color: Int) = apply { this.color = color }
fun colorStateList(colorStateList: ColorStateList?) =
apply { this.colorStateList = colorStateList }
fun radius(radius: Int) = apply { this.radius = radius }
override fun build(): Drawable {
var drawable = this.drawable!!
val colorStateList = this.colorStateList ?: ColorStateList(
arrayOf(StateSet.WILD_CARD),
intArrayOf(color)
)
var mask = if (drawable is DrawableContainer) drawable.getCurrent() else drawable
if (mask is ShapeDrawable) {
val state = mask.getConstantState()
if (state != null) {
val temp = state.newDrawable().mutate() as ShapeDrawable
temp.paint.color = Color.BLACK
mask = temp
}
} else if (mask is GradientDrawable) {
val state = mask.getConstantState()
if (state != null) {
val temp = state.newDrawable().mutate() as GradientDrawable
temp.setColor(Color.BLACK)
mask = temp
}
} else {
mask = ColorDrawable(Color.BLACK)
}
val rippleDrawable = RippleDrawable(colorStateList, drawable, mask)
setRadius(rippleDrawable, radius)
rippleDrawable.invalidateSelf()
drawable = rippleDrawable
return drawable
}
}

View File

@@ -1,52 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.drawable.Drawable
import android.graphics.drawable.RotateDrawable
class RotateDrawableBuilder : DrawableWrapperBuilder<RotateDrawableBuilder>() {
private var pivotX: Float = 0.5f
private var pivotY: Float = 0.5f
private var fromDegrees: Float = 0f
private var toDegrees: Float = 360f
fun pivotX(x: Float) = apply { pivotX = x }
fun pivotY(y: Float) = apply { pivotY = y }
fun fromDegrees(degree: Float) = apply { fromDegrees = degree }
fun toDegrees(degree: Float) = apply { toDegrees = degree }
override fun build(): Drawable {
val rotateDrawable = RotateDrawable()
drawable?.let {
setDrawable(rotateDrawable, it)
apply {
setPivotX(rotateDrawable, pivotX)
setPivotY(rotateDrawable, pivotY)
setFromDegrees(rotateDrawable, fromDegrees)
setToDegrees(rotateDrawable, toDegrees)
}
}
return rotateDrawable
}
}

View File

@@ -1,45 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.drawable.Drawable
import android.graphics.drawable.ScaleDrawable
import android.view.Gravity
class ScaleDrawableBuilder : DrawableWrapperBuilder<ScaleDrawableBuilder>() {
private var level: Int = 10000
private var scaleGravity = Gravity.CENTER
private var scaleWidth: Float = 0f
private var scaleHeight: Float = 0f
fun level(level: Int) = apply { this.level = level }
fun scaleGravity(gravity: Int) = apply { this.scaleGravity = gravity }
fun scaleWidth(scale: Float) = apply { this.scaleWidth = scale }
fun scaleHeight(scale: Float) = apply { this.scaleHeight = scale }
override fun build(): Drawable {
val scaleDrawable = ScaleDrawable(drawable, scaleGravity, scaleWidth, scaleHeight)
scaleDrawable.level = level
return scaleDrawable
}
}

View File

@@ -1,60 +0,0 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.StateListDrawable
import android.util.StateSet
class StateListDrawableBuilder {
private var pressed: Drawable? = null
private var disabled: Drawable? = null
private var selected: Drawable? = null
private var normal: Drawable = ColorDrawable(Color.TRANSPARENT)
fun pressed(pressed: Drawable?) = apply { this.pressed = pressed }
fun disabled(disabled: Drawable?) = apply { this.disabled = disabled }
fun selected(selected: Drawable?) = apply { this.selected = selected }
fun normal(normal: Drawable) = apply { this.normal = normal }
fun build(): StateListDrawable {
val stateListDrawable = StateListDrawable()
setupStateListDrawable(stateListDrawable)
return stateListDrawable
}
private fun setupStateListDrawable(stateListDrawable: StateListDrawable) {
pressed?.let {
stateListDrawable.addState(intArrayOf(android.R.attr.state_pressed), it)
}
disabled?.let {
stateListDrawable.addState(intArrayOf(-android.R.attr.state_enabled), it)
}
selected?.let {
stateListDrawable.addState(intArrayOf(android.R.attr.state_selected), it)
}
stateListDrawable.addState(StateSet.WILD_CARD, normal)
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -25,9 +25,6 @@ package com.fankes.apperrorstracking.utils.factory
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.InsetDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@@ -35,43 +32,56 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AlertDialog
import androidx.viewbinding.ViewBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsDisplayActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.shape.MaterialShapeDrawable
import com.highcapable.yukihookapi.YukiHookAPI
import com.highcapable.yukihookapi.annotation.CauseProblemsApi
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.type.android.LayoutInflaterClass
/**
* 构造 [VB] 自定义 View 对话框
* @param isDisableMaterial3 是否禁用 Material 3 风格对话框 - 默认否
* @param initiate 对话框方法体
*/
@JvmName(name = "showDialog-VB")
inline fun <reified VB : ViewBinding> Context.showDialog(initiate: DialogBuilder<VB>.() -> Unit) =
DialogBuilder<VB>(context = this, VB::class.java).apply(initiate).show()
@JvmName(name = "showDialog_Generics")
inline fun <reified VB : ViewBinding> Context.showDialog(isDisableMaterial3: Boolean = false, initiate: DialogBuilder<VB>.() -> Unit) =
DialogBuilder<VB>(context = this, isDisableMaterial3, VB::class.java).apply(initiate).show()
/**
* 构造对话框
* @param initiate 对话框方法体
* @param isDisableMaterial3 是否禁用 Material 3 风格对话框 - 默认否
*/
inline fun Context.showDialog(initiate: DialogBuilder<*>.() -> Unit) = DialogBuilder<ViewBinding>(context = this).apply(initiate).show()
inline fun Context.showDialog(isDisableMaterial3: Boolean = false, initiate: DialogBuilder<*>.() -> Unit) =
DialogBuilder<ViewBinding>(context = this, isDisableMaterial3).apply(initiate).show()
/**
* 对话框构造器
* @param context 实例
* @param isDisableMaterial3 是否禁用 Material 3 风格对话框 - 默认否
* @param bindingClass [ViewBinding] 的 [Class] 实例 or null
*/
class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingClass: Class<*>? = null) {
class DialogBuilder<VB : ViewBinding>(
val context: Context,
private val isDisableMaterial3: Boolean = false,
private val bindingClass: Class<*>? = null
) {
private var instanceAndroidX: androidx.appcompat.app.AlertDialog.Builder? = null // 实例对象
private var instanceAndroid: android.app.AlertDialog.Builder? = null // 实例对象
/** 实例对象 */
private var instance: AlertDialog.Builder? = null
private var onCancel: (() -> Unit)? = null // 对话框取消监听
private var dialogInstance: Dialog? = null // 对话框实例
private var customLayoutView: View? = null // 自定义布局
/** 对话框取消监听 */
private var onCancel: (() -> Unit)? = null
/** 对话框实例 */
private var dialogInstance: Dialog? = null
/** 自定义布局 */
private var customLayoutView: View? = null
/**
* 获取 [DialogBuilder] 绑定布局对象
@@ -86,49 +96,31 @@ class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingC
} ?: error("This dialog maybe not a custom view dialog")
}
/**
* 是否需要使用 AndroidX 风格对话框
* @return [Boolean]
*/
private val isUsingAndroidX get() = runCatching { context is AppCompatActivity }.getOrNull() ?: false
init {
if (isUsingAndroidX) runCatching {
instanceAndroidX = MaterialAlertDialogBuilder(context).also { builder ->
if (context is AppErrorsDisplayActivity)
builder.background = (builder.background as MaterialShapeDrawable).apply { setCornerSize(15.dpFloat(context)) }
}
} else runCatching {
instanceAndroid = android.app.AlertDialog.Builder(
context,
if (context.isSystemInDarkMode) android.R.style.Theme_Material_Dialog else android.R.style.Theme_Material_Light_Dialog
)
if (YukiHookAPI.Status.isXposedEnvironment) error("This dialog is not allowed to created in Xposed environment")
instance = MaterialAlertDialogBuilder(context).also { builder ->
if (isDisableMaterial3)
builder.background = (builder.background as? MaterialShapeDrawable)?.apply { setCornerSize(15.dpFloat(context)) }
}
}
/** 设置对话框不可关闭 */
fun noCancelable() {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setCancelable(false) }
else runCatching { instanceAndroid?.setCancelable(false) }
instance?.setCancelable(false)
}
/** 设置对话框标题 */
var title
get() = ""
set(value) {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setTitle(value) }
else runCatching { instanceAndroid?.setTitle(value) }
instance?.setTitle(value)
}
/** 设置对话框消息内容 */
var msg
get() = ""
set(value) {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setMessage(value) }
else runCatching { instanceAndroid?.setMessage(value) }
instance?.setMessage(value)
}
/** 设置进度条对话框消息内容 */
@@ -156,9 +148,7 @@ class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingC
* @param callback 点击事件
*/
fun confirmButton(text: String = LocaleString.confirm, callback: () -> Unit = {}) {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setPositiveButton(text) { _, _ -> callback() } }
else runCatching { instanceAndroid?.setPositiveButton(text) { _, _ -> callback() } }
instance?.setPositiveButton(text) { _, _ -> callback() }
}
/**
@@ -167,9 +157,7 @@ class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingC
* @param callback 点击事件
*/
fun cancelButton(text: String = LocaleString.cancel, callback: () -> Unit = {}) {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setNegativeButton(text) { _, _ -> callback() } }
else runCatching { instanceAndroid?.setNegativeButton(text) { _, _ -> callback() } }
instance?.setNegativeButton(text) { _, _ -> callback() }
}
/**
@@ -178,9 +166,7 @@ class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingC
* @param callback 点击事件
*/
fun neutralButton(text: String = LocaleString.more, callback: () -> Unit = {}) {
if (isUsingAndroidX)
runCatching { instanceAndroidX?.setNeutralButton(text) { _, _ -> callback() } }
else runCatching { instanceAndroid?.setNeutralButton(text) { _, _ -> callback() } }
instance?.setNeutralButton(text) { _, _ -> callback() }
}
/**
@@ -199,31 +185,12 @@ class DialogBuilder<VB : ViewBinding>(val context: Context, private val bindingC
fun show() {
/** 若当前自定义 View 的对话框没有调用 [binding] 将会对其手动调用一次以确保显示布局 */
if (bindingClass != null) binding
if (isUsingAndroidX) runCatching {
instanceAndroidX?.create()?.apply {
runCatching {
instance?.create()?.apply {
customLayoutView?.let { setView(it) }
dialogInstance = this
setOnCancelListener { onCancel?.invoke() }
}?.show()
} else runCatching {
instanceAndroid?.create()?.apply {
customLayoutView?.let { setView(it) }
window?.setBackgroundDrawable(
InsetDrawable(
GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
if (context.isSystemInDarkMode) intArrayOf(0xFF2D2D2D.toInt(), 0xFF2D2D2D.toInt())
else intArrayOf(Color.WHITE, Color.WHITE)
).apply {
shape = GradientDrawable.RECTANGLE
gradientType = GradientDrawable.LINEAR_GRADIENT
cornerRadius = 15.dpFloat(this@DialogBuilder.context)
}, 30.dp(context), 0, 30.dp(context), 0
)
)
dialogInstance = this
setOnCancelListener { onCancel?.invoke() }
}?.show()
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -19,22 +19,28 @@
*
* This file is Created by fankes on 2022/5/7.
*/
@file:Suppress("DEPRECATION", "PrivateApi", "unused", "ObsoleteSdkInt")
@file:Suppress("unused", "NotificationPermission")
package com.fankes.apperrorstracking.utils.factory
import android.app.*
import android.content.*
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import com.fankes.apperrorstracking.BuildConfig
@@ -42,10 +48,17 @@ import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.locale.LocaleString
import com.google.android.material.snackbar.Snackbar
import com.highcapable.yukihookapi.hook.factory.field
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.type.android.ApplicationInfoClass
import com.highcapable.yukihookapi.hook.type.android.ContextClass
import com.highcapable.yukihookapi.hook.type.android.IntentClass
import com.highcapable.yukihookapi.hook.type.android.UserHandleClass
import com.topjohnwu.superuser.Shell
import java.io.Serializable
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.*
/**
* 系统深色模式是否开启
@@ -74,54 +87,112 @@ fun Number.dp(context: Context) = dpFloat(context).toInt()
fun Number.dpFloat(context: Context) = toFloat() * context.resources.displayMetrics.density
/**
* 从 [Int] resId 获取 [Drawable]
* @param resources 使用的 [Resources]
* 获取 [Drawable]
* @param resId 属性资源 ID
* @return [Drawable]
*/
fun Int.drawableOf(resources: Resources) = ResourcesCompat.getDrawable(resources, this, null) ?: error("Invalid resources")
fun Resources.drawableOf(@DrawableRes resId: Int) = ResourcesCompat.getDrawable(this, resId, null) ?: error("Invalid resources")
/**
* 获取 APP 名称
* @param packageName 包名
* @return [String]
* 获取颜色
* @param resId 属性资源 ID
* @return [Int]
*/
fun Context.appName(packageName: String) =
runCatching {
packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)
.applicationInfo.loadLabel(packageManager).toString()
}.getOrNull() ?: packageName
fun Resources.colorOf(@ColorRes resId: Int) = ResourcesCompat.getColor(this, resId, null)
/**
* 获取 APP 完整版本
* @param packageName 包名
* 得到 APP 安装包信息 (兼容)
* @param packageName APP 包名
* @param flag [PackageInfoFlags]
* @return [PackageInfo] or null
*/
private fun Context.getPackageInfoCompat(packageName: String, flag: Number = 0) = runCatching {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 33)
packageManager?.getPackageInfo(packageName, PackageInfoFlags.of(flag.toLong()))
else packageManager?.getPackageInfo(packageName, flag.toInt())
}.getOrNull()
/**
* 得到 APP 版本号 (兼容 [PackageInfo.getLongVersionCode])
* @return [Int]
*/
private val PackageInfo.versionCodeCompat get() = PackageInfoCompat.getLongVersionCode(this)
/**
* 获取系统中全部已安装应用列表
* @return [List]<[PackageInfo]>
*/
fun Context.listOfPackages() = runCatching {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 33)
packageManager?.getInstalledPackages(PackageInfoFlags.of(PackageManager.GET_CONFIGURATIONS.toLong()))
else packageManager?.getInstalledPackages(PackageManager.GET_CONFIGURATIONS)
}.getOrNull() ?: emptyList()
/**
* 得到 APP 名称
* @param packageName APP 包名 - 默认为当前 APP
* @return [String]
*/
fun Context.appVersion(packageName: String) =
runCatching {
packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)?.let { "${it.versionName} (${it.versionCode})" }
}.getOrNull() ?: "<unknown>"
fun Context.appNameOf(packageName: String = getPackageName()) =
getPackageInfoCompat(packageName)?.applicationInfo?.loadLabel(packageManager)?.toString() ?: "unknown"
/**
* 得到 APP 版本信息与版本号
* @param packageName APP 包名 - 默认为当前 APP
* @return [String] 无法获取时返回 "unknown(-1)"
*/
fun Context.appVersionBrandOf(packageName: String = getPackageName()) = "${appVersionNameOf(packageName)}(${appVersionCodeOf(packageName)})"
/**
* 得到 APP 版本名称
* @param packageName APP 包名 - 默认为当前 APP
* @return [String] 无法获取时返回 "unknown"
*/
fun Context.appVersionNameOf(packageName: String = getPackageName()) = getPackageInfoCompat(packageName)?.versionName ?: "unknown"
/**
* 得到 APP 版本号
* @param packageName APP 包名 - 默认为当前 APP
* @return [Long] 无法获取时返回 -1
*/
fun Context.appVersionCodeOf(packageName: String = getPackageName()) = getPackageInfoCompat(packageName)?.versionCodeCompat ?: -1L
/**
* 获取 APP CPU ABI 名称
* @param packageName 包名
* @param packageName APP 包名 - 默认为当前 APP
* @return [String]
*/
fun Context.appCpuAbi(packageName: String) =
runCatching {
ApplicationInfoClass.field { name = "primaryCpuAbi" }
.get(packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)?.applicationInfo).string()
}.getOrNull() ?: ""
fun Context.appCpuAbiOf(packageName: String = getPackageName()) = runCatching {
ApplicationInfoClass.field { name = "primaryCpuAbi" }.get(getPackageInfoCompat(packageName)?.applicationInfo).string()
}.getOrNull() ?: ""
/**
* 获取 APP 图标
* @param packageName 包名
* @return [Drawable]
* 得到 APP 图标
* @param packageName APP 包名 - 默认为当前 APP
* @return [Drawable] 无发获取时返回 [R.drawable.ic_android]
*/
fun Context.appIcon(packageName: String) =
runCatching {
packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)
.applicationInfo.loadIcon(packageManager)
}.getOrNull() ?: R.drawable.ic_android.drawableOf(resources)
fun Context.appIconOf(packageName: String = getPackageName()) =
getPackageInfoCompat(packageName)?.applicationInfo?.loadIcon(packageManager) ?: resources.drawableOf(R.drawable.ic_android)
/**
* 获取 [Serializable] (兼容)
* @param key 键值名称
* @return [T] or null
*/
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? {
@Suppress("DEPRECATION")
return if (Build.VERSION.SDK_INT >= 33)
getSerializableExtra(key, T::class.java)
else getSerializableExtra(key) as? T?
}
/**
* [List]<[T]> 转换为 [ArrayList]<[T]>
* @return [ArrayList]<[T]>
*/
fun <T> List<T>.toArrayList() = toMutableList() as ArrayList<T>
/**
* 计算与当前时间戳相差的友好时间
@@ -172,6 +243,12 @@ fun Number.decimal(count: Int = 2) = runCatching {
).apply { roundingMode = RoundingMode.HALF_UP }.format(this) ?: toString()
}.getOrNull() ?: this
/**
* [Long] 转换为 UTC 时间
* @return [String]
*/
fun Long.toUtcTime() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ROOT).format(Date(this)) ?: ""
/**
* 弹出 [Toast]
* @param msg 提示内容
@@ -203,7 +280,8 @@ fun Context.snake(msg: String, actionText: String = "", callback: () -> Unit = {
*/
fun Context.pushNotify(channelId: String, channelName: String, title: String, content: String, icon: IconCompat, color: Int, intent: Intent) {
getSystemService<NotificationManager>()?.apply {
createNotificationChannel(NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
createNotificationChannel(NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH))
notify((0..999).random(), NotificationCompat.Builder(this@pushNotify, channelId).apply {
this.color = color
setAutoCancel(true)
@@ -255,7 +333,7 @@ fun Context.openSelfSetting(packageName: String = this.packageName) = runCatchin
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", packageName, null)
})
}.onFailure { toast(msg = "Cannot open '$packageName'") }
}.onFailure { toast(msg = "Cannot open \"$packageName\"") }
/**
* 启动系统浏览器
@@ -271,7 +349,7 @@ fun Context.openBrowser(url: String, packageName: String = "") = runCatching {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.onFailure {
if (packageName.isNotBlank()) snake(msg = "Cannot start '$packageName'")
if (packageName.isNotBlank()) snake(msg = "Cannot start \"$packageName\"")
else snake(msg = "Start system browser failed")
}
@@ -284,13 +362,15 @@ fun Context.isAppCanOpened(packageName: String = this.packageName) =
/**
* 启动指定 APP
* @param packageName 包名
* @param packageName APP 包名 - 默认为当前 APP
* @param userId APP 用户 ID - 默认 0
*/
fun Context.openApp(packageName: String = this.packageName) = runCatching {
startActivity(packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.onFailure { toast(msg = "Cannot start '$packageName'") }
fun Context.openApp(packageName: String = getPackageName(), userId: Int = 0) = runCatching {
ContextClass.method {
name = "startActivityAsUser"
param(IntentClass, UserHandleClass)
}.get(this).call(packageManager.getLaunchIntentForPackage(packageName), UserHandleClass.method { name = "of" }.get().call(userId))
}.onFailure { toast(msg = "Cannot start \"$packageName\"${if (userId > 0) " for user $userId" else ""}") }
/**
* 是否有 Root 权限
@@ -308,4 +388,27 @@ fun execShell(cmd: String, isSu: Boolean = true) = runCatching {
(if (isSu) Shell.su(cmd) else Shell.sh(cmd)).exec().out.let {
if (it.isNotEmpty()) it[0].trim() else ""
}
}.getOrNull() ?: ""
}.getOrNull() ?: ""
/**
* 隐藏或显示启动器图标
*
* - 你可能需要 LSPosed 的最新版本以开启高版本系统中隐藏 APP 桌面图标功能
* @param isShow 是否显示
*/
fun Context.hideOrShowLauncherIcon(isShow: Boolean) {
packageManager?.setComponentEnabledSetting(
ComponentName(packageName, "${BuildConfig.APPLICATION_ID}.Home"),
if (isShow) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
/**
* 获取启动器图标状态
* @return [Boolean] 是否显示
*/
val Context.isLauncherIconShowing
get() = packageManager?.getComponentEnabledSetting(
ComponentName(packageName, "${BuildConfig.APPLICATION_ID}.Home")
) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED

View File

@@ -0,0 +1,60 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/10/3.
*/
package com.fankes.apperrorstracking.utils.factory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonIOException
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
/**
* 创建 [Gson] 实例
* @return [Gson]
*/
val GSON by lazy { GsonBuilder().setLenient().create() ?: error("Gson create failed") }
/**
* 实体类转 Json 字符串
* @return [String]
* @throws [JsonIOException] 如果不是有效的 Json 数据
*/
fun Any?.toJson() = GSON.toJson(this) ?: ""
/**
* 实体类转 Json 字符串
* @return [String] or null
*/
fun Any?.toJsonOrNull() = runCatching { toJson() }.getOrNull()
/**
* Json 字符串转实体类
* @return [T] or null
* @throws [JsonSyntaxException] 如果 Json 格式不正确
*/
inline fun <reified T> String.toEntity(): T? = takeIf { it.isNotBlank() }.let { GSON.fromJson(this, object : TypeToken<T>() {}.type) }
/**
* Json 字符串转实体类
* @return [T] or null
*/
inline fun <reified T> String.toEntityOrNull(): T? = runCatching { toEntity<T?>() }.getOrNull()

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -17,19 +17,30 @@
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/5/7.
* This file is Created by fankes on 2022/10/3.
*/
package com.fankes.apperrorstracking.utils.drawable.drawabletoolbox
package com.fankes.apperrorstracking.utils.factory
import android.graphics.drawable.Drawable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class FlipDrawableBuilder : DrawableWrapperBuilder<FlipDrawableBuilder>() {
/**
* 创建当前线程池服务
* @return [ExecutorService]
*/
private val currentThreadPool get() = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
private var orientation: Int = FlipDrawable.ORIENTATION_HORIZONTAL
fun orientation(orientation: Int) = apply { this.orientation = orientation }
override fun build(): Drawable {
return FlipDrawable(drawable!!, orientation)
/**
* 创建并启动新的临时线程池
*
* 等待 [block] 执行完成并自动释放
* @param block 方法块
*/
fun newThread(block: () -> Unit) {
currentThreadPool.apply {
execute {
block()
shutdown()
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2022/10/5.
*/
@file:Suppress("unused")
package com.fankes.apperrorstracking.utils.tool
import android.app.Application
import android.widget.CompoundButton
import com.fankes.apperrorstracking.BuildConfig
import com.highcapable.yukihookapi.hook.factory.modulePrefs
import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData
import com.microsoft.appcenter.AppCenter
import com.microsoft.appcenter.analytics.Analytics
import com.microsoft.appcenter.crashes.Crashes
/**
* Microsoft App Center 工具
*/
object AppAnalyticsTool {
/** App Secret */
private const val APP_CENTER_SECRET = BuildConfig.APP_CENTER_SECRET
/** 启用匿名统计收集使用情况功能 */
private val ENABLE_APP_CENTER_ANALYTICS = PrefsData("_enable_app_center_analytics", true)
/** 当前实例 */
private var instance: Application? = null
/**
* 是否启用匿名统计收集使用情况功能
* @return [Boolean]
*/
private var isEnableAppCenterAnalytics
get() = instance?.modulePrefs?.get(ENABLE_APP_CENTER_ANALYTICS) ?: true
set(value) {
instance?.modulePrefs?.put(ENABLE_APP_CENTER_ANALYTICS, value)
}
/** 绑定到 [CompoundButton] 自动设置选中状态 */
fun CompoundButton.bindAppAnalytics() {
isChecked = isEnableAppCenterAnalytics
setOnCheckedChangeListener { button, isChecked ->
if (button.isPressed.not()) return@setOnCheckedChangeListener
isEnableAppCenterAnalytics = isChecked
}
}
/**
* 上传分析日志
* @param name 事件名称
* @param data 事件详细内容 - 默认空
*/
fun trackEvent(name: String, data: HashMap<String, String>? = null) {
if (data != null) Analytics.trackEvent(name, data)
else Analytics.trackEvent(name)
}
/**
* 初始化 App Center
* @param instance 实例
*/
fun init(instance: Application) {
this.instance = instance
if (isEnableAppCenterAnalytics && APP_CENTER_SECRET.isNotBlank())
AppCenter.start(instance, APP_CENTER_SECRET, Analytics::class.java, Crashes::class.java)
}
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
@@ -32,7 +32,6 @@ import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.utils.factory.execShell
import com.fankes.apperrorstracking.utils.factory.isRootAccess
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.factory.snake
import com.highcapable.yukihookapi.hook.factory.dataChannel
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData
@@ -43,8 +42,9 @@ import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData
object FrameworkTool {
/** 系统框架包名 */
private const val SYSTEM_FRAMEWORK_NAME = "android"
const val SYSTEM_FRAMEWORK_NAME = "android"
private const val CALL_REFRESH_HOST_PREFS_DATA = "call_refresh_host_prefs_data"
private const val CALL_APP_ERRORS_DATA_GET = "call_app_errors_data_get"
private const val CALL_MUTED_ERRORS_APP_DATA_GET = "call_muted_app_errors_data_get"
private const val CALL_APP_ERRORS_DATA_CLEAR = "call_app_errors_data_clear"
@@ -56,10 +56,12 @@ object FrameworkTool {
private const val CALL_UNMUTE_ERRORS_APP_DATA_RESULT = "call_unmute_errors_app_data_result"
private const val CALL_UNMUTE_ALL_ERRORS_APPS_DATA_RESULT = "call_unmute_all_errors_apps_data_result"
private val CALL_OPEN_SPECIFY_APP = ChannelData<String>("call_open_specify_app")
private val CALL_APP_ERROR_DATA_GET = ChannelData<Int>("call_app_error_data_get")
private val CALL_OPEN_SPECIFY_APP = ChannelData<Pair<String, Int>>("call_open_specify_app")
private val CALL_APP_LIST_DATA_GET = ChannelData<AppFiltersBean>("call_app_info_list_data_get")
private val CALL_APP_ERRORS_DATA_REMOVE = ChannelData<AppErrorsInfoBean>("call_app_errors_data_remove")
private val CALL_APP_LIST_DATA_GET_RESULT = ChannelData<ArrayList<AppInfoBean>>("call_app_info_list_data_get_result")
private val CALL_APP_ERROR_DATA_GET_RESULT = ChannelData<AppErrorsInfoBean>("call_app_error_data_get_result")
private val CALL_APP_ERRORS_DATA_GET_RESULT = ChannelData<ArrayList<AppErrorsInfoBean>>("call_app_errors_data_get_result")
private val CALL_MUTED_ERRORS_APP_DATA_GET_RESULT = ChannelData<ArrayList<MutedErrorsAppBean>>("call_muted_app_errors_data_get_result")
private val CALL_UNMUTE_ERRORS_APP_DATA = ChannelData<MutedErrorsAppBean>("call_unmute_errors_app_data")
@@ -83,10 +85,24 @@ object FrameworkTool {
fun with(instance: PackageParam, initiate: Host.() -> Unit) = apply { this.instance = instance }.apply(initiate)
/**
* 监听使用系统框架打开 APP
* @param result 回调包名
* 通知系统框架刷新存储的数据
* @param callback 回调
*/
fun onOpenAppUsedFramework(result: (String) -> Unit) = instance?.dataChannel?.wait(CALL_OPEN_SPECIFY_APP) { result(it) }
fun onRefreshFrameworkPrefsData(callback: () -> Unit) = instance?.dataChannel?.wait(CALL_REFRESH_HOST_PREFS_DATA) { callback() }
/**
* 监听使用系统框架打开 APP
* @param result 回调包名和用户 ID
*/
fun onOpenAppUsedFramework(result: (Pair<String, Int>) -> Unit) = instance?.dataChannel?.wait(CALL_OPEN_SPECIFY_APP) { result(it) }
/**
* 监听发送指定 APP 异常信息
* @param result 回调数据
*/
fun onPushAppErrorInfoData(result: (Int) -> AppErrorsInfoBean) {
instance?.dataChannel?.with { wait(CALL_APP_ERROR_DATA_GET) { put(CALL_APP_ERROR_DATA_GET_RESULT, result(it)) } }
}
/**
* 监听发送 APP 异常信息数组
@@ -195,37 +211,73 @@ object FrameworkTool {
* 重启系统
* @param context 实例
*/
fun restartSystem(context: Context) =
fun restartSystem(context: Context) {
/** 当 Root 权限获取失败时显示对话框 */
fun showWhenAccessRootFail() =
context.showDialog {
title = LocaleString.accessRootFail
msg = LocaleString.accessRootFailTip
confirmButton(LocaleString.gotIt)
}
context.showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureRestartSystem
confirmButton {
if (isRootAccess)
execShell(cmd = "reboot")
else context.snake(LocaleString.accessRootFail)
else showWhenAccessRootFail()
}
neutralButton(LocaleString.fastRestart) {
if (isRootAccess)
execShell(cmd = "killall zygote")
else context.snake(LocaleString.accessRootFail)
context.showDialog {
title = LocaleString.warning
msg = LocaleString.fastRestartProblem
confirmButton {
if (isRootAccess)
execShell(cmd = "killall zygote")
else showWhenAccessRootFail()
}
cancelButton()
}
}
cancelButton()
}
}
/**
* 检查模块是否激活
* @param context 实例
* @param result 成功后回调
*/
fun checkingActivated(context: Context, result: (Boolean) -> Unit) = context.dataChannel(SYSTEM_FRAMEWORK_NAME).checkingVersionEquals(result)
fun checkingActivated(context: Context, result: (Boolean) -> Unit) =
context.dataChannel(SYSTEM_FRAMEWORK_NAME).checkingVersionEquals(result = result)
/**
* 通知系统框架刷新存储的数据
* @param context 实例
*/
fun refreshFrameworkPrefsData(context: Context) = context.dataChannel(SYSTEM_FRAMEWORK_NAME).put(CALL_REFRESH_HOST_PREFS_DATA)
/**
* 使用系统框架打开 [packageName]
* @param context 实例
* @param packageName APP 包名
* @param userId APP 用户 ID
*/
fun openAppUsedFramework(context: Context, packageName: String) =
context.dataChannel(SYSTEM_FRAMEWORK_NAME).put(CALL_OPEN_SPECIFY_APP, packageName)
fun openAppUsedFramework(context: Context, packageName: String, userId: Int) =
context.dataChannel(SYSTEM_FRAMEWORK_NAME).put(CALL_OPEN_SPECIFY_APP, Pair(packageName, userId))
/**
* 获取指定 APP 异常信息
* @param context 实例
* @param pid 当前进程 ID
* @param result 回调数据
*/
fun fetchAppErrorInfoData(context: Context, pid: Int, result: (AppErrorsInfoBean) -> Unit) {
context.dataChannel(SYSTEM_FRAMEWORK_NAME).with {
wait(CALL_APP_ERROR_DATA_GET_RESULT) { result(it) }
put(CALL_APP_ERROR_DATA_GET, pid)
}
}
/**
* 获取 APP 异常信息数组

View File

@@ -0,0 +1,113 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* and eula along with this software. If not, see
* <https://www.gnu.org/licenses/>
*
* This file is Created by fankes on 2023/1/23.
*/
package com.fankes.apperrorstracking.utils.tool
import android.app.Activity
import android.content.Context
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import android.icu.util.TimeZone
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.utils.factory.openBrowser
import com.fankes.apperrorstracking.utils.factory.showDialog
import okhttp3.*
import org.json.JSONObject
import java.io.IOException
import java.io.Serializable
import java.util.*
/**
* 获取 Github Release 最新版本工具类
*/
object GithubReleaseTool {
/** 仓库作者 */
private const val REPO_AUTHOR = "KitsunePie"
/** 仓库名称 */
private const val REPO_NAME = "AppErrorsTracking"
/**
* 获取最新版本信息
* @param context 实例
* @param version 当前版本
* @param result 成功后回调 - ([String] 最新版本,[Function] 更新对话框方法体)
*/
fun checkingForUpdate(context: Context, version: String, result: (String, () -> Unit) -> Unit) = runCatching {
OkHttpClient().newBuilder().build().newCall(
Request.Builder()
.url("https://api.github.com/repos/$REPO_AUTHOR/$REPO_NAME/releases/latest")
.get()
.build()
).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) = runCatching {
JSONObject(response.body.string()).apply {
GithubReleaseBean(
name = getString("name"),
htmlUrl = getString("html_url"),
content = getString("body"),
date = getString("published_at").localTime()
).apply {
fun showUpdate() = context.showDialog {
title = LocaleString.latestVersion(name)
msg = LocaleString.latestVersionTip(date, content)
confirmButton(LocaleString.updateNow) { context.openBrowser(htmlUrl) }
cancelButton()
}
if (name != version) (context as? Activity?)?.runOnUiThread {
showUpdate()
result(name) { showUpdate() }
}
}
}
}.getOrNull().let {}
})
}.getOrNull().let {}
/**
* 格式化时间为本地时区
* @return [String] 本地时区时间
*/
private fun String.localTime() = replace("T", " ").replace("Z", "").let {
runCatching {
val local = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT).apply { timeZone = Calendar.getInstance().timeZone }
val current = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT).apply { timeZone = TimeZone.getTimeZone("GMT") }
local.format(current.parse(it))
}.getOrNull() ?: it
}
/**
* Github Release bean
* @param name 版本名称
* @param htmlUrl 网页地址
* @param content 更新日志
* @param date 发布时间
*/
private data class GithubReleaseBean(
var name: String,
var htmlUrl: String,
var content: String,
var date: String
) : Serializable
}

View File

@@ -1,6 +1,6 @@
/*
* AppErrorsTracking - Added more features to app's crash dialog, fixed custom rom deleted dialog, the best experience to Android developer.
* Copyright (C) 2019-2022 Fankes Studio(qzmmcn@163.com)
* Copyright (C) 2017-2023 Fankes Studio(qzmmcn@163.com)
* https://github.com/KitsunePie/AppErrorsTracking
*
* This software is non-free but opensource software: you can redistribute it

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#2196F3" />
<corners android:radius="5dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#565656" />
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#E53935" />
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#3974CB" />
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FB8C00" />
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp" />
</shape>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:fillColor="#ffffff"
android:pathData="M44.5,49.92L57,51.79L44.5,57L7,44.5L44.5,49.92L44.5,7L57,12.21L57,51.79L44.5,49.92Z" />
<path
android:fillColor="#ffffff"
android:pathData="M19.5,15.33L24.29,16.38L24.29,23.88L32,18.46L38.25,20.54L38.25,37.21L32,39.29L24.08,33.88L24.08,41.38L19.5,42.42L11.17,35.13L11.17,32L15.96,28.88L11.17,25.75L11.17,22.63L19.5,15.33ZM24.08,28.88L32,33.04L32,24.71L24.08,28.88ZM13.67,23.88L19.5,26.79L19.5,19.5L13.67,23.88ZM19.5,38.25L19.5,30.96L13.67,33.88L19.5,38.25Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="150.39165dp"
android:viewportWidth="1307"
android:viewportHeight="1024">
<path
android:fillColor="#ffffff"
android:pathData="M268.7,566.5h929.6c36.3,0 72.6,-29 72.6,-72.6 0,-36.3 -29,-72.6 -72.6,-72.6H305l297.8,-297.8c29,-29 29,-72.6 0,-101.7 -29,-29 -72.6,-29 -101.7,0L72.6,450.3c-14.5,14.5 -21.8,36.3 -21.8,58.1 0,21.8 0,43.6 21.8,58.1l428.5,428.5c29,29 72.6,29 101.7,0 29,-29 29,-72.6 0,-101.7l-334.1,-326.8z" />
</vector>

View File

@@ -3,7 +3,7 @@
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M851.5,239.1l-66.6,24.6v499.7l66.6,24.6c29.7,10.8 55.3,-17.9 55.3,-65V304.1c0,-47.1 -25.6,-76.3 -55.3,-65M362.5,374.8l-185.3,23.6c-7.2,1 -13.3,-10.2 -13.3,-24.6s5.6,-27.1 13.3,-28.7l185.3,-35.8c11.3,-2 20.5,10.8 20.5,29.2 -0.5,18.9 -9.7,34.8 -20.5,36.4M164.4,503.8c0,-14.3 5.6,-26.6 13.3,-26.6l93.7,-3.1c9.2,-0.5 16.9,12.8 16.9,29.7s-7.7,30.2 -16.9,29.7l-93.7,-3.1c-7.7,0 -13.3,-11.8 -13.3,-26.6m135.2,182.8l-122.4,-23.6c-7.2,-1.5 -13.3,-14.3 -13.3,-28.7s5.6,-25.6 13.3,-24.6l122.4,15.9c9.7,1 17.9,16.4 17.9,33.3s-8.2,29.2 -17.9,27.6M414.2,110.1L145.4,209.9c-22,8.2 -38.9,50.7 -38.9,95.7v397.3c0,45.1 16.9,87.6 38.9,95.7l268.8,99.8c42,15.9 78.8,-25.6 78.8,-93.2V203.3c0,-67.6 -36.9,-109.1 -78.8,-93.2m250.4,129L558.1,278.5v470.5l106.5,39.4c29.7,10.8 55.3,-17.9 55.3,-65V304.1c0,-47.1 -26.1,-76.3 -55.3,-65"
android:fillColor="#ffffff"/>
</vector>
<path
android:fillColor="#ffffff"
android:pathData="M851.5,239.1l-66.6,24.6v499.7l66.6,24.6c29.7,10.8 55.3,-17.9 55.3,-65V304.1c0,-47.1 -25.6,-76.3 -55.3,-65M362.5,374.8l-185.3,23.6c-7.2,1 -13.3,-10.2 -13.3,-24.6s5.6,-27.1 13.3,-28.7l185.3,-35.8c11.3,-2 20.5,10.8 20.5,29.2 -0.5,18.9 -9.7,34.8 -20.5,36.4M164.4,503.8c0,-14.3 5.6,-26.6 13.3,-26.6l93.7,-3.1c9.2,-0.5 16.9,12.8 16.9,29.7s-7.7,30.2 -16.9,29.7l-93.7,-3.1c-7.7,0 -13.3,-11.8 -13.3,-26.6m135.2,182.8l-122.4,-23.6c-7.2,-1.5 -13.3,-14.3 -13.3,-28.7s5.6,-25.6 13.3,-24.6l122.4,15.9c9.7,1 17.9,16.4 17.9,33.3s-8.2,29.2 -17.9,27.6M414.2,110.1L145.4,209.9c-22,8.2 -38.9,50.7 -38.9,95.7v397.3c0,45.1 16.9,87.6 38.9,95.7l268.8,99.8c42,15.9 78.8,-25.6 78.8,-93.2V203.3c0,-67.6 -36.9,-109.1 -78.8,-93.2m250.4,129L558.1,278.5v470.5l106.5,39.4c29.7,10.8 55.3,-17.9 55.3,-65V304.1c0,-47.1 -26.1,-76.3 -55.3,-65" />
</vector>

View File

@@ -3,7 +3,7 @@
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M785.8,927.4h-64.9c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.1,-26.6 -27.2,-11.7 -7.2,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5h-56.5c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5h-55.8c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5H360c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.4 6.5H207.5a13.1,13.1 0,0 1,-7.1 -1.9c-43.5,-25.3 -62.9,-84.4 0.6,-138.9 42.2,-36.4 46.7,-94.8 45.4,-147.3 0,-8.4 6.5,-15.6 14.9,-15.6h526.4c7.1,0 13,4.5 14.3,11.7 14.9,64.9 20.1,116.2 32.4,205.8 8.4,65.5 -48.7,86.3 -48.7,86.3zM215.3,570.4c-20.1,0 -29.2,-14.9 -20.8,-32.5l70.8,-150.6c8.4,-18.2 31.8,-32.5 51.3,-32.5h113.6a14.7,14.7 0,0 0,14.9 -14.9V147.8c0.6,-28.6 23.4,-51.3 51.3,-51.3h42.2c27.9,0 51.3,22.7 51.3,51.3v190.8c0,8.4 6.5,14.9 14.9,14.9h109.7c20.1,0 42.8,14.9 51.3,32.4l70.7,150.6c8.5,18.2 -0.6,32.4 -20.7,32.4l-600.4,1.3z"
android:fillColor="#ffffff"/>
</vector>
<path
android:fillColor="#ffffff"
android:pathData="M785.8,927.4h-64.9c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.1,-26.6 -27.2,-11.7 -7.2,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5h-56.5c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5h-55.8c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.3 6.5H360c-9.7,0 -16.9,-9.7 -14.3,-19.5 0,-0.6 0,-0.6 0.6,-1.3 4.5,-16.9 -18.2,-26.6 -27.3,-11.7 -7.1,11 -13,20.2 -16.9,26a14.8,14.8 0,0 1,-12.4 6.5H207.5a13.1,13.1 0,0 1,-7.1 -1.9c-43.5,-25.3 -62.9,-84.4 0.6,-138.9 42.2,-36.4 46.7,-94.8 45.4,-147.3 0,-8.4 6.5,-15.6 14.9,-15.6h526.4c7.1,0 13,4.5 14.3,11.7 14.9,64.9 20.1,116.2 32.4,205.8 8.4,65.5 -48.7,86.3 -48.7,86.3zM215.3,570.4c-20.1,0 -29.2,-14.9 -20.8,-32.5l70.8,-150.6c8.4,-18.2 31.8,-32.5 51.3,-32.5h113.6a14.7,14.7 0,0 0,14.9 -14.9V147.8c0.6,-28.6 23.4,-51.3 51.3,-51.3h42.2c27.9,0 51.3,22.7 51.3,51.3v190.8c0,8.4 6.5,14.9 14.9,14.9h109.7c20.1,0 42.8,14.9 51.3,32.4l70.7,150.6c8.5,18.2 -0.6,32.4 -20.7,32.4l-600.4,1.3z" />
</vector>

View File

@@ -3,7 +3,7 @@
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M511,0a376.4,376.4 0,0 0,-265.1 106.8,365.1 365.1,0 0,0 -108,258.6v498.7h742L880,365.5a362.4,362.4 0,0 0,-108.9 -258.6A374.9,374.9 0,0 0,511 0zM480.3,744.6L469.5,502.9L286.9,502.9l239.9,-303.1 20.5,195.2 174.1,6.1 -243.2,343.3zM0.1,966.4a55.6,55.6 0,0 1,55.6 -56.2h910.6a57.6,57.6 0,0 1,39.3 16.3,58.7 58.7,0 0,1 16.4,39.9 55.7,55.7 0,0 1,-55.7 55.6L55.6,1022.1a55.6,55.6 0,0 1,-55.5 -55.6z"
android:fillColor="#ffffff"/>
</vector>
<path
android:fillColor="#ffffff"
android:pathData="M511,0a376.4,376.4 0,0 0,-265.1 106.8,365.1 365.1,0 0,0 -108,258.6v498.7h742L880,365.5a362.4,362.4 0,0 0,-108.9 -258.6A374.9,374.9 0,0 0,511 0zM480.3,744.6L469.5,502.9L286.9,502.9l239.9,-303.1 20.5,195.2 174.1,6.1 -243.2,343.3zM0.1,966.4a55.6,55.6 0,0 1,55.6 -56.2h910.6a57.6,57.6 0,0 1,39.3 16.3,58.7 58.7,0 0,1 16.4,39.9 55.7,55.7 0,0 1,-55.7 55.6L55.6,1022.1a55.6,55.6 0,0 1,-55.5 -55.6z" />
</vector>

View File

@@ -6,4 +6,4 @@
<path
android:fillColor="#ffffff"
android:pathData="M419.2,548.3L17,108.9C-23.6,64.4 13,0 79.7,0h846.5c66.6,0 103.3,64.4 62.8,108.9l-402.2,439.3v472.7c0,1.6 -1.3,2.9 -2.8,2.9a8.3,8.3 0,0 1,-5.2 -1.8l-154.3,-120a13.7,13.7 0,0 1,-5.3 -10.8L419.2,548.3zM838.2,511.9L1257.3,511.9L1257.3,597.2h-419.1L838.2,511.9zM838.2,682.5L1257.3,682.5v85.3h-419.1L838.2,682.5zM838.2,853.2L1257.3,853.2v85.3h-419.1v-85.3z" />
</vector>
</vector>

View File

@@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#00000000"
android:pathData="M11,16V42"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M24,29V42"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M24,19V6"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M37,6V32"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M11,16C13.761,16 16,13.761 16,11C16,8.239 13.761,6 11,6C8.239,6 6,8.239 6,11C6,13.761 8.239,16 11,16Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M24,29C26.761,29 29,26.761 29,24C29,21.239 26.761,19 24,19C21.239,19 19,21.239 19,24C19,26.761 21.239,29 24,29Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M37,42C39.761,42 42,39.761 42,37C42,34.239 39.761,32 37,32C34.239,32 32,34.239 32,37C32,39.761 34.239,42 37,42Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#ffffff"
android:pathData="M512,12.6c-282.8,0 -512,229.2 -512,512 0,226.2 146.7,418.1 350.1,485.8 25.6,4.7 35,-11.1 35,-24.6 0,-12.2 -0.5,-52.5 -0.7,-95.3 -142.5,31 -172.5,-60.4 -172.5,-60.4 -23.3,-59.2 -56.8,-74.9 -56.8,-74.9 -46.5,-31.8 3.5,-31.1 3.5,-31.1 51.4,3.6 78.5,52.8 78.5,52.8 45.7,78.3 119.8,55.6 149,42.6 4.6,-33.1 17.9,-55.7 32.5,-68.5 -113.7,-12.9 -233.3,-56.9 -233.3,-253 0,-55.9 20,-101.6 52.8,-137.4 -5.3,-12.9 -22.8,-65 5,-135.5 0,0 43,-13.8 140.8,52.5 40.8,-11.4 84.6,-17 128.2,-17.2 43.5,0.2 87.3,5.9 128.3,17.2 97.7,-66.2 140.6,-52.5 140.6,-52.5 27.9,70.5 10.3,122.6 5,135.5 32.8,35.8 52.7,81.5 52.7,137.4 0,196.6 -119.8,239.9 -233.8,252.6 18.4,15.9 34.7,47 34.7,94.8 0,68.5 -0.6,123.6 -0.6,140.5 0,13.6 9.2,29.6 35.2,24.6 203.3,-67.8 349.9,-259.6 349.9,-485.8 0,-282.8 -229.2,-512 -512,-512z" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#ffffff"
android:pathData="M18,6H8C6.895,6 6,6.895 6,8V18C6,19.105 6.895,20 8,20H18C19.105,20 20,19.105 20,18V8C20,6.895 19.105,6 18,6Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffffff"
android:pathData="M18,28H8C6.895,28 6,28.895 6,30V40C6,41.105 6.895,42 8,42H18C19.105,42 20,41.105 20,40V30C20,28.895 19.105,28 18,28Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffffff"
android:pathData="M40,6H30C28.895,6 28,6.895 28,8V18C28,19.105 28.895,20 30,20H40C41.105,20 42,19.105 42,18V8C42,6.895 41.105,6 40,6Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffffff"
android:pathData="M40,28H30C28.895,28 28,28.895 28,30V40C28,41.105 28.895,42 30,42H40C41.105,42 42,41.105 42,40V30C42,28.895 41.105,28 40,28Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
</vector>

View File

@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#00000000"
android:pathData="M9,18V42H39V18L24,6L9,18Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M19,29V42H29V29H19Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M9,42H39"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round" />
</vector>

View File

@@ -5,12 +5,13 @@
android:viewportWidth="108"
android:viewportHeight="108"
tools:ignore="VectorRaster">
<group android:scaleX="0.038671874"
android:scaleY="0.038671874"
android:translateX="34.2"
android:translateY="34.2">
<path
android:fillColor="#ffffff"
android:pathData="M1024,577.8c-1,34.9 -30.4,62.2 -65.3,62.2H848v32c0,43.7 -9.8,85.2 -27.2,122.3l120.5,120.5c25,25 25,65.5 0,90.5 -25,25 -65.5,25 -90.5,0l-109.5,-109.5C691.8,935.9 628.7,960 560,960V472c0,-13.3 -10.7,-24 -24,-24h-48c-13.3,0 -24,10.7 -24,24v488c-68.7,0 -131.8,-24.1 -181.3,-64.2l-109.5,109.5c-25,25 -65.5,25 -90.5,0 -25,-25 -25,-65.5 0,-90.5l120.5,-120.5C185.8,757.2 176,715.7 176,672v-32H65.3C30.5,640 1,612.7 0,577.8 -1,541.6 28.1,512 64,512h112v-117.5l-93.3,-93.3c-25,-25 -25,-65.5 0,-90.5 25,-25 65.5,-25 90.5,0L282.5,320h459l109.3,-109.3c25,-25 65.5,-25 90.5,0 25,25 25,65.5 0,90.5L848,394.5V512h112c35.9,0 65,29.6 64,65.8zM514,0c-123.7,0 -224,100.3 -224,224h448C738,100.3 637.7,0 514,0z" />
</group>
<group
android:scaleX="0.038671874"
android:scaleY="0.038671874"
android:translateX="34.2"
android:translateY="34.2">
<path
android:fillColor="#ffffff"
android:pathData="M1024,577.8c-1,34.9 -30.4,62.2 -65.3,62.2H848v32c0,43.7 -9.8,85.2 -27.2,122.3l120.5,120.5c25,25 25,65.5 0,90.5 -25,25 -65.5,25 -90.5,0l-109.5,-109.5C691.8,935.9 628.7,960 560,960V472c0,-13.3 -10.7,-24 -24,-24h-48c-13.3,0 -24,10.7 -24,24v488c-68.7,0 -131.8,-24.1 -181.3,-64.2l-109.5,109.5c-25,25 -65.5,25 -90.5,0 -25,-25 -25,-65.5 0,-90.5l120.5,-120.5C185.8,757.2 176,715.7 176,672v-32H65.3C30.5,640 1,612.7 0,577.8 -1,541.6 28.1,512 64,512h112v-117.5l-93.3,-93.3c-25,-25 -25,-65.5 0,-90.5 25,-25 65.5,-25 90.5,0L282.5,320h459l109.3,-109.3c25,-25 65.5,-25 90.5,0 25,25 25,65.5 0,90.5L848,394.5V512h112c35.9,0 65,29.6 64,65.8zM514,0c-123.7,0 -224,100.3 -224,224h448C738,100.3 637.7,0 514,0z" />
</group>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#ffffff"
android:pathData="m967.6,570.6c-0.9,31.1 -27.1,55.3 -58.1,55.3h-98.5v28.5c0,38.9 -8.7,75.8 -24.2,108.8l107.2,107.2c22.2,22.2 22.2,58.3 0,80.5 -22.2,22.2 -58.3,22.2 -80.5,0l-97.4,-97.4c-44,35.7 -100.2,57.1 -161.3,57.1V476.4c0,-11.8 -9.5,-21.4 -21.4,-21.4h-42.7c-11.8,0 -21.4,9.5 -21.4,21.4v434.2c-61.1,0 -117.3,-21.4 -161.3,-57.1l-97.4,97.4c-22.2,22.2 -58.3,22.2 -80.5,0 -22.2,-22.2 -22.2,-58.3 0,-80.5L237.2,763.2C221.7,730.2 213,693.3 213,654.4V625.9H114.5c-31,0 -57.2,-24.3 -58.1,-55.3 -0.9,-32.2 25,-58.6 56.9,-58.6h99.7V407.4l-83,-83c-22.2,-22.2 -22.2,-58.3 0,-80.5 22.2,-22.2 58.3,-22.2 80.5,0l97.3,97.3H716.2L813.5,243.9c22.2,-22.2 58.3,-22.2 80.5,0 22.2,22.2 22.2,58.3 0,80.5l-83,83v104.6h99.7c31.9,0 57.8,26.3 56.9,58.6zM513.8,56.4c-110.1,0 -199.3,89.3 -199.3,199.3H713.1c0,-110.1 -89.3,-199.3 -199.3,-199.3z"
android:strokeWidth="0.889835" />
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#00000000"
android:pathData="M44,16C44,22.627 38.627,28 32,28C29.973,28 28.064,27.497 26.39,26.61L9,44L4,39L21.39,21.61C20.503,19.936 20,18.027 20,16C20,9.373 25.373,4 32,4C34.027,4 35.936,4.502 37.61,5.39L30,13L35,18L42.61,10.39C43.498,12.064 44,13.973 44,16Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View File

@@ -3,7 +3,7 @@
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M851.2,957.9L179.2,957.9c-61.9,0 -110.9,-49.1 -110.9,-110.9L68.3,454.4c0,-32 25.6,-55.5 55.5,-55.5h55.5v224c0,32 25.6,55.5 55.5,55.5h558.9c32,0 55.5,-25.6 55.5,-55.5L849.1,398.9h55.5c32,0 55.5,25.6 55.5,55.5v390.4c2.1,64 -46.9,113.1 -108.8,113.1zM738.1,622.9h-448c-32,0 -55.5,-25.6 -55.5,-55.5v-448C234.7,89.6 260.3,64 290.1,64h448c32,0 55.5,25.6 55.5,55.5v448c0,29.9 -23.5,55.5 -55.5,55.5zM657.1,232.5L377.6,232.5c-19.2,0 -32,19.2 -32,32s12.8,25.6 32,32h279.5c19.2,0 32,-12.8 32,-32s-19.2,-32 -32,-32zM657.1,398.9L377.6,398.9c-19.2,0 -32,12.8 -32,32 0,12.8 19.2,25.6 32,25.6h279.5c19.2,0 32,-12.8 32,-25.6 0,-19.2 -19.2,-32 -32,-32z"
android:fillColor="#ffffff"/>
</vector>
<path
android:fillColor="#ffffff"
android:pathData="M851.2,957.9L179.2,957.9c-61.9,0 -110.9,-49.1 -110.9,-110.9L68.3,454.4c0,-32 25.6,-55.5 55.5,-55.5h55.5v224c0,32 25.6,55.5 55.5,55.5h558.9c32,0 55.5,-25.6 55.5,-55.5L849.1,398.9h55.5c32,0 55.5,25.6 55.5,55.5v390.4c2.1,64 -46.9,113.1 -108.8,113.1zM738.1,622.9h-448c-32,0 -55.5,-25.6 -55.5,-55.5v-448C234.7,89.6 260.3,64 290.1,64h448c32,0 55.5,25.6 55.5,55.5v448c0,29.9 -23.5,55.5 -55.5,55.5zM657.1,232.5L377.6,232.5c-19.2,0 -32,19.2 -32,32s12.8,25.6 32,32h279.5c19.2,0 32,-12.8 32,-32s-19.2,-32 -32,-32zM657.1,398.9L377.6,398.9c-19.2,0 -32,12.8 -32,32 0,12.8 19.2,25.6 32,25.6h279.5c19.2,0 32,-12.8 32,-25.6 0,-19.2 -19.2,-32 -32,-32z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M712.9,295.1c-120.7,-110.7 -308.3,-102.6 -419,18.1 -87.4,95.3 -103,236.3 -38.5,348.4 82,141.7 263.2,190.3 405,108.4 66.8,-38.6 116,-101.7 137,-176 8.9,-31.5 41.7,-49.8 73.2,-40.9 31.5,8.9 49.8,41.7 40.9,73.2C849.1,846.8 619.8,975 399.2,912.6 178.7,850.2 50.5,620.9 112.9,400.3S404.6,51.6 625.2,114c64.2,18.2 123.1,51.5 171.6,97.3l79.7,-79.7c11.6,-11.6 30.3,-11.6 41.9,-0.1 5.6,5.6 8.7,13.1 8.7,21V407c0,16.4 -13.3,29.6 -29.6,29.6H642.9c-16.4,0 -29.6,-13.3 -29.6,-29.7 0,-7.8 3.1,-15.3 8.6,-20.9l91,-90.9z"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:viewportWidth="4800"
android:viewportHeight="4800">
<path
android:fillColor="#ffffff"
android:pathData="M2270.9,56.1L2527.1,56.1A200,307.5 0,0 1,2727.1 363.6L2727.1,1098.6A200,307.5 0,0 1,2527.1 1406.1L2270.9,1406.1A200,307.5 0,0 1,2070.9 1098.6L2070.9,363.6A200,307.5 0,0 1,2270.9 56.1z" />
<path
android:fillColor="#ffffff"
android:pathData="M2270.9,3406.1L2527.1,3406.1A200,307.5 0,0 1,2727.1 3713.6L2727.1,4448.6A200,307.5 0,0 1,2527.1 4756.1L2270.9,4756.1A200,307.5 0,0 1,2070.9 4448.6L2070.9,3713.6A200,307.5 0,0 1,2270.9 3406.1z" />
<path
android:fillColor="#ffffff"
android:pathData="M646.7,835L827.8,653.9A200,307.5 135,0 1,1186.7 729.9L1706.4,1249.6A200,307.5 135,0 1,1782.4 1608.5L1601.3,1789.6A307.5,200 45,0 1,1242.4 1713.6L722.7,1193.9A307.5,200 45,0 1,646.7 835z" />
<path
android:fillColor="#ffffff"
android:pathData="M3015.5,3203.8L3196.7,3022.7A200,307.5 135,0 1,3555.5 3098.7L4075.2,3618.4A307.5,200 45,0 1,4151.3 3977.3L3970.1,4158.4A307.5,200 45,0 1,3611.3 4082.4L3091.5,3562.7A307.5,200 45,0 1,3015.5 3203.8z" />
<path
android:fillColor="#ffffff"
android:pathData="M49,2534.2L49,2278A200,307.5 90,0 1,356.5 2078L1091.5,2078A200,307.5 90,0 1,1399 2278L1399,2534.2A200,307.5 90,0 1,1091.5 2734.2L356.5,2734.2A200,307.5 90,0 1,49 2534.2z" />
<path
android:fillColor="#ffffff"
android:pathData="M3399,2534.2L3399,2278.1A200,307.5 90,0 1,3706.5 2078.1L4441.5,2078.1A200,307.5 90,0 1,4749 2278.1L4749,2534.2A200,307.5 90,0 1,4441.5 2734.2L3706.5,2734.2A200,307.5 90,0 1,3399 2534.2z" />
<path
android:fillColor="#ffffff"
android:pathData="M827.8,4158.4L646.7,3977.3A307.5,200 135,0 1,722.7 3618.4L1242.4,3098.7A200,307.5 45,0 1,1601.3 3022.7L1782.4,3203.8A200,307.5 45,0 1,1706.4 3562.7L1186.7,4082.4A200,307.5 45,0 1,827.8 4158.4z" />
<path
android:fillColor="#ffffff"
android:pathData="M3196.7,1789.6L3015.5,1608.4A307.5,200 135,0 1,3091.5 1249.6L3611.3,729.9A307.5,200 135,0 1,3970.1 653.9L4151.3,835A200,307.5 45,0 1,4075.2 1193.9L3555.5,1713.6A200,307.5 45,0 1,3196.7 1789.6z" />
</vector>

View File

@@ -3,7 +3,7 @@
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M579.2,736.9l-218,-118.9a149.3,149.3 0,1 1,0 -212l218,-118.9a149.3,149.3 0,1 1,40.9 74.9l-218,118.9a149.9,149.9 0,0 1,0 62.2l218,118.9a149.3,149.3 0,1 1,-40.9 74.9z"
android:fillColor="#ffffff"/>
</vector>
<path
android:fillColor="#ffffff"
android:pathData="M579.2,736.9l-218,-118.9a149.3,149.3 0,1 1,0 -212l218,-118.9a149.3,149.3 0,1 1,40.9 74.9l-218,118.9a149.9,149.9 0,0 1,0 62.2l218,118.9a149.3,149.3 0,1 1,-40.9 74.9z" />
</vector>

View File

@@ -6,4 +6,4 @@
<path
android:fillColor="#ffffff"
android:pathData="M446.2,566.7h369.3c-2.9,103.8 -39.4,190.7 -109.4,260.7s-156.6,106.4 -259.9,109.4c-103.8,-2.9 -190.7,-39.4 -260.7,-109.4s-106.4,-156.9 -109.4,-260.7c2.9,-103.2 39.4,-189.9 109.4,-259.9S342.4,200.4 446.2,197.5v369.2zM829.5,183.5c70,70 106.4,156.9 109.4,260.7L568.7,444.2L568.7,74.1c103.9,2.9 190.8,39.4 260.8,109.4z" />
</vector>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:viewportWidth="1008.7"
android:viewportHeight="1008.7">
<path
android:fillColor="#ffffff"
android:pathData="M504.4,0C226.3,0 0,226.3 0,504.4 0,782.5 226.3,1008.7 504.4,1008.7c278.1,0 504.4,-226.3 504.4,-504.4C1008.7,226.3 782.5,0 504.4,0ZM786.6,407.7 L458.6,743.9c-7.8,8 -18.6,12.6 -29.8,12.6h-0.2c-11.1,0 -21.8,-4.4 -29.7,-12.3L222.5,567.9c-16.4,-16.4 -16.4,-43 0,-59.4 16.4,-16.4 43,-16.4 59.4,0L428.2,654.8 726.5,348.9c16.3,-16.6 42.9,-16.9 59.4,-0.7 16.6,16.2 16.9,42.8 0.7,59.4z" />
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M37,37C39.209,37 41,35.209 41,33C41,31.527 39.667,29.527 37,27C34.333,29.527 33,31.527 33,33C33,35.209 34.791,37 37,37Z" />
<path
android:fillColor="#00000000"
android:pathData="M20.854,5.504L24.389,9.04"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round" />
<path
android:fillColor="#00000000"
android:pathData="M23.682,8.333L8.125,23.889L19.439,35.203L34.995,19.646L23.682,8.333Z"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M12,20.073L28.961,25.65"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round" />
<path
android:fillColor="#00000000"
android:pathData="M4,43H44"
android:strokeWidth="4"
android:strokeColor="#ffffff"
android:strokeLineCap="round" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="150dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#ffffff"
android:pathData="m512,794a44.8,44.8 0,1 1,44.8 -44.8,44.8 44.8,0 0,1 -44.8,44.8zM471.9,230.8a40.1,40.1 0,0 1,80.2 0v369.1a40.1,40.1 0,0 1,-79.8 0zM512,0A512,512 0,1 0,1024 512,512 512,0 0,0 512,0Z" />
</vector>

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.debug.LoggerActivity"
tools:ignore="ContentDescription,UseCompoundDrawables,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
android:gravity="center|start"
android:paddingLeft="15dp"
android:paddingTop="13dp"
android:paddingRight="15dp"
android:paddingBottom="5dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/title_back_icon"
style="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_back"
android:tint="@color/colorTextGray"
android:tooltipText="@string/back" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="2.5dp"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:singleLine="true"
android:text="@string/debug_logs"
android:textColor="@color/colorTextGray"
android:textSize="19sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_count_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/this_contents_clear_when_restarts_tip"
android:textColor="@color/colorTextDark"
android:textSize="11.5sp"
android:tooltipText="@string/this_contents_clear_when_restarts_tip" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:gravity="center|end"
android:orientation="horizontal">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/refresh_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_refresh"
android:tint="@color/colorTextGray"
android:tooltipText="@string/refresh" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/filter_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_filter"
android:tint="@color/colorTextGray"
android:tooltipText="@string/filter_by_condition" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/export_all_icon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_export"
android:tint="@color/colorTextGray"
android:tooltipText="@string/export_all" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp">
<TextView
android:id="@+id/list_no_data_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:lineSpacingExtra="6dp"
android:text="@string/no_list_data"
android:textColor="@color/colorTextDark"
android:textSize="17sp" />
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/trans"
android:dividerHeight="5dp"
android:fadingEdgeLength="10dp"
android:listSelector="@color/trans"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="5dp"
android:requiresFadingEdge="vertical"
android:scrollbars="none"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -8,7 +8,7 @@
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.errors.AppErrorsDetailActivity"
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp">
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
@@ -27,7 +27,7 @@
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:src="@mipmap/ic_back"
android:src="@drawable/ic_back"
android:tint="@color/colorTextGray"
android:tooltipText="@string/back" />
@@ -151,6 +151,23 @@
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:id="@+id/app_user_id_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/bg_blue_round"
android:ellipsize="end"
android:paddingLeft="3dp"
android:paddingTop="0.5dp"
android:paddingRight="3dp"
android:paddingBottom="0.5dp"
android:singleLine="true"
android:text="@string/user_id"
android:textColor="@color/white"
android:textSize="9sp"
android:visibility="gone" />
<ImageView
android:layout_width="13dp"
android:layout_height="13dp"
@@ -165,7 +182,7 @@
android:orientation="horizontal">
<TextView
android:id="@+id/app_abi_text"
android:id="@+id/app_cpu_abi_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
@@ -420,8 +437,52 @@
</LinearLayout>
</LinearLayout>
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/disable_auto_wrap_error_stack_trace_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_permotion_round"
android:paddingLeft="15dp"
android:paddingTop="10dp"
android:paddingRight="15dp"
android:paddingBottom="10dp"
android:text="@string/disable_auto_wrap_error_stack_trace_content"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<HorizontalScrollView
android:id="@+id/error_stack_trace_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/bg_stack_round"
android:fadingEdgeLength="10dp"
android:fillViewport="true"
android:overScrollMode="never"
android:padding="15dp"
android:requiresFadingEdge="horizontal"
android:scrollbars="none"
android:visibility="gone">
<TextView
android:id="@+id/error_stack_trace_movable_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:lineSpacingExtra="5dp"
android:textColor="#B65B57"
android:textIsSelectable="true"
android:textSize="12sp"
android:typeface="monospace" />
</HorizontalScrollView>
<TextView
android:id="@+id/error_stack_text"
android:id="@+id/error_stack_trace_fixed_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"

View File

@@ -6,7 +6,7 @@
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.errors.AppErrorsRecordActivity"
tools:ignore="ContentDescription,UseCompoundDrawables">
tools:ignore="ContentDescription,UseCompoundDrawables,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
@@ -25,7 +25,7 @@
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:src="@mipmap/ic_back"
android:src="@drawable/ic_back"
android:tint="@color/colorTextGray"
android:tooltipText="@string/back" />
@@ -77,7 +77,7 @@
android:listSelector="@color/trans"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="15dp"
android:paddingBottom="5dp"
android:requiresFadingEdge="vertical"
android:scrollbars="none"
android:visibility="gone" />

View File

@@ -6,7 +6,7 @@
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.errors.AppErrorsRecordActivity"
tools:ignore="ContentDescription,UseCompoundDrawables">
tools:ignore="ContentDescription,UseCompoundDrawables,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
@@ -25,7 +25,7 @@
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:src="@mipmap/ic_back"
android:src="@drawable/ic_back"
android:tint="@color/colorTextGray"
android:tooltipText="@string/back" />
@@ -97,7 +97,7 @@
android:listSelector="@color/trans"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="15dp"
android:paddingBottom="5dp"
android:requiresFadingEdge="vertical"
android:scrollbars="none"
android:visibility="gone" />

View File

@@ -6,7 +6,7 @@
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.main.ConfigureActivity"
tools:ignore="ContentDescription,UseCompoundDrawables">
tools:ignore="ContentDescription,UseCompoundDrawables,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
@@ -25,7 +25,7 @@
android:layout_height="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="20dp"
android:src="@mipmap/ic_back"
android:src="@drawable/ic_back"
android:tint="@color/colorTextGray"
android:tooltipText="@string/back" />
@@ -57,6 +57,16 @@
android:textSize="11.5sp" />
</LinearLayout>
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/global_icon"
android:layout_width="23dp"
android:layout_height="23dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_global"
android:tint="@color/colorTextGray"
android:tooltipText="@string/global_config"
android:visibility="gone" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/batch_icon"
android:layout_width="25dp"
@@ -112,7 +122,7 @@
android:listSelector="@color/trans"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="15dp"
android:paddingBottom="5dp"
android:requiresFadingEdge="vertical"
android:scrollbars="none"
android:visibility="gone" />

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.main.MainActivity"
tools:ignore="UseCompoundDrawables,ContentDescription,SmallSp">
tools:ignore="UseCompoundDrawables,ContentDescription,SmallSp,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
@@ -29,13 +30,25 @@
android:textStyle="bold" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/title_restart_icon"
android:id="@+id/title_logger_icon"
style="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="15dp"
android:alpha="0.85"
android:src="@mipmap/ic_restart"
android:padding="0.5dp"
android:src="@drawable/ic_debug"
android:tint="@color/colorTextGray"
android:tooltipText="@string/debug_logs" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/title_restart_icon"
style="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginEnd="15dp"
android:alpha="0.85"
android:src="@drawable/ic_restart"
android:tint="@color/colorTextGray"
android:tooltipText="@string/restart_system" />
@@ -46,7 +59,7 @@
android:layout_height="27dp"
android:layout_marginEnd="5dp"
android:alpha="0.85"
android:src="@mipmap/ic_github"
android:src="@drawable/ic_github"
android:tint="@color/colorTextGray"
android:tooltipText="@string/project_address" />
</LinearLayout>
@@ -69,7 +82,7 @@
android:layout_height="25dp"
android:layout_marginStart="25dp"
android:layout_marginEnd="5dp"
android:src="@mipmap/ic_warn"
android:src="@drawable/ic_warn"
android:tint="@color/white" />
<LinearLayout
@@ -187,11 +200,21 @@
android:layout_marginBottom="10dp"
android:gravity="center|start">
<ImageView
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
android:src="@mipmap/ic_basic" />
app:cardBackgroundColor="#009688"
app:cardCornerRadius="50dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="2.5dp"
android:src="@drawable/ic_preference" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
@@ -203,7 +226,7 @@
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.view.MaterialSwitch
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/only_show_errors_in_front_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
@@ -227,7 +250,7 @@
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<com.fankes.apperrorstracking.ui.view.MaterialSwitch
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/only_show_errors_in_main_process_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
@@ -251,7 +274,7 @@
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<com.fankes.apperrorstracking.ui.view.MaterialSwitch
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/always_shows_reopen_app_options_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
@@ -275,7 +298,7 @@
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<com.fankes.apperrorstracking.ui.view.MaterialSwitch
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/enable_apps_configs_template_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
@@ -317,6 +340,71 @@
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:background="@drawable/bg_permotion_round"
android:elevation="0dp"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingTop="15dp"
android:paddingRight="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start">
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
app:cardBackgroundColor="#BA68C8"
app:cardCornerRadius="50dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="2.5dp"
android:src="@drawable/ic_theme" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.85"
android:singleLine="true"
android:text="@string/theme_settings"
android:textColor="@color/colorTextGray"
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/enable_material3_app_errors_dialog_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_md3_app_errors_dialog"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/enable_md3_app_errors_dialog_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -337,11 +425,21 @@
android:layout_marginBottom="15dp"
android:gravity="center|start">
<ImageView
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
android:src="@mipmap/ic_control" />
app:cardBackgroundColor="#2196F3"
app:cardCornerRadius="50dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="2.5dp"
android:src="@drawable/ic_function" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
@@ -427,11 +525,21 @@
android:layout_height="wrap_content"
android:gravity="center|start">
<ImageView
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
android:src="@mipmap/ic_home" />
app:cardBackgroundColor="#FFFF9800"
app:cardCornerRadius="50dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="2.5dp"
android:src="@drawable/ic_home" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
@@ -443,7 +551,7 @@
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.view.MaterialSwitch
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/hide_icon_in_launcher_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -473,6 +581,71 @@
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="15dp"
android:layout_marginRight="15dp"
android:background="@drawable/bg_permotion_round"
android:elevation="0dp"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingTop="15dp"
android:paddingRight="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start">
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
app:cardBackgroundColor="#FFCB2E62"
app:cardCornerRadius="50dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="2dp"
android:src="@drawable/ic_appcenter" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="0.85"
android:singleLine="true"
android:text="@string/microsoft_app_center"
android:textColor="@color/colorTextGray"
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/enable_anonymous_statistics_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_anonymous_statistics"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/enable_anonymous_statistics_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -39,16 +39,39 @@
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:id="@+id/app_name_text"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_weight="0.75"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
android:gravity="center|start">
<TextView
android:id="@+id/app_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:id="@+id/app_user_id_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/bg_blue_round"
android:ellipsize="end"
android:paddingLeft="3dp"
android:paddingTop="0.5dp"
android:paddingRight="3dp"
android:paddingBottom="0.5dp"
android:singleLine="true"
android:text="@string/user_id"
android:textColor="@color/white"
android:textSize="9sp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/errors_time_text"

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_permotion_round"
android:orientation="horizontal">
<TextView
android:id="@+id/priority_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:padding="5dp"
android:textColor="@color/white"
android:textSize="13sp"
android:typeface="monospace" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/message_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="3dp"
android:textColor="@color/colorTextGray"
android:textSize="13sp" />
<TextView
android:id="@+id/time_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
<TextView
android:id="@+id/throwable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:lineSpacingExtra="3dp"
android:textColor="#EF5350"
android:textSize="11sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@@ -27,27 +27,39 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/show_errors_dialog"
android:singleLine="true"
android:text="@string/follow_global_config"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/config_radio_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/show_errors_notify"
android:singleLine="true"
android:text="@string/show_errors_dialog"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/config_radio_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/show_errors_toast"
android:singleLine="true"
android:text="@string/show_errors_notify"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/config_radio_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@string/show_errors_toast"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/config_radio_4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@string/show_nothing"
app:buttonTint="@color/colorPrimaryAccent" />
</RadioGroup>

View File

@@ -19,136 +19,157 @@
android:textSize="12sp"
android:visibility="gone" />
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/app_info_item"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
android:layout_height="match_parent"
android:fadingEdgeLength="10dp"
android:fillViewport="true"
android:requiresFadingEdge="vertical"
android:scrollbars="none">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:padding="1dp"
android:src="@drawable/ic_baseline_info"
app:tint="@color/colorTextGray" />
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_info"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
android:orientation="vertical">
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/close_app_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/app_info_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_close"
app:tint="@color/colorTextGray" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/app_info_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:padding="1dp"
android:src="@drawable/ic_baseline_info"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/close_app"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_info"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/reopen_app_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/close_app_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_refresh"
app:tint="@color/colorTextGray" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/close_app_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_close"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/reopen_app"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/close_app"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/error_detail_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/reopen_app_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_bug_report"
app:tint="@color/colorTextGray" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/reopen_app_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_refresh"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/error_detail"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/reopen_app"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/muted_if_unlock_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/error_detail_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_eject"
app:tint="@color/colorTextGray" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/error_detail_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_bug_report"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mute_if_unlock"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/error_detail"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
<com.fankes.apperrorstracking.ui.view.ItemLinearLayout
android:id="@+id/muted_if_restart_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/muted_if_unlock_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_eject"
app:tint="@color/colorTextGray" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/muted_if_unlock_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_eject"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mute_if_restart"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.view.ItemLinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mute_if_unlock"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
<com.fankes.apperrorstracking.ui.widget.ItemLinearLayout
android:id="@+id/muted_if_restart_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:paddingVertical="15dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/muted_if_restart_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_baseline_eject"
app:tint="@color/colorTextGray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mute_if_restart"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
</com.fankes.apperrorstracking.ui.widget.ItemLinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -21,11 +21,37 @@
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/contains_system_switch"
<RadioGroup
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/result_contains_system_apps"
app:buttonTint="@color/colorPrimaryAccent" />
android:orientation="horizontal">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/filters_radio_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:checked="true"
android:singleLine="true"
android:text="@string/user_apps"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/filters_radio_system"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:singleLine="true"
android:text="@string/system_apps"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/filters_radio_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@string/all_apps"
app:buttonTint="@color/colorPrimaryAccent" />
</RadioGroup>
</LinearLayout>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingTop="15dp"
android:paddingRight="15dp"
tools:ignore="HardcodedText">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8.5dp"
android:layout_marginRight="8.5dp"
android:layout_marginBottom="10dp"
android:lineSpacingExtra="6dp"
android:text="@string/when_logger_how_to_show_tip"
android:textSize="13sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/config_check_0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="DEBUG"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/config_check_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="INFO"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/config_check_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="WARN"
app:buttonTint="@color/colorPrimaryAccent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/config_check_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="ERROR"
app:buttonTint="@color/colorPrimaryAccent" />
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More