feat: lots of changes

- add BuildConfigWrapper
- add project promote
- add ci version tag support
- change app analytics config item show when available
- fix system api compat issues
This commit is contained in:
2023-09-19 05:12:44 +08:00
parent ccc50d720e
commit c1e584d739
136 changed files with 357 additions and 101 deletions

View File

@@ -0,0 +1,24 @@
package com.fankes.apperrorstracking
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.fankes.apperrorstracking", appContext.packageName)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,43 @@
/*
* 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/5/10.
*/
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() {
override fun onCreate() {
super.onCreate()
/** 跟随系统夜间模式 */
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
/** 绑定 I18n */
LocaleString.bind(instance = this)
/** 装载存储控制类 */
ConfigData.init(instance = this)
/** 装载 App Center */
AppAnalyticsTool.init(instance = this)
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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/6/1.
*/
package com.fankes.apperrorstracking.bean
import java.io.Serializable
/**
* 应用异常信息显示 bean
* @param pid APP 进程 ID
* @param userId APP 用户 ID
* @param packageName APP 包名
* @param processName APP 进程名
* @param appName APP 名称
* @param title 标题
* @param isShowAppInfoButton 是否显示应用信息按钮
* @param isShowCloseAppButton 是否显示关闭应用按钮
* @param isShowReopenButton 是否显示重新打开按钮
*/
data class AppErrorsDisplayBean(
var pid: Int,
var userId: Int,
var packageName: String,
var processName: String,
var appName: String,
var title: String,
var isShowAppInfoButton: Boolean,
var isShowCloseAppButton: Boolean,
var isShowReopenButton: Boolean
) : Serializable

View File

@@ -0,0 +1,220 @@
/*
* 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/5/10.
*/
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.appCpuAbiOf
import com.fankes.apperrorstracking.utils.factory.appVersionCodeOf
import com.fankes.apperrorstracking.utils.factory.appVersionNameOf
import com.fankes.apperrorstracking.utils.factory.difference
import com.fankes.apperrorstracking.utils.factory.toUtcTime
import com.google.gson.annotations.SerializedName
import java.io.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 应用异常信息 bean
* @param pid 进程 ID
* @param userId 用户 ID
* @param cpuAbi CPU 架构类型
* @param packageName 包名
* @param versionName 版本名称
* @param versionCode 版本号
* @param isNativeCrash 是否为原生层异常
* @param exceptionClassName 异常类名
* @param exceptionMessage 异常信息
* @param throwClassName 抛出异常的类名
* @param throwFileName 抛出异常的文件名
* @param throwMethodName 抛出异常的方法名
* @param throwLineNumber 抛出异常的行号
* @param stackTrace 异常堆栈
* @param timestamp 记录时间戳
*/
data class AppErrorsInfoBean(
@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(context: Context, pid: Int, userId: Int, packageName: String?, crashInfo: ApplicationErrorReport.CrashInfo?) =
(crashInfo?.exceptionClassName?.lowercase() == "native crash").let { isNativeCrash ->
AppErrorsInfoBean(
pid = pid,
userId = userId,
cpuAbi = packageName?.let { context.appCpuAbiOf(it) } ?: "",
packageName = packageName ?: "unknown",
versionName = packageName?.let { context.appVersionNameOf(it).ifBlank { "unknown" } } ?: "",
versionCode = packageName?.let { context.appVersionCodeOf(it) } ?: -1L,
isNativeCrash = isNativeCrash,
exceptionClassName = crashInfo?.exceptionClassName ?: "unknown",
exceptionMessage = if (isNativeCrash) crashInfo?.stackTrace.let {
if (it?.contains("Abort message: '") == true)
runCatching { it.split("Abort message: '")[1].split("'")[0] }.getOrNull()
?: 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() ?: "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]
*/
val crossTime
get() = timestamp.difference(
now = LocaleString.momentAgo,
second = LocaleString.secondAgo,
minute = LocaleString.minuteAgo,
hour = LocaleString.hourAgo,
day = LocaleString.dayAgo,
month = LocaleString.monthAgo,
year = LocaleString.yearAgo
)
/**
* 获取异常本地化时间
* @return [String]
*/
val dateTime get() = SimpleDateFormat.getDateTimeInstance().format(Date(timestamp)) ?: utcTime
/**
* 获取异常堆栈分享模板
* @return [String]
*/
val stackOutputShareContent
get() = """
Generated by AppErrorsTracking
Project Url: https://github.com/KitsunePie/AppErrorsTracking
===============
$environmentInfo
""".trimIndent()
/**
* 获取异常堆栈文件模板
* @return [String]
*/
val stackOutputFileContent
get() = """
================================================================
Generated by AppErrorsTracking
Project Url: https://github.com/KitsunePie/AppErrorsTracking
================================================================
""".trimIndent()
/**
* 获取运行环境信息
* @return [String]
*/
private val environmentInfo
get() = """
[Device Brand]: ${Build.BRAND}
[Device Model]: ${Build.MODEL}
[Display]: ${Build.DISPLAY}
[Android Version]: ${Build.VERSION.RELEASE}
[Android API Level]: ${Build.VERSION.SDK_INT}
[System Locale]: ${Locale.getDefault()}
[Process ID]: $pid
[User ID]: $userId
[CPU ABI]: ${cpuAbi.ifBlank { "none" }}
[Package Name]: $packageName
[Version Name]: ${versionName.ifBlank { "unknown" }}
[Version Code]: ${versionCode.takeIf { it != -1L } ?: "unknown"}
[Error Type]: ${if (isNativeCrash) "Native" else "JVM"}
[Crash Time]: $utcTime
[Stack Trace]:
$stackTrace
""".trimIndent()
}

View File

@@ -0,0 +1,35 @@
/*
* 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/6/4.
*/
package com.fankes.apperrorstracking.bean
import com.fankes.apperrorstracking.bean.enum.AppFiltersType
import java.io.Serializable
/**
* 应用过滤条件 bean
* @param name 名称或包名
* @param type 过滤条件类型
*/
data class AppFiltersBean(
var name: String = "",
var type: AppFiltersType = AppFiltersType.USER
) : Serializable

View File

@@ -0,0 +1,37 @@
/*
* 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/6/8.
*/
package com.fankes.apperrorstracking.bean
import android.graphics.drawable.Drawable
import java.io.Serializable
/**
* 应用信息 bean
* @param icon 图标
* @param name APP 名称
* @param packageName APP 包名
*/
data class AppInfoBean(
var icon: Drawable? = null,
var name: String,
var packageName: String
) : Serializable

View File

@@ -0,0 +1,40 @@
/*
* 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/6/3.
*/
package com.fankes.apperrorstracking.bean
import java.io.Serializable
/**
* 已忽略异常的应用 bean
* @param type 类型
* @param packageName 包名
*/
data class MutedErrorsAppBean(
var type: MuteType,
var packageName: String
) : Serializable {
/**
* 已忽略的异常类型
*/
enum class MuteType { UNTIL_UNLOCKS, UNTIL_REBOOTS }
}

View File

@@ -0,0 +1,36 @@
/*
* 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/22.
*/
package com.fankes.apperrorstracking.bean.enum
/**
* 应用过滤条件类型定义类
*/
enum class AppFiltersType {
/** 用户 */
USER,
/** 系统 */
SYSTEM,
/** 全部 */
ALL
}

View File

@@ -0,0 +1,59 @@
/*
* 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/9/19.
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package com.fankes.apperrorstracking.const
import com.fankes.apperrorstracking.generated.ModuleAppProperties
import com.fankes.apperrorstracking.wrapper.BuildConfigWrapper
/**
* 包名常量定义类
*/
object PackageName {
/** 系统框架 */
const val SYSTEM_FRAMEWORK = "android"
}
/**
* 模块版本常量定义类
*/
object ModuleVersion {
/** 当前 GitHub 提交的 ID (CI 自动构建) */
const val GITHUB_COMMIT_ID = ModuleAppProperties.GITHUB_CI_COMMIT_ID
/** 版本名称 */
const val NAME = BuildConfigWrapper.VERSION_NAME
/** 版本号 */
const val CODE = BuildConfigWrapper.VERSION_CODE
/** 是否为 CI 自动构建版本 */
val isCiMode = GITHUB_COMMIT_ID.isNotBlank()
/** 当前版本名称后缀 */
val suffix = GITHUB_COMMIT_ID.let { if (it.isNotBlank()) "-$it" else "" }
override fun toString() = "$NAME$suffix($CODE)"
}

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,138 @@
/*
* 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.appCpuAbiOf
import com.fankes.apperrorstracking.utils.factory.appVersionCodeOf
import com.fankes.apperrorstracking.utils.factory.appVersionNameOf
import com.fankes.apperrorstracking.utils.factory.toEntityOrNull
import com.fankes.apperrorstracking.utils.factory.toJsonOrNull
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).ifBlank { "unknown" }
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,206 @@
/*
* 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 com.highcapable.yukihookapi.hook.factory.prefs
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).prefs().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).prefs().edit { 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).prefs().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).prefs().edit { 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]
*/
internal fun getBoolean(data: PrefsData<Boolean>) = when (instance) {
is Context -> (instance as Context).prefs().get(data)
is PackageParam -> (instance as PackageParam).prefs.get(data)
else -> error("Unknown type for get prefs data")
}
/**
* 存入 [Boolean] 数据
* @param data 键值数据模板
* @param value 键值内容
*/
internal fun putBoolean(data: PrefsData<Boolean>, value: Boolean) {
when (instance) {
is Context -> (instance as Context).prefs().edit { put(data, value) }
is PackageParam -> loggerW(msg = "Not support for this method")
else -> error("Unknown type for put prefs data")
}
}
/**
* 是否显示开发者提示
* @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

@@ -0,0 +1,42 @@
/*
* 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.enum
/**
* 应用配置模版类型定义类
*/
enum class AppErrorsConfigType {
/** 跟随全局配置 */
GLOBAL,
/** 对话框 */
DIALOG,
/** 通知 */
NOTIFY,
/** Toast */
TOAST,
/** 什么也不显示 */
NOTHING
}

View File

@@ -0,0 +1,101 @@
/*
* 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/2/3.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.fankes.apperrorstracking.data.factory
import android.widget.CompoundButton
import com.fankes.apperrorstracking.data.ConfigData
import com.highcapable.yukihookapi.hook.xposed.prefs.data.PrefsData
/**
* 绑定到 [CompoundButton] 自动设置选中状态
* @param data 键值数据模板
* @param initiate 方法体
*/
fun CompoundButton.bind(data: PrefsData<Boolean>, initiate: CompoundButtonDataBinder.(CompoundButton) -> Unit = {}) {
val binder = CompoundButtonDataBinder(button = this).also { initiate(it, this) }
isChecked = ConfigData.getBoolean(data).also { binder.initializeCallback?.invoke(it) }
binder.applyChangesCallback = { ConfigData.putBoolean(data, it) }
setOnCheckedChangeListener { button, isChecked ->
if (button.isPressed) {
if (binder.isAutoApplyChanges) binder.applyChangesCallback?.invoke(isChecked)
binder.changedCallback?.invoke(isChecked)
}
}
}
/**
* [CompoundButton] 数据绑定管理器实例
* @param button 当前实例
*/
class CompoundButtonDataBinder(private val button: CompoundButton) {
/** 状态初始化回调事件 */
internal var initializeCallback: ((Boolean) -> Unit)? = null
/** 状态改变回调事件 */
internal var changedCallback: ((Boolean) -> Unit)? = null
/** 应用更改回调事件 */
internal var applyChangesCallback: ((Boolean) -> Unit)? = null
/** 是否启用自动应用更改 */
var isAutoApplyChanges = true
/**
* 监听状态初始化
* @param result 回调结果
*/
fun onInitialize(result: (Boolean) -> Unit) {
initializeCallback = result
}
/**
* 监听状态改变
* @param result 回调结果
*/
fun onChanged(result: (Boolean) -> Unit) {
changedCallback = result
}
/** 重新初始化 */
fun reinitialize() {
initializeCallback?.invoke(button.isChecked)
}
/** 应用更改并重新初始化 */
fun applyChangesAndReinitialize() {
applyChanges()
reinitialize()
}
/** 应用更改 */
fun applyChanges() {
applyChangesCallback?.invoke(button.isChecked)
}
/** 取消更改 */
fun cancelChanges() {
button.isChecked = button.isChecked.not()
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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/5/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
import com.highcapable.yukihookapi.hook.factory.configs
import com.highcapable.yukihookapi.hook.factory.encase
import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit
@InjectYukiHookWithXposed(entryClassName = "AppErrorsTracking", isUsingResourcesHook = false)
object HookEntry : IYukiHookXposedInit {
override fun onInit() = configs {
debugLog {
tag = "AppErrorsTracking"
isRecord = true
}
isDebug = false
}
override fun onHook() = encase {
loadSystem {
LocaleString.bind(instance = this)
ConfigData.init(instance = this)
loadHooker(FrameworkHooker)
}
}
}

View File

@@ -0,0 +1,465 @@
/*
* 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/5/7.
*/
@file:Suppress("ConstPropertyName")
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.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.R
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.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
import com.fankes.apperrorstracking.utils.factory.appNameOf
import com.fankes.apperrorstracking.utils.factory.drawableOf
import com.fankes.apperrorstracking.utils.factory.isAppCanOpened
import com.fankes.apperrorstracking.utils.factory.listOfPackages
import com.fankes.apperrorstracking.utils.factory.openApp
import com.fankes.apperrorstracking.utils.factory.pushNotify
import com.fankes.apperrorstracking.utils.factory.toArrayList
import com.fankes.apperrorstracking.utils.factory.toast
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
import com.fankes.apperrorstracking.wrapper.BuildConfigWrapper
import com.highcapable.yukihookapi.hook.bean.VariousClass
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
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() {
private const val ActivityManagerServiceClass = "com.android.server.am.ActivityManagerService"
private const val UserControllerClass = "com.android.server.am.UserController"
private const val AppErrorsClass = "com.android.server.am.AppErrors"
private const val AppErrorDialogClass = "com.android.server.am.AppErrorDialog"
private const val AppErrorDialog_DataClass = "com.android.server.am.AppErrorDialog\$Data"
private const val ProcessRecordClass = "com.android.server.am.ProcessRecord"
private const val ActivityTaskManagerService_LocalServiceClass = "com.android.server.wm.ActivityTaskManagerService\$LocalService"
private val PackageListClass = VariousClass(
"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"
)
/** 已忽略错误的 APP 数组 - 直到重新解锁 */
private var mutedErrorsIfUnlockApps = HashSet<String>()
/** 已忽略错误的 APP 数组 - 直到重新启动 */
private var mutedErrorsIfRestartApps = HashSet<String>()
/**
* APP 进程异常数据定义类
* @param errors [AppErrorsClass] 实例
* @param proc [ProcessRecordClass] 实例
* @param resultData [AppErrorDialog_DataClass] 实例 - 默认空
*/
private class AppErrorsProcessData(errors: Any?, proc: Any?, resultData: Any? = null) {
/**
* 获取当前包列表实例
* @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) {
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() }
?.forEach { add(MutedErrorsAppBean(MutedErrorsAppBean.MuteType.UNTIL_UNLOCKS, it)) }
mutedErrorsIfRestartApps.takeIf { it.isNotEmpty() }
?.forEach { add(MutedErrorsAppBean(MutedErrorsAppBean.MuteType.UNTIL_REBOOTS, it)) }
}
}
onUnmuteErrorsApp {
when (it.type) {
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 ->
appContext?.let { context ->
context.listOfPackages()
.filter { it.packageName.let { e -> e != "android" && e != BuildConfigWrapper.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).ifBlank { 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 == BuildConfigWrapper.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() {
/** 注册生命周期 */
registerLifecycle()
/** 干掉原生错误对话框 - 如果有 */
ErrorDialogControllerClass.hook {
injectMember {
method {
name = "hasCrashDialogs"
emptyParam()
}
replaceToTrue()
}
injectMember {
method {
name = "showCrashDialogs"
paramCount = 1
}
intercept()
}
}.ignoredHookClassNotFoundFailure()
/** 干掉原生错误对话框 - API 30 以下 */
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 {
method {
name = "onCreate"
param(BundleClass)
}
afterHook { instance<Dialog>().cancel() }
}.ignoredNoSuchMemberFailure()
injectMember {
method {
name = "onStart"
emptyParam()
}
afterHook { instance<Dialog>().cancel() }
}.ignoredNoSuchMemberFailure()
}
/** 注入自定义错误对话框 */
AppErrorsClass.hook {
injectMember {
method {
name = "handleShowAppErrorUi"
param(MessageClass)
}
afterHook {
/** 当前实例 */
val context = appContext ?: field { name = "mContext" }.get(instance).cast<Context>() ?: return@afterHook
/** 当前错误数据 */
val resultData = args().first().cast<Message>()?.obj
/** 当前进程信息 */
val proc = AppErrorDialog_DataClass.toClass().field { name = "proc" }.get(resultData).any()
/** 创建 APP 进程异常数据类 */
AppErrorsProcessData(instance, proc, resultData).handleShowAppErrorUi(context)
}
}
injectMember {
method {
name = "handleAppCrashInActivityController"
returnType = BooleanType
}
afterHook {
/** 当前实例 */
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

@@ -0,0 +1,35 @@
/*
* 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/5/11.
*/
package com.fankes.apperrorstracking.service
import android.service.quicksettings.TileService
import com.fankes.apperrorstracking.ui.activity.errors.AppErrorsRecordActivity
import com.fankes.apperrorstracking.utils.factory.navigate
class QuickStartTileService : TileService() {
override fun onClick() {
super.onClick()
/** 启动异常历史记录窗口 */
navigate<AppErrorsRecordActivity>()
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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/5/7.
*/
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
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.WindowCompat
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
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
/** 获取绑定布局对象 */
lateinit var binding: VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = current().generic()?.argument()?.method {
name = "inflate"
param(LayoutInflaterClass)
}?.get()?.invoke<VB>(layoutInflater) ?: error("binding failed")
setContentView(binding.root)
/** 隐藏系统的标题栏 */
supportActionBar?.hide()
/** 初始化沉浸状态栏 */
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = isNotSystemInDarkMode
isAppearanceLightNavigationBars = isNotSystemInDarkMode
}
ResourcesCompat.getColor(resources, R.color.colorThemeBackground, null).also {
window?.statusBarColor = it
window?.navigationBarColor = it
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) window?.navigationBarDividerColor = it
}
/** 装载子类 */
onCreate()
}
/** 回调 [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 名称
*/
fun toastAndFinish(name: String) {
toast(msg = "Invalid $name, exit")
finish()
}
}

View File

@@ -0,0 +1,190 @@
/*
* 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.const.PackageName
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.bindAdapter
import com.fankes.apperrorstracking.utils.factory.copyToClipboard
import com.fankes.apperrorstracking.utils.factory.showDialog
import com.fankes.apperrorstracking.utils.factory.toUtcTime
import com.fankes.apperrorstracking.utils.factory.toast
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.Date
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(PackageName.SYSTEM_FRAMEWORK).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

@@ -0,0 +1,193 @@
/*
* 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/5/7.
*/
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
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.factory.bind
import com.fankes.apperrorstracking.databinding.ActivityAppErrorsDetailBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
import com.fankes.apperrorstracking.utils.factory.appIconOf
import com.fankes.apperrorstracking.utils.factory.appNameOf
import com.fankes.apperrorstracking.utils.factory.copyToClipboard
import com.fankes.apperrorstracking.utils.factory.dp
import com.fankes.apperrorstracking.utils.factory.getSerializableExtraCompat
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.highcapable.yukihookapi.hook.log.loggerE
class AppErrorsDetailActivity : BaseActivity<ActivityAppErrorsDetailBinding>() {
companion object {
/** 请求保存文件回调标识 */
private const val WRITE_REQUEST_CODE = 0
/** [AppErrorsInfoBean] 传值 */
private const val EXTRA_APP_ERRORS_INFO = "app_errors_info_extra"
/**
* 启动 [AppErrorsDetailActivity]
* @param context 实例
* @param appErrorsInfo 应用异常信息
*/
fun start(context: Context, appErrorsInfo: AppErrorsInfoBean) =
context.navigate<AppErrorsDetailActivity> { putExtra(EXTRA_APP_ERRORS_INFO, appErrorsInfo) }
}
/** 预导出的异常堆栈 */
private var stackTrace = ""
override fun onCreate() {
if (initUi(intent).not()) return
binding.titleBackIcon.setOnClickListener { onBackPressed() }
binding.disableAutoWrapErrorStackTraceSwitch.bind(ConfigData.DISABLE_AUTO_WRAP_ERROR_STACK_TRACE) {
onInitialize {
binding.errorStackTraceScrollView.isVisible = it
binding.errorStackTraceFixedText.isGone = it
}
onChanged {
reinitialize()
resetScrollView()
}
}
binding.detailTitleText.setOnClickListener { binding.appPanelScrollView.smoothScrollTo(0, 0) }
resetScrollView()
}
/**
* 从 [Intent] 中解析 [AppErrorsInfoBean] 并加载至界面
*
* @param intent 用于获取并解析 [AppErrorsInfoBean] 的 [Intent] 实例
* @return [Boolean] 是否解析成功true 为成功false 为失败,可能是 [Intent] 为空或者 [AppErrorsInfoBean] 为空
*/
private fun initUi(intent: Intent?): Boolean {
val appErrorsInfo = runCatching { intent?.getSerializableExtraCompat<AppErrorsInfoBean>(EXTRA_APP_ERRORS_INFO) }.getOrNull()
if (appErrorsInfo == null) {
toastAndFinish(name = "AppErrorsInfo")
return false
}
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 false
}
binding.appInfoItem.setOnClickListener { openSelfSetting(appErrorsInfo.packageName) }
binding.printIcon.setOnClickListener {
loggerE(msg = appErrorsInfo.stackTrace)
toast(LocaleString.printToLogcatSuccess)
}
binding.copyIcon.setOnClickListener { copyToClipboard(appErrorsInfo.stackOutputShareContent) }
binding.exportIcon.setOnClickListener {
stackTrace = appErrorsInfo.stackOutputFileContent
runCatching {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/application"
putExtra(Intent.EXTRA_TITLE, "${appErrorsInfo.packageName}_${appErrorsInfo.utcTime}.log")
}, WRITE_REQUEST_CODE)
}.onFailure { toast(msg = "Start Android SAF failed") }
}
binding.shareIcon.setOnClickListener {
startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, appErrorsInfo.stackOutputShareContent)
}, LocaleString.shareErrorStack))
}
binding.appIcon.setImageDrawable(appIconOf(appErrorsInfo.packageName))
binding.appNameText.text = appNameOf(appErrorsInfo.packageName).ifBlank { 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
binding.errorTypeText.text = appErrorsInfo.exceptionClassName
binding.errorFileNameText.text = appErrorsInfo.throwFileName
binding.errorThrowClassText.text = appErrorsInfo.throwClassName
binding.errorThrowMethodText.text = appErrorsInfo.throwMethodName
binding.errorLineNumberText.text = appErrorsInfo.throwLineNumber.toString()
binding.errorRecordTimeText.text = appErrorsInfo.dateTime
binding.errorStackTraceMovableText.text = appErrorsInfo.stackTrace
binding.errorStackTraceFixedText.text = appErrorsInfo.stackTrace
binding.appPanelScrollView.setOnScrollChangeListener { _, _, y, _, _ ->
binding.detailTitleText.text = if (y >= 30.dp(context = this@AppErrorsDetailActivity))
appNameOf(appErrorsInfo.packageName).ifBlank { appErrorsInfo.packageName }
else LocaleString.appName
}
return true
}
/** 修复在一些小屏设备上设置了 [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?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) runCatching {
data?.data?.let {
contentResolver?.openOutputStream(it)?.apply { write(stackTrace.toByteArray()) }?.close()
toast(LocaleString.outputStackSuccess)
} ?: toast(LocaleString.outputStackFail)
}.onFailure { toast(LocaleString.outputStackFail) }
}
override fun onBackPressed() {
intent?.removeExtra(EXTRA_APP_ERRORS_INFO)
finish()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (initUi(intent)) binding.appPanelScrollView.scrollTo(0, 0)
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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/6/1.
*/
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.colorOf
import com.fankes.apperrorstracking.utils.factory.getSerializableExtraCompat
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.tool.FrameworkTool
class AppErrorsDisplayActivity : BaseActivity<ActivityAppErrorsDisplayBinding>() {
companion object {
/** 当前实例 - 单例运行 */
private var instance: AppErrorsDisplayActivity? = null
/** [AppErrorsDisplayBean] 传值 */
private const val EXTRA_APP_ERRORS_DISPLAY = "app_errors_display_extra"
/**
* 启动 [AppErrorsDisplayActivity]
* @param context 实例
* @param appErrorsDisplay 应用异常信息显示
*/
fun start(context: Context, appErrorsDisplay: AppErrorsDisplayBean) =
context.navigate<AppErrorsDisplayActivity>(isOutSide = true) { putExtra(EXTRA_APP_ERRORS_DISPLAY, appErrorsDisplay) }
}
override fun onCreate() {
instance?.finish()
instance = this
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>(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
binding.reopenAppItem.isVisible = appErrorsDisplay.isShowReopenButton
binding.processNameText.text = LocaleString.crashProcess(appErrorsDisplay.processName)
binding.appInfoItem.setOnClickListener {
cancel()
openSelfSetting(appErrorsDisplay.packageName)
}
binding.closeAppItem.setOnClickListener { cancel() }
binding.reopenAppItem.setOnClickListener {
FrameworkTool.openAppUsedFramework(context, appErrorsDisplay.packageName, appErrorsDisplay.userId)
cancel()
}
binding.errorDetailItem.setOnClickListener {
FrameworkTool.fetchAppErrorInfoData(context, appErrorsDisplay.pid) { appErrorsInfo ->
AppErrorsDetailActivity.start(context, appErrorsInfo)
cancel()
}
}
binding.mutedIfUnlockItem.setOnClickListener {
FrameworkTool.mutedErrorsIfUnlock(context, appErrorsDisplay.packageName) {
toast(LocaleString.muteIfUnlockTip(appErrorsDisplay.appName))
cancel()
}
}
binding.mutedIfRestartItem.setOnClickListener {
FrameworkTool.mutedErrorsIfRestart(context, appErrorsDisplay.packageName) {
toast(LocaleString.muteIfRestartTip(appErrorsDisplay.appName))
cancel()
}
}
onCancel { finish() }
}
}
override fun onDestroy() {
super.onDestroy()
instance = null
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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/6/3.
*/
package com.fankes.apperrorstracking.ui.activity.errors
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.bean.MutedErrorsAppBean
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.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
class AppErrorsMutedActivity : BaseActivity<ActivityAppErrorsMutedBinding>() {
/** 回调适配器改变 */
private var onChanged: (() -> Unit)? = null
/** 全部的已忽略异常的 APP 信息 */
private val listData = ArrayList<MutedErrorsAppBean>()
override fun onCreate() {
binding.titleBackIcon.setOnClickListener { onBackPressed() }
binding.unmuteAllIcon.setOnClickListener {
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureUnmuteAll
confirmButton { FrameworkTool.unmuteAllErrorsApps(context) { refreshData() } }
cancelButton()
}
}
/** 设置列表元素和 Adapter */
binding.listView.bindAdapter {
onBindDatas { listData }
onBindViews<AdapterAppErrorsMutedBinding> { binding, position ->
listData[position].also { bean ->
binding.appIcon.setImageDrawable(appIconOf(bean.packageName))
binding.appNameText.text = appNameOf(bean.packageName).ifBlank { bean.packageName }
binding.muteTypeText.text = when (bean.type) {
MutedErrorsAppBean.MuteType.UNTIL_UNLOCKS -> LocaleString.muteIfUnlock
MutedErrorsAppBean.MuteType.UNTIL_REBOOTS -> LocaleString.muteIfRestart
}
binding.unmuteButton.setOnClickListener { FrameworkTool.unmuteErrorsApp(context, bean) { refreshData() } }
}
}
}.apply { onChanged = { notifyDataSetChanged() } }
}
/** 更新列表数据 */
private fun refreshData() {
FrameworkTool.fetchMutedErrorsAppsData(context = this) {
listData.clear()
it.takeIf { e -> e.isNotEmpty() }?.forEach { e -> listData.add(e) }
onChanged?.invoke()
binding.unmuteAllIcon.isVisible = listData.isNotEmpty()
binding.listView.isVisible = listData.isNotEmpty()
binding.listNoDataView.isVisible = listData.isEmpty()
}
}
override fun onResume() {
super.onResume()
/** 执行更新 */
refreshData()
}
}

View File

@@ -0,0 +1,240 @@
/*
* 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/5/11.
*/
@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "SetTextI18n")
package com.fankes.apperrorstracking.ui.activity.errors
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.view.ContextMenu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView.AdapterContextMenuInfo
import androidx.core.view.isVisible
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
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.fankes.apperrorstracking.utils.tool.ZipFileTool
import com.fankes.apperrorstracking.wrapper.BuildConfigWrapper
import java.io.File
import java.io.FileInputStream
class AppErrorsRecordActivity : BaseActivity<ActivityAppErrorsRecordBinding>() {
companion object {
/** 请求保存文件回调标识 */
private const val WRITE_REQUEST_CODE = 0
/**
* 获取 [Intent]
* @return [Intent]
*/
fun intent() = Intent().apply { component = ComponentName(BuildConfigWrapper.APPLICATION_ID, AppErrorsRecordActivity::class.java.name) }
}
/** 当前导出文件的路径 */
private var outPutFilePath = ""
/** 回调适配器改变 */
private var onChanged: (() -> Unit)? = null
/** 全部的 APP 异常信息 */
private val listData = ArrayList<AppErrorsInfoBean>()
override fun onCreate() {
binding.titleBackIcon.setOnClickListener { onBackPressed() }
binding.appErrorSisIcon.setOnClickListener {
showDialog {
title = LocaleString.notice
progressContent = LocaleString.generatingStatistics
noCancelable()
FrameworkTool.fetchAppListData(context, AppFiltersBean(type = AppFiltersType.ALL)) {
newThread {
val errorsApps = listData.groupBy { it.packageName }
.map { it.key to it.value.size }
.sortedByDescending { it.second }
.takeIf { it.isNotEmpty() }
val mostAppPackageName = errorsApps?.get(0)?.first ?: ""
val mostErrorsType = listData.groupBy { it.exceptionClassName }
.map { it.key to it.value.size }
.sortedByDescending { it.second }
.takeIf { it.isNotEmpty() }?.get(0)?.first?.simpleThwName() ?: ""
val pptCount = (((errorsApps?.size?.toFloat() ?: 0f) * 100f) / it.size.toFloat()).decimal()
runOnUiThread {
cancel()
showDialog<DiaAppErrorsStatisticsBinding> {
title = LocaleString.appErrorsStatistics
binding.totalErrorsUnitText.text = LocaleString.totalErrorsUnit(listData.size)
binding.totalAppsUnitText.text = LocaleString.totalAppsUnit(it.size)
binding.mostErrorsAppIcon.setImageDrawable(appIconOf(mostAppPackageName))
binding.mostErrorsAppText.text = appNameOf(mostAppPackageName).ifBlank { mostAppPackageName }
binding.mostErrorsTypeText.text = mostErrorsType
binding.totalPptOfErrorsText.text = "$pptCount%"
confirmButton(LocaleString.gotIt)
}
}
}
}
}
}
binding.clearAllIcon.setOnClickListener {
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureClearErrors
confirmButton {
FrameworkTool.clearAppErrorsInfoData(context) {
refreshData()
toast(LocaleString.allErrorsClearSuccess)
}
}
cancelButton()
}
}
binding.exportAllIcon.setOnClickListener {
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureExportAllErrors
confirmButton { exportAll() }
cancelButton()
}
}
/** 设置列表元素和 Adapter */
binding.listView.apply {
bindAdapter {
onBindDatas { listData }
onBindViews<AdapterAppErrorsRecordBinding> { binding, position ->
listData[position].also { bean ->
binding.appIcon.setImageDrawable(appIconOf(bean.packageName))
binding.appNameText.text = appNameOf(bean.packageName).ifBlank { 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()
binding.errorMsgText.text = bean.exceptionMessage
}
}
}.apply { onChanged = { notifyDataSetChanged() } }
registerForContextMenu(this)
setOnItemClickListener { _, _, p, _ -> AppErrorsDetailActivity.start(context, listData[p]) }
}
}
/** 更新列表数据 */
private fun refreshData() {
FrameworkTool.fetchAppErrorsInfoData(context = this) {
binding.titleCountText.text = LocaleString.recordCount(it.size)
binding.listProgressView.isVisible = false
binding.appErrorSisIcon.isVisible = it.size >= 5
binding.clearAllIcon.isVisible = it.isNotEmpty()
binding.exportAllIcon.isVisible = it.isNotEmpty()
binding.listView.isVisible = it.isNotEmpty()
binding.listNoDataView.isVisible = it.isEmpty()
listData.clear()
it.takeIf { e -> e.isNotEmpty() }?.forEach { e -> listData.add(e) }
onChanged?.invoke()
}
}
/** 打包导出全部 */
private fun exportAll() {
clearAllExportTemp()
("${cacheDir.absolutePath}/temp").also { path ->
File(path).mkdirs()
listData.takeIf { it.isNotEmpty() }?.forEach {
File("$path/${it.packageName}_${it.utcTime}.log").writeText(it.stackOutputFileContent)
}
outPutFilePath = "${cacheDir.absolutePath}/temp_${System.currentTimeMillis()}.zip"
ZipFileTool.zipMultiFile(path, outPutFilePath)
runCatching {
startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/application"
putExtra(Intent.EXTRA_TITLE, "app_errors_info_${System.currentTimeMillis().toUtcTime()}.zip")
}, WRITE_REQUEST_CODE)
}.onFailure { toast(msg = "Start Android SAF failed") }
}
}
/** 清空导出的临时文件 */
private fun clearAllExportTemp() {
cacheDir.deleteRecursively()
cacheDir.mkdirs()
}
/**
* 获取异常的精简名称
* @return [String]
*/
private fun String.simpleThwName() =
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)
super.onCreateContextMenu(menu, v, menuInfo)
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (item.menuInfo is AdapterContextMenuInfo)
(item.menuInfo as? AdapterContextMenuInfo?)?.also {
when (item.itemId) {
R.id.aerrors_view_detail -> AppErrorsDetailActivity.start(context = this, listData[it.position])
R.id.aerrors_app_info -> openSelfSetting(listData[it.position].packageName)
R.id.aerrors_remove_record ->
showDialog {
title = LocaleString.notice
msg = LocaleString.areYouSureRemoveRecord
confirmButton { FrameworkTool.removeAppErrorsInfoData(context, listData[it.position]) { refreshData() } }
cancelButton()
}
}
}
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(FileInputStream(outPutFilePath).readBytes()) }?.close()
clearAllExportTemp()
toast(LocaleString.exportAllErrorsSuccess)
} ?: toast(LocaleString.exportAllErrorsFail)
}.onFailure { toast(LocaleString.exportAllErrorsFail) }
}
override fun onResume() {
super.onResume()
/** 执行更新 */
refreshData()
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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/6/4.
*/
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.locale.LocaleString
import com.fankes.apperrorstracking.ui.activity.base.BaseActivity
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
class ConfigureActivity : BaseActivity<ActivityConfigBinding>() {
/** 过滤条件 */
private var appFilters = AppFiltersBean()
/** 回调适配器改变 */
private var onChanged: (() -> Unit)? = null
/** 全部的 APP 信息 */
private val listData = ArrayList<AppInfoBean>()
override fun onCreate() {
binding.titleBackIcon.setOnClickListener { finish() }
binding.globalIcon.setOnClickListener {
showAppConfigDialog(LocaleString.globalConfig, isShowGlobalConfig = false) { type ->
AppErrorsConfigData.putAppShowingType(type)
onChanged?.invoke()
}
}
binding.batchIcon.setOnClickListener {
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()
}
}
}
binding.filterIcon.setOnClickListener {
showDialog<DiaAppsFilterBinding> {
title = LocaleString.filterByCondition
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()
if (appFilters.name.isNotBlank()) {
setText(appFilters.name)
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 {
setAppFiltersType()
appFilters.name = binding.appFiltersEdit.text.toString().trim()
refreshData()
}
cancelButton()
if (appFilters.name.isNotBlank())
neutralButton(LocaleString.clearFilters) {
setAppFiltersType()
appFilters.name = ""
refreshData()
}
}
}
binding.listView.apply {
bindAdapter {
onBindDatas { listData }
onBindViews<AdapterAppInfoBinding> { binding, position ->
listData[position].also { bean ->
binding.appIcon.setImageDrawable(bean.icon)
binding.appNameText.text = bean.name
binding.configTypeText.text = when {
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"
}
}
}
}.apply { onChanged = { notifyDataSetChanged() } }
setOnItemClickListener { _, _, p, _ ->
listData[p].also { bean ->
showAppConfigDialog(bean.name, bean.packageName) { type ->
AppErrorsConfigData.putAppShowingType(type, bean.packageName)
onChanged?.invoke()
}
}
}
}
/** 模块未完全激活将显示警告 */
if (MainActivity.isModuleValied.not())
showDialog {
title = LocaleString.notice
msg = LocaleString.moduleNotFullyActivatedTip
confirmButton { FrameworkTool.restartSystem(context) }
cancelButton()
noCancelable()
}
/** 开始刷新数据 */
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) {
/** 设置一个临时变量用于更新列表数据 */
val tempsData = ArrayList<AppInfoBean>()
newThread {
runCatching {
it.takeIf { e -> e.isNotEmpty() }?.forEach { e ->
tempsData.add(e)
e.icon = appIconOf(e.packageName)
}
}
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)
} else tempsData.clear()
}
}
}
}

View File

@@ -0,0 +1,184 @@
/*
* 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/5/14.
*/
@file:Suppress("SetTextI18n")
package com.fankes.apperrorstracking.ui.activity.main
import android.os.Build
import androidx.core.view.isVisible
import com.fankes.apperrorstracking.R
import com.fankes.apperrorstracking.const.ModuleVersion
import com.fankes.apperrorstracking.data.ConfigData
import com.fankes.apperrorstracking.data.factory.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.hideOrShowLauncherIcon
import com.fankes.apperrorstracking.utils.factory.isLauncherIconShowing
import com.fankes.apperrorstracking.utils.factory.isSystemLanguageSimplifiedChinese
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.tool.AppAnalyticsTool
import com.fankes.apperrorstracking.utils.tool.AppAnalyticsTool.bindAppAnalytics
import com.fankes.apperrorstracking.utils.tool.FrameworkTool
import com.fankes.apperrorstracking.utils.tool.GithubReleaseTool
import com.fankes.projectpromote.ProjectPromote
import com.highcapable.yukihookapi.YukiHookAPI
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, ModuleVersion.NAME) { version, function ->
binding.mainTextReleaseVersion.apply {
text = LocaleString.clickToUpdate(version)
isVisible = true
setOnClickListener { function() }
}
}
/** 推广、恰饭 */
if (YukiHookAPI.Status.isXposedModuleActive) ProjectPromote.show(activity = this, ModuleVersion.toString())
/** 显示开发者提示 */
if (ConfigData.isShowDeveloperNotice)
showDialog {
title = LocaleString.developerNotice
msg = LocaleString.developerNoticeTip
confirmButton(LocaleString.gotIt) { ConfigData.isShowDeveloperNotice = false }
noCancelable()
}
/** 设置 CI 自动构建标识 */
if (ModuleVersion.isCiMode)
binding.mainTextReleaseVersion.apply {
text = "CI ${ModuleVersion.GITHUB_COMMIT_ID}"
isVisible = true
setOnClickListener {
showDialog {
title = LocaleString.ciNoticeDialogTitle
msg = LocaleString.ciNoticeDialogContent(ModuleVersion.GITHUB_COMMIT_ID)
confirmButton(LocaleString.gotIt)
noCancelable()
}
}
}
binding.mainTextVersion.text = LocaleString.moduleVersion(ModuleVersion.NAME)
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) {
onInitialize { binding.mgrAppsConfigsTemplateButton.isVisible = it }
onChanged { reinitialize() }
}
binding.enableMaterial3AppErrorsDialogSwitch.bind(ConfigData.ENABLE_MATERIAL3_STYLE_APP_ERRORS_DIALOG)
/** 设置匿名统计 */
binding.appAnalyticsConfigItem.isVisible = AppAnalyticsTool.isAvailable
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") }
/** 恰饭! */
binding.paymentFollowingZhCnItem.isVisible = isSystemLanguageSimplifiedChinese
binding.linkWithFollowMe.setOnClickListener {
openBrowser(url = "https://www.coolapk.com/u/876977", packageName = "com.coolapk.market")
}
/** 设置桌面图标显示隐藏 */
binding.hideIconInLauncherSwitch.isChecked = isLauncherIconShowing.not()
binding.hideIconInLauncherSwitch.setOnCheckedChangeListener { btn, b ->
if (btn.isPressed.not()) return@setOnCheckedChangeListener
hideOrShowLauncherIcon(b)
}
}
/** 刷新模块状态 */
private fun refreshModuleStatus() {
binding.mainLinStatus.setBackgroundResource(
when {
YukiHookAPI.Status.isXposedModuleActive && isModuleValied.not() -> R.drawable.bg_yellow_round
YukiHookAPI.Status.isXposedModuleActive -> R.drawable.bg_green_round
else -> R.drawable.bg_dark_round
}
)
binding.mainImgStatus.setImageResource(
when {
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.mainTextApiWay.isVisible = YukiHookAPI.Status.isXposedModuleActive
binding.mainTextApiWay.text = "Activated by ${YukiHookAPI.Status.Executor.name} API ${YukiHookAPI.Status.Executor.apiLevel}"
}
/**
* 当模块激活后才能执行相应功能
* @param callback 激活后回调
*/
private inline fun whenActivated(callback: () -> Unit) {
if (YukiHookAPI.Status.isXposedModuleActive) callback() else toast(LocaleString.moduleNotActivated)
}
override fun onResume() {
super.onResume()
/** 刷新模块状态 */
refreshModuleStatus()
/** 检查模块激活状态 */
FrameworkTool.checkingActivated(context = this) { isValied ->
isModuleValied = isValied
refreshModuleStatus()
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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/6/1.
*/
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.factory.dp
import top.defaults.drawabletoolbox.DrawableBuilder
class ItemLinearLayout(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
init {
gravity = Gravity.CENTER or Gravity.START
background = DrawableBuilder()
.rounded()
.cornerRadius(15.dp(context))
.ripple()
.rippleColor(0xFFAAAAAA.toInt())
.build()
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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/5/14.
*/
@file:Suppress("SameParameterValue")
package com.fankes.apperrorstracking.ui.widget
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.text.TextUtils
import android.util.AttributeSet
import androidx.appcompat.widget.SwitchCompat
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) {
private fun toColors(selected: Int, pressed: Int, normal: Int): ColorStateList {
val colors = intArrayOf(selected, pressed, normal)
val states = arrayOfNulls<IntArray>(3)
states[0] = intArrayOf(android.R.attr.state_checked)
states[1] = intArrayOf(android.R.attr.state_pressed)
states[2] = intArrayOf()
return ColorStateList(states, colors)
}
private val thumbColor get() = if (context.isSystemInDarkMode) 0xFF7C7C7C else 0xFFCCCCCC
init {
trackDrawable = DrawableBuilder()
.rectangle()
.rounded()
.solidColor(0xFF656565.toInt())
.height(20.dp(context))
.cornerRadius(15.dp(context))
.build()
thumbDrawable = DrawableBuilder()
.rectangle()
.rounded()
.solidColor(Color.WHITE)
.size(20.dp(context), 20.dp(context))
.cornerRadius(20.dp(context))
.strokeWidth(8.dp(context))
.strokeColor(Color.TRANSPARENT)
.build()
trackTintList = toColors(
0xFF656565.toInt(),
thumbColor.toInt(),
thumbColor.toInt()
)
isSingleLine = true
ellipsize = TextUtils.TruncateAt.END
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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/6/3.
*/
package com.fankes.apperrorstracking.utils.factory
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import androidx.viewbinding.ViewBinding
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.type.android.LayoutInflaterClass
/**
* 绑定 [BaseAdapter] 到 [ListView]
* @param initiate 方法体
* @return [BaseAdapter]
*/
inline fun ListView.bindAdapter(initiate: BaseAdapterCreater.() -> Unit) =
BaseAdapterCreater(context).apply(initiate).baseAdapter?.apply { adapter = this } ?: error("BaseAdapter not binded")
/**
* [BaseAdapter] 创建类
* @param context 实例
*/
class BaseAdapterCreater(val context: Context) {
/** 当前 [List] 回调 */
var listDataCallback: (() -> List<*>)? = null
/** 当前 [BaseAdapter] */
var baseAdapter: BaseAdapter? = null
/**
* 绑定 [List] 到 [ListView]
* @param result 回调数据
*/
fun onBindDatas(result: (() -> List<*>)) {
listDataCallback = result
}
/**
* 绑定 [BaseAdapter] 到 [ListView]
* @param bindViews 回调 - ([VB] 每项,[Int] 下标)
*/
inline fun <reified VB : ViewBinding> onBindViews(crossinline bindViews: (binding: VB, position: Int) -> Unit) {
baseAdapter = object : BaseAdapter() {
override fun getCount() = listDataCallback?.let { it() }?.size ?: 0
override fun getItem(position: Int) = listDataCallback?.let { it() }?.get(position)
override fun getItemId(position: Int) = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var holderView = convertView
val holder: VB
if (convertView == null) {
holder = VB::class.java.method {
name = "inflate"
param(LayoutInflaterClass)
}.get().invoke<VB>(LayoutInflater.from(context)) ?: error("ViewHolder binding failed")
holderView = holder.root.apply { tag = holder }
} else holder = convertView.tag as VB
bindViews(holder, position)
return holderView ?: error("ViewHolder binding failed")
}
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* 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/5/12.
*/
@file:Suppress("unused", "OPT_IN_USAGE", "EXPERIMENTAL_API_USAGE")
package com.fankes.apperrorstracking.utils.factory
import android.app.Dialog
import android.content.Context
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.viewbinding.ViewBinding
import com.fankes.apperrorstracking.locale.LocaleString
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.CircularProgressIndicator
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_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(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 isDisableMaterial3: Boolean = false,
private val bindingClass: Class<*>? = null
) {
/** 实例对象 */
private var instance: AlertDialog.Builder? = null
/** 对话框取消监听 */
private var onCancel: (() -> Unit)? = null
/** 对话框实例 */
private var dialogInstance: Dialog? = null
/** 自定义布局 */
private var customLayoutView: View? = null
/**
* 获取 [DialogBuilder] 绑定布局对象
* @return [VB]
*/
val binding by lazy {
bindingClass?.method {
name = "inflate"
param(LayoutInflaterClass)
}?.get()?.invoke<VB>(LayoutInflater.from(context))?.apply {
customLayoutView = root
} ?: error("This dialog maybe not a custom view dialog")
}
init {
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() {
instance?.setCancelable(false)
}
/** 设置对话框标题 */
var title
get() = ""
set(value) {
instance?.setTitle(value)
}
/** 设置对话框消息内容 */
var msg
get() = ""
set(value) {
instance?.setMessage(value)
}
/** 设置进度条对话框消息内容 */
var progressContent
get() = ""
set(value) {
if (customLayoutView == null)
customLayoutView = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER or Gravity.START
addView(CircularProgressIndicator(context).apply {
isIndeterminate = true
trackCornerRadius = 10.dp(context)
})
addView(View(context).apply { layoutParams = ViewGroup.LayoutParams(20.dp(context), 5) })
addView(TextView(context).apply {
tag = "progressContent"
text = value
})
setPadding(20.dp(context), 20.dp(context), 20.dp(context), 20.dp(context))
}
else customLayoutView?.findViewWithTag<TextView>("progressContent")?.text = value
}
/**
* 设置对话框确定按钮
* @param text 按钮文本内容
* @param callback 点击事件
*/
fun confirmButton(text: String = LocaleString.confirm, callback: () -> Unit = {}) {
instance?.setPositiveButton(text) { _, _ -> callback() }
}
/**
* 设置对话框取消按钮
* @param text 按钮文本内容
* @param callback 点击事件
*/
fun cancelButton(text: String = LocaleString.cancel, callback: () -> Unit = {}) {
instance?.setNegativeButton(text) { _, _ -> callback() }
}
/**
* 设置对话框第三个按钮
* @param text 按钮文本内容
* @param callback 点击事件
*/
fun neutralButton(text: String = LocaleString.more, callback: () -> Unit = {}) {
instance?.setNeutralButton(text) { _, _ -> callback() }
}
/**
* 当对话框关闭时
* @param callback 回调
*/
fun onCancel(callback: () -> Unit) {
onCancel = callback
}
/** 取消对话框 */
fun cancel() = dialogInstance?.cancel()
/** 显示对话框 */
@CauseProblemsApi
fun show() {
/** 若当前自定义 View 的对话框没有调用 [binding] 将会对其手动调用一次以确保显示布局 */
if (bindingClass != null) binding
runCatching {
instance?.create()?.apply {
customLayoutView?.let { setView(it) }
dialogInstance = this
setOnCancelListener { onCancel?.invoke() }
}?.show()
}
}
}

View File

@@ -0,0 +1,438 @@
/*
* 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/5/7.
*/
@file:Suppress("unused", "NotificationPermission")
package com.fankes.apperrorstracking.utils.factory
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
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.R
import com.fankes.apperrorstracking.locale.LocaleString
import com.fankes.apperrorstracking.wrapper.BuildConfigWrapper
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.Date
import java.util.Locale
/**
* 当前系统环境是否为简体中文
* @return [Boolean]
*/
val isSystemLanguageSimplifiedChinese
get(): Boolean {
val locale = Locale.getDefault()
return locale.language == "zh" && locale.country == "CN"
}
/**
* 系统深色模式是否开启
* @return [Boolean] 是否开启
*/
val Context.isSystemInDarkMode get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
/**
* 系统深色模式是否没开启
* @return [Boolean] 是否开启
*/
inline val Context.isNotSystemInDarkMode get() = !isSystemInDarkMode
/**
* dp 转换为 pxInt
* @param context 使用的实例
* @return [Int]
*/
fun Number.dp(context: Context) = dpFloat(context).toInt()
/**
* dp 转换为 pxFloat
* @param context 使用的实例
* @return [Float]
*/
fun Number.dpFloat(context: Context) = toFloat() * context.resources.displayMetrics.density
/**
* 获取 [Drawable]
* @param resId 属性资源 ID
* @return [Drawable]
*/
fun Resources.drawableOf(@DrawableRes resId: Int) = ResourcesCompat.getDrawable(this, resId, null) ?: error("Invalid resources")
/**
* 获取颜色
* @param resId 属性资源 ID
* @return [Int]
*/
fun Resources.colorOf(@ColorRes resId: Int) = ResourcesCompat.getColor(this, resId, null)
/**
* 得到 APP 安装包信息 (兼容)
* @param packageName APP 包名
* @param flag [PackageInfoFlags]
* @return [PackageInfo] or null
*/
private fun Context.getPackageInfoCompat(packageName: String, flag: Number = 0) = runCatching {
@Suppress("DEPRECATION", "KotlinRedundantDiagnosticSuppress")
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", "KotlinRedundantDiagnosticSuppress")
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.appNameOf(packageName: String = getPackageName()) =
getPackageInfoCompat(packageName)?.applicationInfo?.loadLabel(packageManager)?.toString() ?: ""
/**
* 得到 APP 版本信息与版本号
* @param packageName APP 包名 - 默认为当前 APP
* @return [String] 无法获取时返回 ""
*/
fun Context.appVersionBrandOf(packageName: String = getPackageName()) =
if (appVersionNameOf(packageName).isNotBlank()) "${appVersionNameOf(packageName)}(${appVersionCodeOf(packageName)})" else ""
/**
* 得到 APP 版本名称
* @param packageName APP 包名 - 默认为当前 APP
* @return [String] 无法获取时返回 ""
*/
fun Context.appVersionNameOf(packageName: String = getPackageName()) = getPackageInfoCompat(packageName)?.versionName ?: ""
/**
* 得到 APP 版本号
* @param packageName APP 包名 - 默认为当前 APP
* @return [Long] 无法获取时返回 -1
*/
fun Context.appVersionCodeOf(packageName: String = getPackageName()) = getPackageInfoCompat(packageName)?.versionCodeCompat ?: -1L
/**
* 获取 APP CPU ABI 名称
* @param packageName APP 包名 - 默认为当前 APP
* @return [String] 无法获取时返回 ""
*/
fun Context.appCpuAbiOf(packageName: String = getPackageName()) = runCatching {
ApplicationInfoClass.field { name = "primaryCpuAbi" }.get(getPackageInfoCompat(packageName)?.applicationInfo).string()
}.getOrNull() ?: ""
/**
* 得到 APP 图标
* @param packageName APP 包名 - 默认为当前 APP
* @return [Drawable] 无发获取时返回 [R.drawable.ic_android]
*/
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>
/**
* 计算与当前时间戳相差的友好时间
* @param now 刚刚
* @param second 秒前
* @param minute 分钟前
* @param hour 小时前
* @param day 天前
* @param month 月前
* @param year 年前
* @return [String] 友好时间
*/
fun Long.difference(now: String, second: String, minute: String, hour: String, day: String, month: String, year: String) =
((System.currentTimeMillis() - this) / 1000).toInt().let { diff ->
when (diff) {
in 0..10 -> now
in 11..20 -> "10 $second"
in 21..30 -> "20 $second"
in 31..40 -> "30 $second"
in 41..50 -> "40 $second"
in 51..59 -> "50 $second"
in 60..3599 -> "${(diff / 60).coerceAtLeast(1)} $minute"
in 3600..86399 -> "${diff / 3600} $hour"
in 86400..2591999 -> "${diff / 86400} $day"
in 2592000..31103999 -> "${diff / 2592000} $month"
else -> "${diff / 31104000} $year"
}
}
/**
* 保留小数
* @param count 要保留的位数 - 默认 2 位 - 最多 7 位
* @return [String] 得到的字符串数字 - 格式化失败返回原始数字的字符串
*/
fun Number.decimal(count: Int = 2) = runCatching {
DecimalFormat(
when (count) {
0 -> "0"
1 -> "0.0"
2 -> "0.00"
3 -> "0.000"
4 -> "0.0000"
5 -> "0.00000"
6 -> "0.000000"
7 -> "0.0000000"
else -> "0.0"
}
).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 提示内容
*/
fun Context.toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
/**
* 弹出 [Snackbar]
* @param msg 提示内容
* @param actionText 按钮文本 - 不写默认取消按钮
* @param callback 按钮事件回调
*/
fun Context.snake(msg: String, actionText: String = "", callback: () -> Unit = {}) =
Snackbar.make((this as Activity).findViewById(android.R.id.content), msg, Snackbar.LENGTH_LONG).apply {
if (actionText.isBlank()) return@apply
setActionTextColor(if (isSystemInDarkMode) Color.BLACK else Color.WHITE)
setAction(actionText) { callback() }
}.show()
/**
* 推送通知
* @param channelId 渠道 Id
* @param channelName 渠道名称
* @param title 标题
* @param content 内容
* @param icon 图标
* @param color 颜色
* @param intent [Intent]
*/
fun Context.pushNotify(channelId: String, channelName: String, title: String, content: String, icon: IconCompat, color: Int, intent: Intent) {
getSystemService<NotificationManager>()?.apply {
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)
setContentTitle(title)
setContentText(content)
setSmallIcon(icon)
setContentIntent(PendingIntent.getActivity(this@pushNotify, (0..999).random(), intent, PendingIntent.FLAG_IMMUTABLE))
setDefaults(NotificationCompat.DEFAULT_ALL)
}.build())
}
}
/**
* 跳转到指定页面
*
* [T] 为指定的 [Activity]
* @param isOutSide 是否从外部启动
* @param initiate [Intent] 方法体
*/
inline fun <reified T : Activity> Context.navigate(isOutSide: Boolean = false, initiate: Intent.() -> Unit = {}) = runCatching {
startActivity((if (isOutSide) Intent() else Intent(if (this is Service) applicationContext else this, T::class.java)).apply {
flags = if (this@navigate !is Activity) Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
else Intent.FLAG_ACTIVITY_NEW_TASK
if (isOutSide) component = ComponentName(BuildConfigWrapper.APPLICATION_ID, T::class.java.name)
initiate(this)
})
}.onFailure { toast(msg = "Start ${T::class.java.name} failed") }
/**
* 复制到剪贴板
* @param content 要复制的文本
*/
fun Context.copyToClipboard(content: String) = runCatching {
(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).apply {
setPrimaryClip(ClipData.newPlainText(null, content))
(primaryClip?.getItemAt(0)?.text ?: "").also {
if (it != content) toast(LocaleString.copyFail) else toast(LocaleString.copied)
}
}
}
/**
* 跳转 APP 自身设置界面
* @param packageName 包名
*/
fun Context.openSelfSetting(packageName: String = this.packageName) = runCatching {
startActivity(Intent().apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", packageName, null)
})
}.onFailure { toast(msg = "Cannot open \"$packageName\"") }
/**
* 启动系统浏览器
* @param url 网址
* @param packageName 指定包名 - 可不填
*/
fun Context.openBrowser(url: String, packageName: String = "") = runCatching {
startActivity(Intent().apply {
if (packageName.isNotBlank()) setPackage(packageName)
action = Intent.ACTION_VIEW
data = Uri.parse(url)
/** 防止顶栈一样重叠在自己的 APP 中 */
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.onFailure {
if (packageName.isNotBlank()) snake(msg = "Cannot start \"$packageName\"")
else snake(msg = "Start system browser failed")
}
/**
* 当前 APP 是否可被启动
* @param packageName 包名
*/
fun Context.isAppCanOpened(packageName: String = this.packageName) =
runCatching { packageManager?.getLaunchIntentForPackage(packageName) != null }.getOrNull() ?: false
/**
* 启动指定 APP
* @param packageName APP 包名 - 默认为当前 APP
* @param userId APP 用户 ID - 默认 0
*/
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 权限
* @return [Boolean]
*/
val isRootAccess get() = runCatching {
@Suppress("DEPRECATION")
Shell.rootAccess()
}.getOrNull() ?: false
/**
* 执行命令
* @param cmd 命令
* @param isSu 是否使用 Root 权限执行 - 默认:是
* @return [String] 执行结果
*/
fun execShell(cmd: String, isSu: Boolean = true) = runCatching {
@Suppress("DEPRECATION")
(if (isSu) Shell.su(cmd) else Shell.sh(cmd)).exec().out.let {
if (it.isNotEmpty()) it[0].trim() else ""
}
}.getOrNull() ?: ""
/**
* 隐藏或显示启动器图标
*
* - 你可能需要 LSPosed 的最新版本以开启高版本系统中隐藏 APP 桌面图标功能
* @param isShow 是否显示
*/
fun Context.hideOrShowLauncherIcon(isShow: Boolean) {
packageManager?.setComponentEnabledSetting(
ComponentName(packageName, "${BuildConfigWrapper.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, "${BuildConfigWrapper.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

@@ -0,0 +1,46 @@
/*
* 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 java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* 创建当前线程池服务
* @return [ExecutorService]
*/
private val currentThreadPool get() = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
/**
* 创建并启动新的临时线程池
*
* 等待 [block] 执行完成并自动释放
* @param block 方法块
*/
fun newThread(block: () -> Unit) {
currentThreadPool.apply {
execute {
block()
shutdown()
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.generated.ModuleAppProperties
import com.highcapable.yukihookapi.hook.factory.prefs
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 = ModuleAppProperties.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?.prefs()?.get(ENABLE_APP_CENTER_ANALYTICS) ?: true
set(value) {
instance?.prefs()?.edit { put(ENABLE_APP_CENTER_ANALYTICS, value) }
}
/** 是否可用 */
val isAvailable = APP_CENTER_SECRET.isNotBlank()
/** 绑定到 [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 && isAvailable)
AppCenter.start(instance, APP_CENTER_SECRET, Analytics::class.java, Crashes::class.java)
}
}

View File

@@ -0,0 +1,390 @@
/*
* 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/5/12.
*/
package com.fankes.apperrorstracking.utils.tool
import android.content.Context
import com.fankes.apperrorstracking.bean.AppErrorsInfoBean
import com.fankes.apperrorstracking.bean.AppFiltersBean
import com.fankes.apperrorstracking.bean.AppInfoBean
import com.fankes.apperrorstracking.bean.MutedErrorsAppBean
import com.fankes.apperrorstracking.const.PackageName
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.highcapable.yukihookapi.hook.factory.dataChannel
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.highcapable.yukihookapi.hook.xposed.channel.data.ChannelData
/**
* 系统框架控制工具
*/
object FrameworkTool {
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"
private const val CALL_UNMUTE_ALL_ERRORS_APPS_DATA = "call_unmute_all_errors_apps_data"
private const val CALL_APP_ERRORS_DATA_REMOVE_RESULT = "call_app_errors_data_remove_result"
private const val CALL_APP_ERRORS_DATA_CLEAR_RESULT = "call_app_errors_data_clear_result"
private const val CALL_MUTED_ERRORS_IF_UNLOCK_RESULT = "call_muted_errors_if_unlock_result"
private const val CALL_MUTED_ERRORS_IF_RESTART_RESULT = "call_muted_errors_if_restart_result"
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_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")
private val CALL_MUTED_ERRORS_IF_UNLOCK = ChannelData<String>("call_muted_errors_if_unlock")
private val CALL_MUTED_ERRORS_IF_RESTART = ChannelData<String>("call_muted_errors_if_restart")
/**
* 宿主注册监听
*/
object Host {
/** [PackageParam] 实例 */
private var instance: PackageParam? = null
/**
* 注册监听
* @param instance 实例
* @param initiate 实例方法体
* @return [Host]
*/
fun with(instance: PackageParam, initiate: Host.() -> Unit) = apply { this.instance = instance }.apply(initiate)
/**
* 通知系统框架刷新存储的数据
* @param callback 回调
*/
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 异常信息数组
* @param result 回调数据
*/
fun onPushAppErrorsInfoData(result: () -> ArrayList<AppErrorsInfoBean>) {
instance?.dataChannel?.with { wait(CALL_APP_ERRORS_DATA_GET) { put(CALL_APP_ERRORS_DATA_GET_RESULT, result()) } }
}
/**
* 监听移除指定 APP 异常信息
* @param result 回调数据
*/
fun onRemoveAppErrorsInfoData(result: (AppErrorsInfoBean) -> Unit) {
instance?.dataChannel?.with {
wait(CALL_APP_ERRORS_DATA_REMOVE) {
result(it)
put(CALL_APP_ERRORS_DATA_REMOVE_RESULT)
}
}
}
/**
* 监听清空 APP 异常信息数组
* @param callback 回调
*/
fun onClearAppErrorsInfoData(callback: () -> Unit) {
instance?.dataChannel?.with {
wait(CALL_APP_ERRORS_DATA_CLEAR) {
callback()
put(CALL_APP_ERRORS_DATA_CLEAR_RESULT)
}
}
}
/**
* 监听忽略 APP 的错误直到设备重新解锁
* @param result 回调包名
*/
fun onMutedErrorsIfUnlock(result: (String) -> Unit) {
instance?.dataChannel?.with {
wait(CALL_MUTED_ERRORS_IF_UNLOCK) {
result(it)
put(CALL_MUTED_ERRORS_IF_UNLOCK_RESULT)
}
}
}
/**
* 监听忽略 APP 的错误直到设备重新启动
* @param result 回调包名
*/
fun onMutedErrorsIfRestart(result: (String) -> Unit) {
instance?.dataChannel?.with {
wait(CALL_MUTED_ERRORS_IF_RESTART) {
result(it)
put(CALL_MUTED_ERRORS_IF_RESTART_RESULT)
}
}
}
/**
* 监听发送已忽略异常的 APP 信息数组
* @param result 回调数据
*/
fun onPushMutedErrorsAppsData(result: () -> ArrayList<MutedErrorsAppBean>) {
instance?.dataChannel?.with { wait(CALL_MUTED_ERRORS_APP_DATA_GET) { put(CALL_MUTED_ERRORS_APP_DATA_GET_RESULT, result()) } }
}
/**
* 监听取消指定已忽略异常的 APP
* @param result 回调数据
*/
fun onUnmuteErrorsApp(result: (MutedErrorsAppBean) -> Unit) {
instance?.dataChannel?.with {
wait(CALL_UNMUTE_ERRORS_APP_DATA) {
result(it)
put(CALL_UNMUTE_ERRORS_APP_DATA_RESULT)
}
}
}
/**
* 监听取消全部已忽略异常的 APP
* @param callback 回调
*/
fun onUnmuteAllErrorsApps(callback: () -> Unit) {
instance?.dataChannel?.with {
wait(CALL_UNMUTE_ALL_ERRORS_APPS_DATA) {
callback()
put(CALL_UNMUTE_ALL_ERRORS_APPS_DATA_RESULT)
}
}
}
/**
* 监听发送已安装 APP 列表数组
* @param result 回调数据
*/
fun onPushAppListData(result: (AppFiltersBean) -> ArrayList<AppInfoBean>) {
instance?.dataChannel?.with { wait(CALL_APP_LIST_DATA_GET) { put(CALL_APP_LIST_DATA_GET_RESULT, result(it)) } }
}
}
/**
* 重启系统
* @param 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 showWhenAccessRootFail()
}
neutralButton(LocaleString.fastRestart) {
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(PackageName.SYSTEM_FRAMEWORK).checkingVersionEquals(result = result)
/**
* 通知系统框架刷新存储的数据
* @param context 实例
*/
fun refreshFrameworkPrefsData(context: Context) = context.dataChannel(PackageName.SYSTEM_FRAMEWORK).put(CALL_REFRESH_HOST_PREFS_DATA)
/**
* 使用系统框架打开 [packageName]
* @param context 实例
* @param packageName APP 包名
* @param userId APP 用户 ID
*/
fun openAppUsedFramework(context: Context, packageName: String, userId: Int) =
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).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(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_APP_ERROR_DATA_GET_RESULT) { result(it) }
put(CALL_APP_ERROR_DATA_GET, pid)
}
}
/**
* 获取 APP 异常信息数组
* @param context 实例
* @param result 回调数据
*/
fun fetchAppErrorsInfoData(context: Context, result: (ArrayList<AppErrorsInfoBean>) -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_APP_ERRORS_DATA_GET_RESULT) { result(it) }
put(CALL_APP_ERRORS_DATA_GET)
}
}
/**
* 移除指定 APP 异常信息
* @param context 实例
* @param appErrorsInfo 指定 APP 异常信息
* @param callback 成功后回调
*/
fun removeAppErrorsInfoData(context: Context, appErrorsInfo: AppErrorsInfoBean, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_APP_ERRORS_DATA_REMOVE_RESULT) { callback() }
put(CALL_APP_ERRORS_DATA_REMOVE, appErrorsInfo)
}
}
/**
* 清空 APP 异常信息数组
* @param context 实例
* @param callback 成功后回调
*/
fun clearAppErrorsInfoData(context: Context, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_APP_ERRORS_DATA_CLEAR_RESULT) { callback() }
put(CALL_APP_ERRORS_DATA_CLEAR)
}
}
/**
* 忽略 [packageName] 的错误直到设备重新解锁
* @param context 实例
* @param packageName APP 包名
* @param callback 成功后回调
*/
fun mutedErrorsIfUnlock(context: Context, packageName: String, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_MUTED_ERRORS_IF_UNLOCK_RESULT) { callback() }
put(CALL_MUTED_ERRORS_IF_UNLOCK, packageName)
}
}
/**
* 忽略 [packageName] 的错误直到设备重新启动
* @param context 实例
* @param packageName APP 包名
* @param callback 成功后回调
*/
fun mutedErrorsIfRestart(context: Context, packageName: String, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_MUTED_ERRORS_IF_RESTART_RESULT) { callback() }
put(CALL_MUTED_ERRORS_IF_RESTART, packageName)
}
}
/**
* 获取已忽略异常的 APP 信息数组
* @param context 实例
* @param result 回调数据
*/
fun fetchMutedErrorsAppsData(context: Context, result: (ArrayList<MutedErrorsAppBean>) -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_MUTED_ERRORS_APP_DATA_GET_RESULT) { result(it) }
put(CALL_MUTED_ERRORS_APP_DATA_GET)
}
}
/**
* 取消指定已忽略异常的 APP
* @param context 实例
* @param mutedErrorsApp 指定已忽略异常的 APP 信息
* @param callback 成功后回调
*/
fun unmuteErrorsApp(context: Context, mutedErrorsApp: MutedErrorsAppBean, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_UNMUTE_ERRORS_APP_DATA_RESULT) { callback() }
put(CALL_UNMUTE_ERRORS_APP_DATA, mutedErrorsApp)
}
}
/**
* 取消全部已忽略异常的 APP
* @param context 实例
* @param callback 成功后回调
*/
fun unmuteAllErrorsApps(context: Context, callback: () -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_UNMUTE_ALL_ERRORS_APPS_DATA_RESULT) { callback() }
put(CALL_UNMUTE_ALL_ERRORS_APPS_DATA)
}
}
/**
* 获取已安装 APP 列表数组
* @param context 实例
* @param appFilters 过滤条件
* @param result 回调数据
*/
fun fetchAppListData(context: Context, appFilters: AppFiltersBean, result: (ArrayList<AppInfoBean>) -> Unit) {
context.dataChannel(PackageName.SYSTEM_FRAMEWORK).with {
wait(CALL_APP_LIST_DATA_GET_RESULT) { result(it) }
put(CALL_APP_LIST_DATA_GET, appFilters)
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
import java.io.Serializable
import java.util.Locale
/**
* 获取 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

@@ -0,0 +1,152 @@
/*
* 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/5/13.
*/
@file:Suppress("unused")
package com.fankes.apperrorstracking.utils.tool
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
/**
* 处理压缩文件
*/
object ZipFileTool {
private const val BUFF_SIZE = 2048
/**
* 压缩整个文件夹中的所有文件 - 生成指定名称的 Zip 压缩包
* @param filePath 文件所在目录
* @param zipPath 压缩后 Zip 文件名称
* @param isDirFlag Zip 文件中第一层是否包含一级目录 - true 包含 false没有
*/
fun zipMultiFile(filePath: String, zipPath: String, isDirFlag: Boolean = false) {
runCatching {
val file = File(filePath)
val zipFile = File(zipPath)
val zipOut = ZipOutputStream(FileOutputStream(zipFile))
if (file.isDirectory) {
val files = file.listFiles() ?: emptyArray()
for (fileSec in files)
if (isDirFlag) recursionZip(zipOut, fileSec, file.name + File.separator)
else recursionZip(zipOut, fileSec, "")
}
zipOut.close()
}
}
/**
* 解压文件
* @param unZipPath 解压后的目录
* @param zipPath 压缩文件目录
*/
fun unZipFile(unZipPath: String, zipPath: String) {
runCatching {
unZipFileByInput(unZipPath, FileInputStream(zipPath))
}
}
/**
* 解压文件
* @param unZipPath 解压后的目录
* @param zips 压缩文件流
*/
private fun unZipFileByInput(unZipPath: String, zips: FileInputStream) {
val path = createSeparator(unZipPath)
var bos: BufferedOutputStream? = null
var zis: ZipInputStream? = null
try {
var filename: String
zis = ZipInputStream(BufferedInputStream(zips))
var ze: ZipEntry
val buffer = ByteArray(BUFF_SIZE)
var count: Int
while (zis.nextEntry.also { ze = it } != null) {
filename = ze.name
createSubFolders(filename, path)
if (ze.isDirectory) {
val fmd = File(path + filename)
fmd.mkdirs()
continue
}
bos = BufferedOutputStream(FileOutputStream(path + filename))
while (zis.read(buffer).also { count = it } != -1) bos.write(buffer, 0, count)
bos.flush()
bos.close()
}
} catch (_: IOException) {
} finally {
runCatching {
if (zis != null) {
zis.closeEntry()
zis.close()
}
bos?.close()
}
}
}
/** @base code */
private fun recursionZip(zipOut: ZipOutputStream, file: File, baseDir: String) {
if (file.isDirectory) {
val files = file.listFiles() ?: emptyArray()
for (fileSec in files) recursionZip(zipOut, fileSec, baseDir + file.name + File.separator)
} else {
val buf = ByteArray(1024)
val input: InputStream = FileInputStream(file)
zipOut.putNextEntry(ZipEntry(baseDir + file.name))
var len: Int
while (input.read(buf).also { len = it } != -1) {
zipOut.write(buf, 0, len)
}
input.close()
}
}
/** @base code */
private fun createSubFolders(filename: String, path: String) {
val subFolders = filename.split("/").toTypedArray()
if (subFolders.size <= 1) return
var pathNow = path
for (i in 0 until subFolders.size - 1) {
pathNow = pathNow + subFolders[i] + "/"
val fmd = File(pathNow)
if (fmd.exists()) continue
fmd.mkdirs()
}
}
/** @base code */
private fun createSeparator(path: String): String {
val dir = File(path)
if (!dir.exists()) dir.mkdirs()
return if (path.endsWith("/")) path else "$path/"
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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/9/19.
*/
@file:Suppress("unused")
package com.fankes.apperrorstracking.wrapper
import com.fankes.apperrorstracking.BuildConfig
/**
* 对 [BuildConfig] 的包装
*/
object BuildConfigWrapper {
const val APPLICATION_ID = BuildConfig.APPLICATION_ID
const val VERSION_NAME = BuildConfig.VERSION_NAME
const val VERSION_CODE = BuildConfig.VERSION_CODE
val isDebug = BuildConfig.DEBUG
}

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="#66A6A6A6" />
<corners android:radius="15dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#777777">
<item>
<shape android:shape="rectangle">
<solid android:color="#666E6E6E" />
<corners android:radius="15dp" />
</shape>
</item>
</ripple>

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="#666E6E6E" />
<corners android:radius="15dp" />
</shape>

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="#292929" />
<corners android:radius="5dp" />
</shape>

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,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#777777">
<item>
<shape android:shape="rectangle">
<solid android:color="#66DAD9D9" />
<corners android:radius="15dp" />
</shape>
</item>
</ripple>

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="#661B1B1B" />
<corners android:radius="15dp" />
</shape>

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="#26A69A" />
<corners android:radius="15dp" />
</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,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FF7043" />
<corners android:radius="15dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#777777">
<item>
<shape android:shape="rectangle">
<solid android:color="#66E4E4E4" />
<corners android:radius="15dp" />
</shape>
</item>
</ripple>

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="#66E4E4E4" />
<corners android:radius="15dp" />
</shape>

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="#EF5350" />
<corners android:radius="5dp" />
</shape>

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="#FF14171F" />
<corners android:radius="15dp" />
</shape>

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="#FF9800" />
<corners android:radius="15dp" />
</shape>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="250dp"
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024"
tools:ignore="VectorRaster">
<path
android:fillColor="#A0C538"
android:pathData="M142.2,56.9h739.6a85.3,85.3 0,0 1,85.3 85.3v739.6a85.3,85.3 0,0 1,-85.3 85.3H142.2a85.3,85.3 0,0 1,-85.3 -85.3V142.2a85.3,85.3 0,0 1,85.3 -85.3z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M636.5,561.5c0,14.5 11.6,26.7 25.7,26.7 14.4,0 25.7,-12.1 25.7,-26.7v-102.7c0,-14.7 -11.4,-26.4 -25.7,-26.4 -14.2,0 -25.7,11.7 -25.7,26.4v102.7zM544.6,371.8c0,-6.9 5.8,-12.6 12.6,-12.6 7.1,0 12.9,5.6 12.9,12.6a12.7,12.7 0,1 1,-25.5 0zM446.9,371.8a12.5,12.5 0,0 1,25.1 0,12.6 12.6,0 0,1 -12.6,12.8 12.6,12.6 0,0 1,-12.4 -12.8zM414.1,641.7h22.7v57.2c0,14.3 11.1,26.4 25.7,26.4 13.9,0 25.7,-12.1 25.7,-26.4v-57.2h39.9v57.2c0,14.3 10.9,26.4 25.5,26.4 14.4,0 25.7,-12.1 25.7,-26.4v-57.2h22.7c11.3,0 20.1,-9.1 20.1,-20.4L622.2,433.1L394,433.1v188.3c0,11.2 8.8,20.4 20.2,20.4zM621.7,416.6c-3.4,-35.5 -26.6,-65.2 -60,-80.8l21,-31c1.4,-2 1.1,-4.6 -0.6,-5.4 -1.7,-1.1 -3.6,-0.4 -5.1,1.3l-22.1,32.1c-14.8,-5.4 -30.4,-8.9 -47.3,-8.9s-33,3.5 -47.6,8.9l-21.6,-32.1c-1.5,-1.7 -3.4,-2.8 -5.1,-1.3 -1.7,1.1 -2.2,3.4 -0.9,5.4l21.2,31c-33.4,16 -57,45.1 -60,80.8h228.2zM327.1,561.5c0,14.5 11.2,26.7 25.7,26.7 13.9,0 25.7,-12.1 25.7,-26.7v-102.7c0,-14.7 -11.8,-26.4 -25.7,-26.4 -14.6,0 -25.7,11.7 -25.7,26.4v102.7z"
tools:ignore="VectorPath" />
</vector>

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

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ffffff"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ffffff"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,17h14v2L5,19zM12,5L5.33,15h13.34z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ffffff"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ffffff"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</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="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

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

@@ -0,0 +1,12 @@
<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="M316.8,784h377.6v140.8c0,42.2 -34.6,76.8 -76.8,76.8h-518.4c-42.2,0 -76.8,-34.6 -76.8,-76.8v-518.4c0,-42.2 34.6,-76.8 76.8,-76.8h140.8v377.6c0,42.2 34.6,76.8 76.8,76.8z" />
<path
android:fillColor="#ffffff"
android:pathData="M1008,105.6v518.4c0,42.2 -34.6,76.8 -76.8,76.8h-518.4c-42.2,0 -76.8,-34.6 -76.8,-76.8v-518.4c0,-42.2 34.6,-76.8 76.8,-76.8h518.4c42.2,0 76.8,34.6 76.8,76.8z" />
</vector>

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="250dp"
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024"
tools:ignore="VectorRaster">
<path
android:fillColor="#1D88E5"
android:pathData="M403.5,880.6c-202.8,0 -368.6,-165.9 -368.6,-368.6s165.9,-368.6 368.6,-368.6c116.7,0 223.2,54.3 292.9,145.4L563.2,367.6c-41,-44 -98.3,-70.7 -158.7,-70.7 -117.8,0 -215,96.3 -215,215s96.3,215 215,215c60.4,0 117.8,-26.6 158.7,-70.7l134.1,78.8C628.7,826.4 520.2,880.6 403.5,880.6z" />
<path
android:fillColor="#1D88E5"
android:pathData="M772.1,542.7L711.7,542.7v61.4h-62.5v-61.4L588.8,542.7v-61.4h60.4v-61.4h62.5v61.4h60.4v61.4zM989.2,542.7L926.7,542.7v61.4h-60.4v-61.4L803.8,542.7v-61.4h62.5v-61.4h60.4v61.4h62.5v61.4z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="250dp"
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024"
tools:ignore="VectorRaster">
<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" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="250dp"
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<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

@@ -0,0 +1,12 @@
<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="M768,678.4v108.8H256v-108.8c0,-12.8 -6.4,-19.2 -19.2,-19.2H147.2c-12.8,0 -19.2,6.4 -19.2,19.2v172.8c0,38.4 25.6,64 64,64h640c38.4,0 64,-25.6 64,-64v-172.8c0,-12.8 -6.4,-19.2 -19.2,-19.2h-89.6c-12.8,0 -19.2,6.4 -19.2,19.2z" />
<path
android:fillColor="#ffffff"
android:pathData="M499.2,115.2L281.6,390.4c-12.8,12.8 0,32 12.8,32H448v268.8c0,6.4 6.4,12.8 19.2,12.8h89.6c12.8,0 19.2,-6.4 19.2,-19.2V416h153.6c19.2,0 25.6,-19.2 12.8,-32L524.8,115.2c-6.4,-6.4 -19.2,-6.4 -25.6,0z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="184.6dp"
android:height="150dp"
android:viewportWidth="1260"
android:viewportHeight="1024">
<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>

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

@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="250dp"
android:height="250dp"
android:viewportWidth="1024"
android:viewportHeight="1024"
tools:ignore="VectorRaster">
<path
android:fillColor="#FF1515"
android:pathData="M747,175.6c-29.7,21 -58.4,38.9 -90.1,64.5 -24.1,19.5 -67.6,46.6 -69.6,82.4 -3.6,54.8 80.9,106 36.4,175.6 -16.9,26.6 -46.1,37.9 -82.4,54.3 -4.6,-8.2 9.7,-14.8 15.4,-23 56.3,-81.4 -58.4,-108.5 -44,-208.9 13.8,-97.8 128,-131.6 234.5,-144.9z" />
<path
android:fillColor="#2365C4"
android:pathData="M388.1,511c-26.6,12.3 -71.2,14.8 -90.1,41.5 19.5,14.8 48.1,14.3 74.8,15.4 109.1,5.1 245.2,-4.6 337.9,-20.5 3.1,6.7 -12.8,18.4 -23,25.6 -58.4,43 -240.6,54.8 -366.6,46.6 -42,-3.1 -138.2,-13.8 -139.3,-51.7 -1.5,-45.6 116.7,-50.7 164.9,-54.3 9.7,-1 27.6,-4.6 41.5,-2.6zM333.8,626.7c12.3,1.5 -8.7,8.7 -5.1,17.9 44.5,44 182.3,31.2 250.4,17.9 14.3,-3.1 28.2,-11.3 38.9,-10.2 25.6,2.6 42.5,32.3 64.5,36.4 -78.3,35.3 -229.9,52.2 -340.5,30.7 -28.7,-5.6 -78.3,-21 -79.9,-44 -2.6,-30.7 49.2,-43.5 71.7,-48.6zM367.6,732.7c8.2,2.6 -3.1,7.2 -2.6,10.2 23.6,41 139.8,26.1 198.7,12.8 11.8,-3.1 23.6,-11.3 33.8,-10.2 29.7,2 41.5,33.3 67.1,38.9 -82.4,50.2 -281.6,70.7 -364,7.7 -4.1,-46.1 32.8,-51.2 67.1,-59.4z" />
<path
android:fillColor="#2365C4"
android:pathData="M292.9,810c-24.6,6.1 -88.1,-2.6 -90.1,30.7 -1,12.8 21.5,28.2 36.4,33.8 84,31.7 252.9,36.9 392.2,20.5 64.5,-7.7 185.9,-29.2 170.5,-95.2 19.5,2.6 36.9,14.8 38.9,33.8 7.7,71.2 -155.6,101.4 -221.7,108.5 -143.9,15.9 -323.1,12.8 -433.7,-25.6 -35.8,-12.3 -79.4,-35.3 -77.3,-69.6 2,-58.4 141.3,-74.2 184.8,-36.9z" />
<path
android:fillColor="#2365C4"
android:pathData="M512,1024c-96.8,-10.8 -190,-24.6 -268.3,-59.4 205.3,49.2 504.3,45.6 647.7,-59.4 7.7,-5.6 14.8,-16.9 25.6,-15.4 -36.4,108.5 -175.1,115.7 -293.9,134.1H512z" />
<path
android:fillColor="#FF1515"
android:pathData="M579.1,0c17.9,16.9 30.7,48.6 30.7,82.4 0,99.8 -106,157.7 -157.2,224.3 -11.3,14.8 -26.1,37.9 -25.6,62 0.5,54.3 56.8,115.2 77.3,159.7 -36.4,-23.6 -79.9,-55.3 -111.1,-92.7 -30.7,-37.4 -62,-97.3 -33.8,-149.5 42.5,-78.3 169.5,-124.9 214,-208.9 11.3,-20.5 20,-51.7 5.6,-77.3z" />
<path
android:fillColor="#2365C4"
android:pathData="M731.1,536.6c53.2,-45.6 143.4,-27.6 146.9,49.2 4.6,89.6 -93.7,140.3 -165.4,144.4 32.8,-31.2 120.3,-81.9 103.4,-154.6 -6.7,-29.7 -43,-47.6 -85,-38.9z" />
</vector>

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp"
android:height="108dp"
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>
</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

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

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

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

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

@@ -0,0 +1,499 @@
<?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:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorThemeBackground"
android:orientation="vertical"
tools:context=".ui.activity.errors.AppErrorsDetailActivity"
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp,UnusedAttribute">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
android:gravity="center|start"
android:paddingLeft="15dp"
android:paddingTop="15dp"
android:paddingRight="15dp"
android:paddingBottom="15dp">
<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" />
<TextView
android:id="@+id/detail_title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="2.5dp"
android:layout_weight="1"
android:singleLine="true"
android:text="@string/app_name"
android:textColor="@color/colorTextGray"
android:textSize="19sp"
android:textStyle="bold" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/print_icon"
android:layout_width="23dp"
android:layout_height="23dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_print"
android:tint="@color/colorTextGray"
android:tooltipText="@string/print_to_logcat" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/copy_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_copy"
android:tint="@color/colorTextGray"
android:tooltipText="@string/copy_error_stack" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/export_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_to_file" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/share_icon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_share"
android:tint="@color/colorTextGray"
android:tooltipText="@string/share_error_stack" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/app_panel_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:fadingEdgeLength="10dp"
android:fillViewport="true"
android:requiresFadingEdge="vertical"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/app_info_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/bg_permotion_ripple"
android:gravity="center|start"
android:padding="10dp">
<androidx.cardview.widget.CardView
android:layout_width="45dp"
android:layout_height="45dp"
app:cardBackgroundColor="@color/trans"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="45dp"
android:layout_height="45dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:gravity="center|start"
android:orientation="horizontal">
<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" />
<ImageView
android:layout_width="13dp"
android:layout_height="13dp"
android:src="@drawable/ic_exception"
app:tint="#FFEF5350" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:id="@+id/app_cpu_abi_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/bg_black_round"
android:ellipsize="end"
android:paddingLeft="3dp"
android:paddingTop="0.5dp"
android:paddingRight="3dp"
android:paddingBottom="0.5dp"
android:singleLine="true"
android:text="@string/no_cpu_abi"
android:textColor="@color/white"
android:textSize="9sp" />
<TextView
android:id="@+id/app_version_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/error_type_icon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="10dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/bg_permotion_round"
android:gravity="center|start"
android:orientation="vertical"
android:padding="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start|top"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_info"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_info_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="5dp"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/jvm_error_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_type"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_type_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#FFEF5350"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_file_name"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_file_name_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_throw_class"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_throw_class_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_throw_method"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_throw_method_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_line_number"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_line_number_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/error_record_time"
android:textColor="@color/colorTextDark"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/error_record_time_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
</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_trace_fixed_text"
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:lineSpacingExtra="5dp"
android:padding="15dp"
android:textColor="#B65B57"
android:textIsSelectable="true"
android:textSize="12sp"
android:typeface="monospace" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -0,0 +1,85 @@
<?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.errors.AppErrorsRecordActivity"
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="15dp"
android:paddingRight="15dp"
android:paddingBottom="15dp">
<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" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="2.5dp"
android:layout_weight="1"
android:singleLine="true"
android:text="@string/muted_errors_apps"
android:textColor="@color/colorTextGray"
android:textSize="19sp"
android:textStyle="bold" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/unmute_all_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_clear"
android:tint="@color/colorTextGray"
android:tooltipText="@string/unmute_all"
android:visibility="gone" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp">
<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="15dp"
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

@@ -0,0 +1,132 @@
<?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.errors.AppErrorsRecordActivity"
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/errors_record"
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:singleLine="true"
android:text="@string/loading"
android:textColor="@color/colorTextDark"
android:textSize="11.5sp" />
</LinearLayout>
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/app_error_sis_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_statistics"
android:tint="@color/colorTextGray"
android:tooltipText="@string/app_errors_statistics"
android:visibility="gone" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/clear_all_icon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_clear"
android:tint="@color/colorTextGray"
android:tooltipText="@string/clear_all"
android:visibility="gone" />
<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"
android:visibility="gone" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/list_progress_view"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:indeterminate="true"
app:trackCornerRadius="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"
android:visibility="gone" />
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/trans"
android:dividerHeight="15dp"
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

@@ -0,0 +1,133 @@
<?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.ConfigureActivity"
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/apps_config_template"
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:singleLine="true"
android:text="@string/loading"
android:textColor="@color/colorTextDark"
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"
android:layout_height="25dp"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_batch"
android:tint="@color/colorTextGray"
android:tooltipText="@string/batch_operations"
android:visibility="gone" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/filter_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_filter"
android:tint="@color/colorTextGray"
android:tooltipText="@string/filter_by_condition"
android:visibility="gone" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:animateLayoutChanges="true">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/list_progress_view"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:indeterminate="true"
app:trackCornerRadius="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_result"
android:textColor="@color/colorTextDark"
android:textSize="17sp"
android:visibility="gone" />
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/trans"
android:dividerHeight="15dp"
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

@@ -0,0 +1,733 @@
<?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,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">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="@string/app_name"
android:textColor="@color/colorTextGray"
android:textSize="25sp"
android:textStyle="bold" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/title_logger_icon"
style="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="15dp"
android:alpha="0.85"
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" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/title_github_icon"
style="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="27dp"
android:layout_height="27dp"
android:layout_marginEnd="5dp"
android:alpha="0.85"
android:src="@drawable/ic_github"
android:tint="@color/colorTextGray"
android:tooltipText="@string/project_address" />
</LinearLayout>
<LinearLayout
android:id="@+id/main_lin_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="5dp"
android:background="@drawable/bg_dark_round"
android:elevation="0dp"
android:gravity="center">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/main_img_status"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginStart="25dp"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_warn"
android:tint="@color/white" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingBottom="10dp">
<TextView
android:id="@+id/main_text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/module_not_activated"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:id="@+id/main_text_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/module_version"
android:textColor="@color/white"
android:textSize="13sp" />
<TextView
android:id="@+id/main_text_release_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:background="@drawable/bg_orange_round"
android:ellipsize="end"
android:paddingLeft="5dp"
android:paddingTop="2dp"
android:paddingRight="5dp"
android:paddingBottom="2dp"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="11sp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/main_text_system_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/system_version"
android:textColor="@color/white"
android:textSize="13sp" />
<TextView
android:id="@+id/main_text_api_way"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:alpha="0.6"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="11sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fadingEdgeLength="10dp"
android:fillViewport="true"
android:requiresFadingEdge="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:animateLayoutChanges="true"
android:background="@drawable/bg_permotion_round"
android:elevation="0dp"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:gravity="center|start">
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
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"
android:layout_height="wrap_content"
android:alpha="0.85"
android:singleLine="true"
android:text="@string/preference_settings"
android:textColor="@color/colorTextGray"
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/only_show_errors_in_front_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="5dp"
android:text="@string/only_show_errors_in_front"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/only_show_errors_in_front_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<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"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="5dp"
android:text="@string/only_show_errors_in_main_process"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/only_show_errors_in_main_process_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/always_shows_reopen_app_options_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="5dp"
android:text="@string/errors_dialog_always_show_reopen"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/errors_dialog_always_show_reopen_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/enable_apps_configs_template_switch"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:text="@string/enable_apps_config_template"
android:textAllCaps="false"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:id="@+id/mgr_apps_configs_template_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:layout_marginTop="10dp"
android:layout_marginRight="13dp"
android:layout_marginBottom="5dp"
android:background="@drawable/bg_button_round"
android:gravity="center"
android:padding="10dp"
android:singleLine="true"
android:text="@string/mgr_apps_config_template"
android:textColor="@color/colorTextGray"
android:textSize="15sp"
android:visibility="gone" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginTop="5dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/apps_config_template_tip"
android:textColor="@color/colorTextDark"
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"
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:paddingTop="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
android:gravity="center|start">
<androidx.cardview.widget.CardView
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginEnd="10dp"
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"
android:layout_height="wrap_content"
android:alpha="0.85"
android:singleLine="true"
android:text="@string/function_mgr"
android:textColor="@color/colorTextGray"
android:textSize="12sp" />
</LinearLayout>
<TextView
android:id="@+id/view_errors_record_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_button_round"
android:gravity="center"
android:padding="10dp"
android:singleLine="true"
android:text="@string/view_errors_record"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/view_errors_record_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<TextView
android:id="@+id/view_muted_errors_apps_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:layout_marginBottom="10dp"
android:background="@drawable/bg_button_round"
android:gravity="center"
android:padding="10dp"
android:singleLine="true"
android:text="@string/view_muted_errors_apps"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="10dp"
android:alpha="0.6"
android:lineSpacingExtra="6dp"
android:text="@string/view_muted_errors_apps_tip"
android:textColor="@color/colorTextDark"
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="#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"
android:layout_height="wrap_content"
android:alpha="0.85"
android:singleLine="true"
android:text="@string/display_settings"
android:textColor="@color/colorTextGray"
android:textSize="12sp" />
</LinearLayout>
<com.fankes.apperrorstracking.ui.widget.MaterialSwitch
android:id="@+id/hide_icon_in_launcher_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hide_app_icon_on_launcher"
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/hide_app_icon_on_launcher_tip"
android:textColor="@color/colorTextDark"
android:textSize="12sp" />
<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/hide_app_icon_on_launcher_notice"
android:textColor="#FF5722"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/app_analytics_config_item"
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:id="@+id/payment_following_zh_cn_item"
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:paddingRight="15dp"
tools:ignore="HardcodedText">
<TextView
android:id="@+id/link_with_follow_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:gravity="center"
android:lineSpacingExtra="6dp"
android:text="恰饭时间\n点击前往酷安关注我获取我的更多应用"
android:textColor="@color/colorTextGray"
android:textSize="16sp" />
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
app:cardCornerRadius="15dp"
app:cardElevation="0dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@mipmap/bg_payment_code" />
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center"
android:lineSpacingExtra="6dp"
android:text="开发者 酷安 @星夜不荟\n未经允许不得转载、修改复制我的劳动成果"
android:textColor="@color/colorTextGray"
android:textSize="16sp" />
</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:layout_marginBottom="10dp"
android:background="@drawable/bg_permotion_round"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginEnd="10dp"
android:src="@mipmap/ic_yukihookapi" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:ellipsize="end"
android:lineSpacingExtra="6dp"
android:maxLines="2"
android:text="@string/about_module"
android:textColor="@color/colorTextGray"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -0,0 +1,67 @@
<?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:background="@drawable/bg_permotion_ripple"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="10dp"
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp">
<androidx.cardview.widget.CardView
android:layout_width="45dp"
android:layout_height="45dp"
app:cardBackgroundColor="@color/trans"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="45dp"
android:layout_height="45dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="vertical">
<TextView
android:id="@+id/app_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:id="@+id/mute_type_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
</LinearLayout>
<TextView
android:id="@+id/unmute_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_round"
android:gravity="center"
android:padding="10dp"
android:singleLine="true"
android:text="@string/unmute"
android:textColor="@color/colorTextGray"
android:textSize="14sp" />
</LinearLayout>

View File

@@ -0,0 +1,127 @@
<?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:background="@drawable/bg_permotion_ripple"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="10dp"
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp">
<androidx.cardview.widget.CardView
android:layout_width="45dp"
android:layout_height="45dp"
app:cardBackgroundColor="@color/trans"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="45dp"
android:layout_height="45dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:gravity="center|start"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.75"
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"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_weight="0.25"
android:alpha="0.85"
android:ellipsize="start"
android:gravity="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:id="@+id/error_type_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/bg_red_round"
android:ellipsize="end"
android:paddingLeft="3dp"
android:paddingTop="0.5dp"
android:paddingRight="3dp"
android:paddingBottom="0.5dp"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="9sp" />
<TextView
android:id="@+id/error_msg_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/error_type_icon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginEnd="10dp" />
</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:background="@drawable/bg_permotion_ripple"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="10dp"
tools:ignore="ContentDescription,UseCompoundDrawables,SmallSp">
<androidx.cardview.widget.CardView
android:layout_width="45dp"
android:layout_height="45dp"
app:cardBackgroundColor="@color/trans"
app:cardCornerRadius="10dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/app_icon"
android:layout_width="45dp"
android:layout_height="45dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:gravity="center|start"
android:orientation="vertical">
<TextView
android:id="@+id/app_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextGray"
android:textSize="15sp" />
<TextView
android:id="@+id/config_type_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/colorTextDark"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>

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

@@ -0,0 +1,66 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingTop="15dp"
android:paddingRight="15dp">
<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_errors_how_to_show_tip"
android:textSize="13sp" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/config_radio_0"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
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: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: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>
</LinearLayout>

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