From 2fce91b8c0faf1290d8a35ee022dab3cdbc28a54 Mon Sep 17 00:00:00 2001
From: cloudroam <cloudroam>
Date: 星期五, 14 二月 2025 14:31:06 +0800
Subject: [PATCH] init
---
app/src/main/res/menu/bottom_nav_menu.xml | 19
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt | 42
gradle/libs.versions.toml | 32
app/src/main/res/drawable/ic_home_black_24dp.xml | 9
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt | 13
.idea/misc.xml | 9
app/.idea/misc.xml | 10
.idea/migrations.xml | 10
app/src/main/java/com/example/firstapp/ui/notifications/NotificationsViewModel.kt | 13
app/src/main/java/com/example/firstapp/database/AppDatabase.kt | 104
app/src/main/java/com/example/firstapp/utils/AppUtils.kt | 104
app/src/main/java/com/example/firstapp/utils/RandomUtils.kt | 293 ++
.idea/gradle.xml | 20
app/src/main/res/layout/activity_main.xml | 33
app/src/main/java/com/example/firstapp/entity/SmsInfo.kt | 37
app/src/main/java/com/example/firstapp/utils/RSACrypt.kt | 209 +
app/.idea/.gitignore | 3
app/src/main/res/values/strings.xml | 10
app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt | 66
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 6
app/src/main/java/com/example/firstapp/utils/HistoryUtils.kt | 115
app/src/main/res/mipmap-xhdpi/ic_launcher.webp | 0
.idea/kotlinc.xml | 6
app/src/main/res/layout/fragment_notifications.xml | 22
app/src/main/res/layout/sms_code.xml | 12
app/src/main/java/com/example/firstapp/utils/FrpcUtils.kt | 51
.idea/compiler.xml | 6
app/src/main/res/mipmap-mdpi/ic_launcher.webp | 0
app/proguard-rules.pro | 21
app/src/main/res/xml/backup_rules.xml | 13
app/src/main/res/layout/item_layout.xml | 22
app/src/main/res/values/themes.xml | 16
app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt | 44
app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt | 57
app/src/main/java/com/example/firstapp/utils/mail/PgpUtils.kt | 247 ++
app/src/main/java/com/example/firstapp/utils/SM4Crypt.kt | 62
gradle/wrapper/gradle-wrapper.properties | 8
app/src/main/java/com/example/firstapp/utils/tinker/TinkerLoadLibrary.kt | 196 +
app/src/main/res/values/colors.xml | 10
app/src/main/res/values-night/themes.xml | 16
app/src/main/res/drawable/ic_dashboard_black_24dp.xml | 9
app/src/main/java/com/example/firstapp/ui/notifications/NotificationsFragment.kt | 42
app/src/main/java/com/example/firstapp/database/repository/MsgRepository.kt | 21
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp | 0
app/src/main/res/navigation/mobile_navigation.xml | 25
app/src/main/res/drawable/ic_launcher_foreground.xml | 30
versions.gradle | 223 +
app/src/main/java/com/example/firstapp/database/entity/Code.kt | 18
app/src/main/java/com/example/firstapp/utils/DrawableUtils.kt | 59
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp | 0
app/src/main/res/drawable/ic_sim2.xml | 13
app/src/main/res/layout/fragment_home.xml | 30
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 6
app/src/main/res/drawable/ic_sms.xml | 9
gradle.properties | 39
app/src/main/java/com/example/firstapp/entity/Item.kt | 7
app/src/androidTest/java/com/example/firstapp/ExampleInstrumentedTest.kt | 24
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp | 0
app/src/main/java/com/example/firstapp/utils/tinker/ShareReflectUtil.kt | 240 +
app/src/main/java/com/example/firstapp/utils/mail/EmailSender.kt | 174 +
app/src/main/java/com/example/firstapp/receiver/CactusReceiver.kt | 33
app/libs/frpclib-sources.jar | 0
.idea/deploymentTargetSelector.xml | 18
app/src/main/java/com/example/firstapp/entity/Rule.kt | 17
app/src/main/res/drawable/ic_sim.xml | 19
app/src/main/java/com/example/firstapp/core/Core.kt | 29
app/src/main/java/com/example/firstapp/adapter/MyAdapter_bak.kt | 33
app/src/main/java/com/example/firstapp/service/HttpServerService.kt | 62
app/src/main/java/com/example/firstapp/database/AppDatabase_bak.kt | 442 +++
gradlew.bat | 89
app/libs/frpclib.aar | 0
app/src/main/java/com/example/firstapp/utils/Base64.kt | 88
app/build.gradle | 266 ++
app/src/main/java/com/example/firstapp/utils/BluetoothUtils.kt | 38
app/src/main/res/values/dimens.xml | 5
gradlew | 185 +
app/src/main/java/com/example/firstapp/utils/Constants.kt | 318 ++
app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt | 21
app/.idea/gradle.xml | 12
.idea/.gitignore | 3
app/src/main/java/com/example/firstapp/adapter/MyAdapter2.kt | 32
app/src/main/java/com/example/firstapp/utils/CactusSave.kt | 15
app/src/main/res/drawable/ic_notifications_black_24dp.xml | 9
app/src/main/res/mipmap-hdpi/ic_launcher.webp | 0
app/x-library.gradle | 66
app/src/main/java/com/example/firstapp/utils/Log.kt | 161 +
.idea/.name | 1
.idea/codeStyles/codeStyleConfig.xml | 5
app/src/main/java/com/example/firstapp/App.kt | 455 +++
app/src/main/java/com/example/firstapp/utils/CacheUtils.kt | 108
.gitignore | 27
app/src/main/res/xml/data_extraction_rules.xml | 19
gradle/wrapper/gradle-wrapper.jar | 0
app/src/main/res/drawable/ic_launcher_background.xml | 170 +
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp | 0
app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt | 36
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp | 0
app/src/main/java/com/example/firstapp/adapter/MyAdapter.kt | 42
app/src/main/java/com/example/firstapp/utils/LocationUtils.kt | 72
app/src/main/res/layout/fragment_dashboard.xml | 22
build.gradle | 45
app/src/main/java/com/example/firstapp/MainActivity.kt | 131 +
app/src/main/res/drawable/ic_sim1.xml | 13
app/src/main/java/com/example/firstapp/utils/mail/SmimeUtils.kt | 252 ++
app/src/main/java/com/example/firstapp/database/entity/Msg.kt | 45
app/.gitignore | 1
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp | 0
settings.gradle | 39
app/src/main/AndroidManifest.xml | 143 +
app/src/main/java/com/example/firstapp/utils/SettingUtils.kt | 166 +
app/src/test/java/com/example/firstapp/ExampleUnitTest.kt | 17
.idea/codeStyles/Project.xml | 123 +
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp | 0
app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt | 71
app/src/main/java/com/example/firstapp/utils/SharedPreference.kt | 131 +
app/src/main/java/com/example/firstapp/service/BluetoothScanService.kt | 73
app/src/main/java/com/example/firstapp/database/ext/ConvertersDate.kt | 16
.idea/runConfigurations.xml | 17
app/src/main/res/drawable/ic_app.xml | 23
app/src/main/java/com/example/firstapp/utils/KeepAliveUtils.kt | 52
120 files changed, 7,199 insertions(+), 12 deletions(-)
diff --git a/.gitignore b/.gitignore
index 32858aa..aa724b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,15 @@
-*.class
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..085690a
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+FirstApp
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+<component name="ProjectCodeStyleConfiguration">
+ <code_scheme name="Project" version="173">
+ <JetCodeStyleSettings>
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </JetCodeStyleSettings>
+ <codeStyleSettings language="XML">
+ <option name="FORCE_REARRANGE_MODE" value="1" />
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ </indentOptions>
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+ </codeStyleSettings>
+ </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+ <state>
+ <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+ </state>
+</component>
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <bytecodeTargetLevel target="17" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..51a703e
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="deploymentTargetSelector">
+ <selectionStates>
+ <SelectionState runConfigName="app">
+ <option name="selectionMode" value="DROPDOWN" />
+ <DropdownSelection timestamp="2025-02-14T05:55:28.808113500Z">
+ <Target type="DEFAULT_BOOT">
+ <handle>
+ <DeviceId pluginId="PhysicalDevice" identifier="serial=88Y5T19C14010323" />
+ </handle>
+ </Target>
+ </DropdownSelection>
+ <DialogSelection />
+ </SelectionState>
+ </selectionStates>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..ad84da2
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="CHOOSE_PER_TEST" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="gradleJvm" value="17" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ </set>
+ </option>
+ <option name="resolveExternalAnnotations" value="false" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..fdf8d99
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="KotlinJpsPluginSettings">
+ <option name="version" value="1.9.0" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectMigrations">
+ <option name="MigrateToGradleLocalJavaHome">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ </set>
+ </option>
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..d7916a5
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+<project version="4">
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RunConfigurationProducerService">
+ <option name="ignoredProducers">
+ <set>
+ <option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
+ <option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
+ <option value="com.intellij.execution.junit.PatternConfigurationProducer" />
+ <option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
+ <option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
+ <option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
+ </set>
+ </option>
+ </component>
+</project>
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/.idea/.gitignore b/app/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/app/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/app/.idea/gradle.xml b/app/.idea/gradle.xml
new file mode 100644
index 0000000..b898c0a
--- /dev/null
+++ b/app/.idea/gradle.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="GRADLE" />
+ <option name="distributionType" value="DEFAULT_WRAPPED" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project>
\ No newline at end of file
diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml
new file mode 100644
index 0000000..6ff4d26
--- /dev/null
+++ b/app/.idea/misc.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
+ <component name="ProjectRootManager" version="2" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..ccc635c
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,266 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.jetbrains.kotlin.android)
+// id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'kotlin-parcelize'
+// id 'img-optimizer'
+// id 'com.yanzhenjie.andserver'
+}
+
+android {
+ namespace 'com.example.firstapp'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.example.firstapp"
+ minSdk 24
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+//dependencies {
+//
+// implementation libs.androidx.core.ktx
+// implementation libs.androidx.appcompat
+// implementation libs.material
+// implementation libs.androidx.constraintlayout
+// implementation libs.androidx.lifecycle.livedata.ktx
+// implementation libs.androidx.lifecycle.viewmodel.ktx
+// implementation libs.androidx.navigation.fragment.ktx
+// implementation libs.androidx.navigation.ui.ktx
+// testImplementation libs.junit
+// androidTestImplementation libs.androidx.junit
+// androidTestImplementation libs.androidx.espresso.core
+//
+//
+// def work_version = '2.8.1'
+// //noinspection GradleDependency
+// api("androidx.work:work-multiprocess:$work_version")
+// //noinspection GradleDependency
+// api("androidx.work:work-runtime-ktx:$work_version")
+//
+// //Android Room
+// def room_version = '2.5.2'
+// //noinspection GradleDependency
+// implementation "androidx.room:room-ktx:$room_version"
+// //noinspection GradleDependency
+// implementation "androidx.room:room-runtime:$room_version"
+// //noinspection GradleDependency
+// implementation "androidx.room:room-paging:$room_version"
+// //noinspection GradleDependency
+// implementation "androidx.room:room-rxjava2:$room_version"
+// //noinspection KaptUsageInsteadOfKsp
+// kapt "androidx.room:room-compiler:$room_version"
+//
+// //基础功能的工具类
+// implementation("com.github.xuexiangjys.XUtil:xutil-core:2.0.0")
+// //附加功能的工具类
+// implementation("com.github.xuexiangjys.XUtil:xutil-sub:2.0.0")
+//
+//
+// //Android Keep Alive(安卓保活),Cactus 集成双进程前台服务,JobScheduler,onePix(一像素),WorkManager,无声音乐
+// //https://github.com/gyf-dev/Cactus
+// implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
+//// implementation 'com.gyf.cactus:cactus-support:1.1.3-beta13'
+//
+//}
+
+android {
+ packagingOptions {
+ //去除FrpcLib的so,用时下载并动态加载
+ if (excludeFrpclib.toBoolean()) {
+ exclude 'lib/armeabi-v7a/libgojni.so'
+ exclude 'lib/arm64-v8a/libgojni.so'
+ exclude 'lib/x86/libgojni.so'
+ exclude 'lib/x86_64/libgojni.so'
+ }
+ jniLibs {
+ excludes += ["kotlin/**"]
+ }
+ resources {
+ merge 'META-INF/mailcap'
+ pickFirst 'META-INF/LICENSE.md'
+ pickFirst 'META-INF/NOTICE.md'
+ excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1']
+ excludes += ["META-INF/*.kotlin_module", "META-INF/*.version", "kotlin/**", "DebugProbesKt.bin"]
+ }
+ }
+}
+
+dependencies {
+
+ implementation libs.androidx.core.ktx
+ implementation libs.androidx.appcompat
+ implementation libs.material
+ implementation libs.androidx.constraintlayout
+ implementation libs.androidx.lifecycle.livedata.ktx
+ implementation libs.androidx.lifecycle.viewmodel.ktx
+ implementation libs.androidx.navigation.fragment.ktx
+ implementation libs.androidx.navigation.ui.ktx
+ testImplementation libs.junit
+ androidTestImplementation libs.androidx.junit
+ androidTestImplementation libs.androidx.espresso.core
+
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ //frpc
+ implementation files('libs/frpclib.aar')
+
+ //MQTT协议
+ implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
+
+ testImplementation deps.junit
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation deps.espresso.core
+
+ //noinspection GradleDependency
+ implementation 'androidx.core:core-ktx:1.9.0'
+ //noinspection GradleDependency
+ implementation 'androidx.activity:activity-ktx:1.6.1'
+ //noinspection GradleDependency
+ implementation 'androidx.fragment:fragment-ktx:1.5.5'
+ implementation "androidx.cardview:cardview:1.0.0"
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+
+ //分包
+ implementation deps.androidx.multidex
+
+ //vLayout:https://github.com/alibaba/vlayout
+ implementation 'com.alibaba.android:vlayout:1.3.0'
+ //下拉刷新
+ implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.4'
+ implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.4'
+ //WebView
+ implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.1'
+ implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.1'//选填
+ //屏幕适配AutoSize:https://github.com/JessYanCoding/AndroidAutoSize
+ implementation 'me.jessyan:autosize:1.2.1'
+ //友盟统计
+ implementation 'com.umeng.umsdk:common:9.6.8'
+ implementation 'com.umeng.umsdk:asms:1.8.2'
+
+ //预加载占位控件
+ implementation 'me.samlss:broccoli:1.0.0'
+
+ //RichText:https://github.com/zzhoujay/RichText
+ implementation 'com.zzhoujay.richtext:richtext:3.0.8'
+
+ //美团多渠道打包
+ //implementation 'com.meituan.android.walle:library:1.1.6'
+
+ def work_version = '2.8.1'
+ //noinspection GradleDependency
+ api("androidx.work:work-multiprocess:$work_version")
+ //noinspection GradleDependency
+ api("androidx.work:work-runtime-ktx:$work_version")
+
+ //Android Room
+ def room_version = '2.5.2'
+ //noinspection GradleDependency
+ implementation "androidx.room:room-ktx:$room_version"
+ //noinspection GradleDependency
+ implementation "androidx.room:room-runtime:$room_version"
+ //noinspection GradleDependency
+ implementation "androidx.room:room-paging:$room_version"
+ //noinspection GradleDependency
+ implementation "androidx.room:room-rxjava2:$room_version"
+ //noinspection KaptUsageInsteadOfKsp
+ kapt "androidx.room:room-compiler:$room_version"
+
+ //CodeView:https://github.com/AmrDeveloper/CodeView
+ implementation 'io.github.amrdeveloper:codeview:1.3.9'
+
+ //LiveEventBus:https://github.com/JeremyLiao/LiveEventBus
+ implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
+
+ //MarkdownView:https://github.com/tiagohm/MarkdownView
+ implementation 'com.github.tiagohm.MarkdownView:library:0.19.0'
+ //implementation 'com.github.tiagohm.MarkdownView:emoji:0.19.0'
+
+ def retrofit2_version = '2.9.0'
+ //noinspection GradleDependency
+ implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
+ //noinspection GradleDependency
+ implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
+ //noinspection GradleDependency
+ implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
+
+ def paging_version = "3.1.1"
+ //noinspection GradleDependency
+ implementation "androidx.paging:paging-runtime-ktx:$paging_version"
+ // alternatively - without Android dependencies for tests
+ //noinspection GradleDependency
+ testImplementation "androidx.paging:paging-common-ktx:$paging_version"
+
+ //权限请求框架:https://github.com/getActivity/XXPermissions
+ implementation 'com.github.getActivity:XXPermissions:18.62'
+ //语种切换框架:https://github.com/getActivity/MultiLanguages
+ implementation 'com.github.getActivity:MultiLanguages:b47f7be' //9.3
+
+ def mail_version = '1.6.7'
+ implementation "com.sun.mail:android-mail:$mail_version"
+ implementation "com.sun.mail:android-activation:$mail_version"
+
+ //国密算法SM4 的JAVA实现(基于BC实现)
+ def bouncycastle_version = '1.77'
+ //noinspection GradleDependency
+ api "org.bouncycastle:bcprov-jdk18on:$bouncycastle_version"
+ //邮件 S/MIME 加密和签名
+ //implementation "org.spongycastle:bcmail-jdk18on:$bouncycastle_version" //Android下报错
+ //noinspection GradleDependency
+ implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version"
+ //implementation "org.bouncycastle:bctls-jdk18on:$bouncycastle_version"
+ //邮件 PGP 加密和签名
+ //implementation "org.bouncycastle:bcpg-jdk18on:$bouncycastle_version" //Thunderbird无法解密
+ //PGPainless: https://github.com/pgpainless/pgpainless
+ implementation 'org.pgpainless:pgpainless-core:1.6.7'
+
+ //Android Keep Alive(安卓保活),Cactus 集成双进程前台服务,JobScheduler,onePix(一像素),WorkManager,无声音乐
+ //https://github.com/gyf-dev/Cactus
+ implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
+
+ //HTTP服务器:https://github.com/yanzhenjie/AndServer
+ implementation 'cn.ppps.andserver:api:2.1.12'
+ kapt 'cn.ppps.andserver:processor:2.1.12'
+
+ //Location 是一个通过 Android 自带的 LocationManager 来实现的定位功能:https://github.com/jenly1314/Location
+ implementation 'com.github.pppscn:location:1.0.0'
+
+ //Partial implementation of Quartz Cron Java for Android: https://github.com/gatewayapps/crondroid
+ implementation 'gatewayapps.crondroid:crondroid:1.0.0'
+ //Java Parser For Cron Expressions: https://github.com/grahamar/cron-parser
+ implementation 'net.redhogs.cronparser:cron-parser-core:3.5'
+
+ //侧边栏菜单:https://github.com/yarolegovich/SlidingRootNav
+ implementation 'com.yarolegovich:sliding-root-nav:1.1.1'
+
+ //基础功能的工具类
+ implementation("com.github.xuexiangjys.XUtil:xutil-core:2.0.0")
+ //附加功能的工具类
+ implementation("com.github.xuexiangjys.XUtil:xutil-sub:2.0.0")
+
+
+}
\ No newline at end of file
diff --git a/app/libs/frpclib-sources.jar b/app/libs/frpclib-sources.jar
new file mode 100644
index 0000000..48fe9ba
--- /dev/null
+++ b/app/libs/frpclib-sources.jar
Binary files differ
diff --git a/app/libs/frpclib.aar b/app/libs/frpclib.aar
new file mode 100644
index 0000000..ea179a1
--- /dev/null
+++ b/app/libs/frpclib.aar
Binary files differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/firstapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/firstapp/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..22e6a12
--- /dev/null
+++ b/app/src/androidTest/java/com/example/firstapp/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.firstapp
+
+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.example.firstapp", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..088e3d9
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-feature
+ android:name="android.hardware.telephony"
+ android:required="false" />
+
+ <!-- 用于查询所有安装的应用程序包。这个权限在 Android 11 及以上版本可能会受到限制。-->
+ <uses-permission
+ android:name="android.permission.QUERY_ALL_PACKAGES"
+ tools:ignore="QueryAllPackagesPermission" />
+
+ <!-- 允许访问设备的精确位置信息(GPS)。-->
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <!-- 允许访问设备的粗略位置信息(基于 Wi-Fi 或移动网络)。-->
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <!-- 允许访问后台位置数据,用于在应用不在前台时获取位置信息。-->
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <!-- 允许更改 Wi-Fi 网络的状态。-->
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <!-- 允许访问网络状态(如是否连接到 Wi-Fi 或移动数据)。-->
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <!-- 允许访问 Wi-Fi 的当前状态(是否连接到 Wi-Fi,Wi-Fi 的信号强度等)。-->
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <!-- 允许更改网络连接状态。-->
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <!-- 允许访问网络上的资源(用于访问互联网)。-->
+ <uses-permission android:name="android.permission.INTERNET" />
+ <!-- 授予应用程序访问系统开机事件的权限 -->
+ <!-- 允许读取设备的电话状态,通常这个权限需要系统签名的应用才能使用。-->
+ <uses-permission
+ android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许应用在设备启动完成时接收广播。-->
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <!-- 允许读取外部存储(如 SD 卡中的文件)。-->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <!-- 允许计划精确的闹钟(例如在指定时间触发某些任务)。-->
+ <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+ <!-- 允许写入外部存储(如 SD 卡中的文件)。-->
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <!-- 允许管理外部存储的文件,通常需要更高的权限。-->
+ <uses-permission
+ android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+ tools:ignore="ScopedStorage" />
+ <!-- 允许接收短信(SMS)。-->
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
+ <!-- 允许接收多媒体短信(MMS)。-->
+ <uses-permission android:name="android.permission.RECEIVE_MMS" />
+ <!-- 允许接收 WAP 推送消息。-->
+ <uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
+ <!-- 允许读取短信内容。-->
+ <uses-permission android:name="android.permission.READ_SMS" />
+ <!-- 允许发送短信-->
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <!-- 允许直接拨打电话。-->
+ <uses-permission android:name="android.permission.CALL_PHONE" />
+ <!-- 允许读取设备的电话状态(例如当前通话状态,设备是否有电话线等)。-->
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <!-- 允许读取设备的电话号码。-->
+ <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
+ <!-- 允许读取通话记录。-->
+ <uses-permission android:name="android.permission.READ_CALL_LOG" />
+ <!-- 允许读取联系人数据。-->
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <!-- 允许写入联系人数据。-->
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+ <!-- 允许访问帐户信息。-->
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <!--Android 9(API 级别 28)或更高版本并使用前台服务,则其必须请求 FOREGROUND_SERVICE 权限-->
+ <!-- 允许启动前台服务。-->
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <!-- 允许请求忽略电池优化,以确保后台任务的持续运行。-->
+ <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
+ <!-- 允许访问电池统计信息。-->
+ <uses-permission
+ android:name="android.permission.BATTERY_STATS"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许发布通知。-->
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <!-- 允许取消通知。-->
+ <uses-permission android:name="android.permission.CANCEL_NOTIFICATIONS " />
+ <!-- 允许应用绑定为通知监听服务。-->
+ <uses-permission
+ android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许跳转到通知监听设置界面-->
+ <uses-permission android:name="android.permission.ACTION_NOTIFICATION_LISTENER_SETTINGS" />
+ <!-- 允许应用安装或卸载其他应用。-->
+ <uses-permission
+ android:name="android.permission.INSTALL_PACKAGES"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许修改系统设置。-->
+ <uses-permission
+ android:name="android.permission.WRITE_SETTINGS"
+ tools:ignore="ProtectedPermissions" />
+ <!--进程杀死-->
+ <!-- 允许终止其他应用程序的后台进程。-->
+ <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+ <!-- 允许读取系统日志(受限权限)。-->
+ <uses-permission
+ android:name="android.permission.READ_LOGS"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许重启设备。-->
+ <uses-permission
+ android:name="android.permission.REBOOT"
+ tools:ignore="ProtectedPermissions" />
+ <!-- 允许访问蓝牙。-->
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <!-- 允许管理蓝牙设备(如打开、关闭蓝牙)。-->
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <!-- 允许连接蓝牙设备。-->
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <!-- 允许扫描蓝牙设备-->
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+ <!-- 允许广播蓝牙设备信息。-->
+ <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+
+ <application
+ android:name=".App"
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.FirstApp"
+ tools:targetApi="31">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/App.kt b/app/src/main/java/com/example/firstapp/App.kt
new file mode 100644
index 0000000..93f6053
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/App.kt
@@ -0,0 +1,455 @@
+package com.example.firstapp
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.app.PendingIntent
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.location.Geocoder
+import android.net.ConnectivityManager
+import android.net.wifi.WifiManager
+import android.os.Build
+import androidx.lifecycle.MutableLiveData
+import androidx.multidex.MultiDex
+import androidx.work.Configuration
+import androidx.work.WorkManager
+import com.gyf.cactus.Cactus
+import com.gyf.cactus.callback.CactusCallback
+import com.gyf.cactus.ext.cactus
+import com.hjq.language.MultiLanguages
+import com.hjq.language.OnLanguageListener
+import com.example.firstapp.core.Core
+import com.example.firstapp.database.repository.CodeRepository
+import com.example.firstapp.database.repository.MsgRepository
+import com.example.firstapp.receiver.CactusReceiver
+import com.example.firstapp.service.BluetoothScanService
+import com.example.firstapp.service.HttpServerService
+import com.example.firstapp.utils.ACTION_START
+import com.example.firstapp.utils.AppInfo
+import com.example.firstapp.utils.CactusSave
+import com.example.firstapp.utils.FRONT_CHANNEL_ID
+import com.example.firstapp.utils.FRONT_CHANNEL_NAME
+import com.example.firstapp.utils.FRONT_NOTIFY_ID
+import com.example.firstapp.utils.FRPC_LIB_VERSION
+import com.example.firstapp.utils.HistoryUtils
+import com.example.firstapp.utils.Log
+import com.example.firstapp.utils.SettingUtils
+import com.example.firstapp.utils.SharedPreference
+
+import com.example.firstapp.utils.tinker.TinkerLoadLibrary
+import com.king.location.LocationClient
+import com.xuexiang.xutil.file.FileUtils
+import frpclib.Frpclib
+import io.reactivex.Observable
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileWriter
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+import java.util.concurrent.TimeUnit
+
+@Suppress("DEPRECATION")
+class App : Application(), CactusCallback, Configuration.Provider by Core {
+
+ val applicationScope = CoroutineScope(SupervisorJob())
+ val database by lazy { AppDatabase.getInstance(this) }
+ val msgRepository by lazy { MsgRepository(database.msgDao()) }
+ val codeRepository by lazy { CodeRepository(database.codeDao()) }
+
+ companion object {
+ const val TAG: String = "SmsForwarder"
+
+ @SuppressLint("StaticFieldLeak")
+ lateinit var context: Context
+
+ //通话类型:1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
+ var CALL_TYPE_MAP: MutableMap<String, String> = mutableMapOf()
+ var FILED_MAP: MutableMap<String, String> = mutableMapOf()
+ var CHECK_MAP: MutableMap<String, String> = mutableMapOf()
+ var SIM_SLOT_MAP: MutableMap<String, String> = mutableMapOf()
+ var FORWARD_STATUS_MAP: MutableMap<Int, String> = mutableMapOf()
+ var BARK_LEVEL_MAP: MutableMap<String, String> = mutableMapOf()
+ var BARK_ENCRYPTION_ALGORITHM_MAP: MutableMap<String, String> = mutableMapOf()
+
+ //已插入SIM卡信息
+// var SimInfoList: MutableMap<Int, SimInfo> = mutableMapOf()
+
+ //已安装App信息
+ var LoadingAppList = false
+ var UserAppList: MutableList<AppInfo> = mutableListOf()
+ var SystemAppList: MutableList<AppInfo> = mutableListOf()
+
+ /**
+ * @return 当前app是否是调试开发模式
+ */
+// var isDebug: Boolean = BuildConfig.DEBUG
+ var isDebug: Boolean = true
+
+ //Cactus相关
+ val mEndDate = MutableLiveData<String>() //结束时间
+ val mLastTimer = MutableLiveData<String>() //上次存活时间
+ val mTimer = MutableLiveData<String>() //存活时间
+ val mStatus = MutableLiveData<Boolean>().apply { value = true } //运行状态
+ var mDisposable: Disposable? = null
+
+ //Location相关
+ val LocationClient by lazy { LocationClient(context) }
+ val Geocoder by lazy { Geocoder(context) }
+ val DateFormat by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) }
+
+ //Frpclib是否已经初始化
+ var FrpclibInited = false
+
+ //是否需要在拼接字符串时添加空格
+ var isNeedSpaceBetweenWords = false
+ }
+
+ override fun attachBaseContext(base: Context) {
+ //super.attachBaseContext(base)
+ // 绑定语种
+ super.attachBaseContext(MultiLanguages.attach(base))
+ //解决4.x运行崩溃的问题
+ MultiDex.install(this)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ // 设置全局异常捕获
+ val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+ Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
+ throwable.printStackTrace()
+ try {
+ val logPath = this.cacheDir.absolutePath + "/logs"
+ val logDir = File(logPath)
+ if (!logDir.exists()) logDir.mkdirs()
+ val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ val currentDateTime = dateFormat.format(Date())
+ val logFile = File(logPath, "crash_$currentDateTime.txt")
+ BufferedWriter(FileWriter(logFile, true)).use { writer ->
+ writer.append("$throwable\n")
+ }
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ }
+ //使用默认的处理方式让APP停止运行
+ defaultHandler?.uncaughtException(thread, throwable)
+ }
+
+ try {
+ context = applicationContext
+ initLibs()
+
+ //纯客户端模式
+ if (SettingUtils.enablePureClientMode) return
+
+ //初始化WorkManager
+ WorkManager.initialize(this, Configuration.Builder().build())
+
+ //动态加载FrpcLib
+ val libPath = filesDir.absolutePath + "/libs"
+ val soFile = File(libPath)
+ if (soFile.exists()) {
+ try {
+ TinkerLoadLibrary.installNativeLibraryPath(classLoader, soFile)
+ FrpclibInited = FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so") && FRPC_LIB_VERSION == Frpclib.getVersion()
+ } catch (throwable: Throwable) {
+ Log.e("APP", throwable.message.toString())
+ }
+ }
+
+ //启动前台服务
+// val foregroundServiceIntent = Intent(this, ForegroundService::class.java)
+// foregroundServiceIntent.action = ACTION_START
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+// startForegroundService(foregroundServiceIntent)
+// } else {
+// startService(foregroundServiceIntent)
+// }
+
+ //启动HttpServer
+// if (HttpServerUtils.enableServerAutorun) {
+// Intent(this, HttpServerService::class.java).also {
+// startService(it)
+// }
+// }
+
+ //启动LocationService
+// if (SettingUtils.enableLocation) {
+// val locationServiceIntent = Intent(this, LocationService::class.java)
+// locationServiceIntent.action = ACTION_START
+// startService(locationServiceIntent)
+// }
+
+ //监听电量&充电状态变化
+ /* val batteryReceiver = BatteryReceiver()
+ val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ registerReceiver(batteryReceiver, batteryFilter)*/
+
+ //监听蓝牙状态变化
+ /*val bluetoothReceiver = BluetoothReceiver()
+ val filter = IntentFilter().apply {
+ addAction(BluetoothDevice.ACTION_FOUND)
+ addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
+ addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
+ addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
+ addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)
+ addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
+ addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
+ addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
+ }
+ registerReceiver(bluetoothReceiver, filter)
+ if (SettingUtils.enableBluetooth) {
+ val bluetoothScanServiceIntent = Intent(this, BluetoothScanService::class.java)
+ bluetoothScanServiceIntent.action = ACTION_START
+ startService(bluetoothScanServiceIntent)
+ }*/
+
+ //监听网络变化
+ /* val networkReceiver = NetworkChangeReceiver()
+ val networkFilter = IntentFilter().apply {
+ addAction(ConnectivityManager.CONNECTIVITY_ACTION)
+ addAction(WifiManager.WIFI_STATE_CHANGED_ACTION)
+ addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION)
+ //addAction("android.intent.action.DATA_CONNECTION_STATE_CHANGED")
+ }
+ registerReceiver(networkReceiver, networkFilter)*/
+
+ //监听锁屏&解锁
+ /* val lockScreenReceiver = LockScreenReceiver()
+ val lockScreenFilter = IntentFilter().apply {
+ addAction(Intent.ACTION_SCREEN_OFF)
+ addAction(Intent.ACTION_SCREEN_ON)
+ addAction(Intent.ACTION_USER_PRESENT)
+ }
+ registerReceiver(lockScreenReceiver, lockScreenFilter)*/
+
+ //Cactus 集成双进程前台服务,JobScheduler,onePix(一像素),WorkManager,无声音乐
+ if (SettingUtils.enableCactus) {
+ //注册广播监听器
+ registerReceiver(CactusReceiver(), IntentFilter().apply {
+ addAction(Cactus.CACTUS_WORK)
+ addAction(Cactus.CACTUS_STOP)
+ addAction(Cactus.CACTUS_BACKGROUND)
+ addAction(Cactus.CACTUS_FOREGROUND)
+ })
+ //设置通知栏点击事件
+ val activityIntent = Intent(this, MainActivity::class.java)
+ val flags = if (Build.VERSION.SDK_INT >= 30) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
+ val pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, flags)
+ cactus {
+ setServiceId(FRONT_NOTIFY_ID) //服务Id
+ setChannelId(FRONT_CHANNEL_ID) //渠道Id
+ setChannelName(FRONT_CHANNEL_NAME) //渠道名
+ setTitle(getString(R.string.app_name))
+ setContent(SettingUtils.notifyContent)
+// setSmallIcon(R.drawable.ic_forwarder)
+ setLargeIcon(R.mipmap.ic_launcher)
+ setPendingIntent(pendingIntent)
+ //无声音乐
+ if (SettingUtils.enablePlaySilenceMusic) {
+ setMusicEnabled(true)
+ setBackgroundMusicEnabled(true)
+// setMusicId(R.raw.silence)
+ //设置音乐间隔时间,时间间隔越长,越省电
+ setMusicInterval(10)
+ isDebug(true)
+ }
+ //是否可以使用一像素,默认可以使用,只有在android p以下可以使用
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && SettingUtils.enableOnePixelActivity) {
+ setOnePixEnabled(true)
+ }
+ //奔溃是否可以重启用户界面
+ setCrashRestartUIEnabled(true)
+ addCallback({
+ Log.d(TAG, "Cactus保活:onStop回调")
+ }) {
+ Log.d(TAG, "Cactus保活:doWork回调")
+ }
+ //切后台切换回调
+ addBackgroundCallback {
+ Log.d(TAG, if (it) "SmsForwarder 切换到后台运行" else "SmsForwarder 切换到前台运行")
+ }
+ }
+ }
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.e(TAG, "onCreate: $e")
+ }
+ }
+
+ /**
+ * 初始化基础库
+ */
+ private fun initLibs() {
+ Core.init(this)
+ Log.init(applicationContext)
+ // 配置文件初始化
+ /* SharedPreference.init(applicationContext)
+ // X系列基础库初始化
+// XBasicLibInit.init(this)
+ // 初始化日志打印
+ isDebug = SettingUtils.enableDebugMode
+ Log.init(applicationContext)
+ // 转发历史工具类初始化
+ HistoryUtils.init(applicationContext)
+ // 版本更新初始化
+// XUpdateInit.init(this)
+ // 运营统计数据
+// UMengInit.init(this)
+ // 初始化语种切换框架
+ MultiLanguages.init(this)
+ // 设置语种变化监听器
+ MultiLanguages.setOnLanguageListener(object : OnLanguageListener {
+ override fun onAppLocaleChange(oldLocale: Locale, newLocale: Locale) {
+ // 注意:只有setAppLanguage时触发,clearAppLanguage时不触发
+ Log.i(TAG, "监听到应用切换了语种,旧语种:$oldLocale,新语种:$newLocale")
+ switchLanguage(newLocale)
+ }
+
+ override fun onSystemLocaleChange(oldLocale: Locale, newLocale: Locale) {
+ Log.i(TAG, "监听到系统切换了语种,旧语种:$oldLocale,新语种:$newLocale")
+ switchLanguage(newLocale)
+ *//*val isFlowSystem = SettingUtils.isFlowSystemLanguage //MultiLanguages.isSystemLanguage(context)取值不对,一直是false
+ Log.i(TAG, "监听到系统切换了语种,旧语种:$oldLocale,新语种:$newLocale,是否跟随系统:$isFlowSystem")
+ if (isFlowSystem) {
+ CommonUtils.switchLanguage(oldLocale, newLocale)
+ }*//*
+ }
+ })
+ switchLanguage(MultiLanguages.getAppLanguage(this))*/
+ }
+
+ @SuppressLint("CheckResult")
+ override fun doWork(times: Int) {
+ /* Log.d(TAG, "doWork:$times")
+ mStatus.postValue(true)
+ val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
+ dateFormat.timeZone = TimeZone.getTimeZone("GMT+00:00")
+ var oldTimer = CactusSave.timer
+ if (times == 1) {
+ CactusSave.lastTimer = oldTimer
+ CactusSave.endDate = CactusSave.date
+ oldTimer = 0L
+ }
+ mLastTimer.postValue(dateFormat.format(Date(CactusSave.lastTimer * 1000)))
+ mEndDate.postValue(CactusSave.endDate)
+ mDisposable = Observable.interval(1, TimeUnit.SECONDS).map {
+ oldTimer + it
+ }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe { aLong ->
+ CactusSave.timer = aLong
+ CactusSave.date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).run {
+ format(Date())
+ }
+ mTimer.value = dateFormat.format(Date(aLong * 1000))
+ }*/
+ }
+
+ override fun onStop() {
+ Log.d(TAG, "onStop")
+ mStatus.postValue(false)
+ mDisposable?.apply {
+ if (!isDisposed) {
+ dispose()
+ }
+ }
+ }
+
+ //多语言切换时枚举常量自动切换语言
+// private fun switchLanguage(newLocale: Locale) {
+// isNeedSpaceBetweenWords = !newLocale.language.contains("zh")
+//
+// CALL_TYPE_MAP.clear()
+// CALL_TYPE_MAP.putAll(
+// mapOf(
+// //"0" to getString(R.string.unknown_call),
+// "1" to getString(R.string.incoming_call_ended),
+// "2" to getString(R.string.outgoing_call_ended),
+// "3" to getString(R.string.missed_call),
+// "4" to getString(R.string.incoming_call_received),
+// "5" to getString(R.string.incoming_call_answered),
+// "6" to getString(R.string.outgoing_call_started),
+// )
+// )
+//
+// FILED_MAP.clear()
+// FILED_MAP.putAll(
+// mapOf(
+// "transpond_all" to getString(R.string.rule_transpond_all),
+// "phone_num" to getString(R.string.rule_phone_num),
+// "msg_content" to getString(R.string.rule_msg_content),
+// "multi_match" to getString(R.string.rule_multi_match),
+// "package_name" to getString(R.string.rule_package_name),
+// "inform_content" to getString(R.string.rule_inform_content),
+// "call_type" to getString(R.string.rule_call_type),
+// "uid" to getString(R.string.rule_uid),
+// )
+// )
+//
+// CHECK_MAP.clear()
+// CHECK_MAP.putAll(
+// mapOf(
+// "is" to getString(R.string.rule_is),
+// "notis" to getString(R.string.rule_notis),
+// "contain" to getString(R.string.rule_contain),
+// "startwith" to getString(R.string.rule_startwith),
+// "endwith" to getString(R.string.rule_endwith),
+// "notcontain" to getString(R.string.rule_notcontain),
+// "regex" to getString(R.string.rule_regex),
+// )
+// )
+//
+// SIM_SLOT_MAP.clear()
+// SIM_SLOT_MAP.putAll(
+// mapOf(
+// "ALL" to getString(R.string.rule_any),
+// "SIM1" to "SIM1",
+// "SIM2" to "SIM2",
+// )
+// )
+//
+// FORWARD_STATUS_MAP.clear()
+// FORWARD_STATUS_MAP.putAll(
+// mapOf(
+// 0 to getString(R.string.failed),
+// 1 to getString(R.string.processing),
+// 2 to getString(R.string.success),
+// )
+// )
+//
+// BARK_LEVEL_MAP.clear()
+// BARK_LEVEL_MAP.putAll(
+// mapOf(
+// "active" to getString(R.string.bark_level_active),
+// "timeSensitive" to getString(R.string.bark_level_timeSensitive),
+// "passive" to getString(R.string.bark_level_passive)
+// )
+// )
+//
+// BARK_ENCRYPTION_ALGORITHM_MAP.clear()
+// BARK_ENCRYPTION_ALGORITHM_MAP.putAll(
+// mapOf(
+// "none" to getString(R.string.bark_encryption_algorithm_none),
+// "AES128/CBC/PKCS7Padding" to "AES128/CBC/PKCS7Padding",
+// "AES128/ECB/PKCS7Padding" to "AES128/ECB/PKCS7Padding",
+// "AES192/CBC/PKCS7Padding" to "AES192/CBC/PKCS7Padding",
+// "AES192/ECB/PKCS7Padding" to "AES192/ECB/PKCS7Padding",
+// "AES256/CBC/PKCS7Padding" to "AES256/CBC/PKCS7Padding",
+// "AES256/ECB/PKCS7Padding" to "AES256/ECB/PKCS7Padding",
+// )
+// )
+// }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/MainActivity.kt b/app/src/main/java/com/example/firstapp/MainActivity.kt
new file mode 100644
index 0000000..9c9052d
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/MainActivity.kt
@@ -0,0 +1,131 @@
+package com.example.firstapp
+
+import android.content.IntentFilter
+import android.os.Bundle
+import android.provider.Telephony
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.navigation.findNavController
+import androidx.navigation.ui.AppBarConfiguration
+import androidx.navigation.ui.setupActionBarWithNavController
+import androidx.navigation.ui.setupWithNavController
+import com.example.firstapp.databinding.ActivityMainBinding
+import com.example.firstapp.receiver.SmsReceiver
+import android.Manifest
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.adapter.MyAdapter
+import com.example.firstapp.core.Core
+import com.example.firstapp.entity.Item
+import com.example.firstapp.ui.home.HomeViewModel
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityMainBinding
+
+ private var smsReceiver:SmsReceiver? = null
+
+ private lateinit var adapter: MyAdapter
+ private lateinit var homeViewModel: HomeViewModel
+
+
+ // 权限请求代码
+ private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ if (isGranted) {
+ // 权限授予后注册短信监听器
+ registerSmsReceiver()
+ } else {
+ // 权限拒绝,提示用户
+ Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ // 在此位置初始化 homeViewModel
+ homeViewModel = ViewModelProvider(this).get(HomeViewModel::class.java)
+
+ val navView: BottomNavigationView = binding.navView
+
+ val navController = findNavController(R.id.nav_host_fragment_activity_main)
+ // Passing each menu ID as a set of Ids because each
+ // menu should be considered as top level destinations.
+ val appBarConfiguration = AppBarConfiguration(
+ setOf(
+ R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
+ )
+ )
+ setupActionBarWithNavController(navController, appBarConfiguration)
+ navView.setupWithNavController(navController)
+
+ // 检查权限
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ // 请求权限
+ permissionRequest.launch(Manifest.permission.READ_SMS)
+ } else {
+ // 权限已经授予,继续执行相关操作
+ registerSmsReceiver()
+ }
+
+
+ // 模拟数据
+// val items = listOf(
+// Item(1, "First Item", "This is the first item description"),
+// Item(2, "Second Item", "This is the second item description"),
+// Item(3, "Third Item", "This is the third item description")
+// )
+//
+// val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
+// recyclerView.layoutManager = LinearLayoutManager(this)
+// recyclerView.adapter = MyAdapter(items)
+
+// var codeList = Core.code.getAllDesc()
+ val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
+ recyclerView.layoutManager = LinearLayoutManager(this)
+// recyclerView.adapter = MyAdapter(codeList)
+
+ // 初始化适配器
+ adapter = MyAdapter()
+ recyclerView.adapter = adapter
+
+ // 观察 LiveData 数据
+ homeViewModel.codeList.observe(this) { codeList ->
+ // 如果 codeList 为 null,避免闪退
+ if (codeList != null) {
+ adapter.submitList(codeList)
+ // 滚动到顶部
+ recyclerView.scrollToPosition(0)
+ } else {
+ // 如果数据为空,可以显示空列表或其他处理
+ Toast.makeText(this, "No data available", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // 注册广播接收器来监听数据更新
+ val filter = IntentFilter("com.example.firstapp.DATA_UPDATED")
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ // 数据已更新,刷新 LiveData
+ homeViewModel.loadData()
+ }
+ }, filter)
+
+ }
+
+ private fun registerSmsReceiver() {
+ smsReceiver = SmsReceiver()
+ val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)
+ registerReceiver(smsReceiver, filter)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/adapter/MyAdapter.kt b/app/src/main/java/com/example/firstapp/adapter/MyAdapter.kt
new file mode 100644
index 0000000..d15ce6a
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/adapter/MyAdapter.kt
@@ -0,0 +1,42 @@
+package com.example.firstapp.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.R
+import com.example.firstapp.database.entity.Code
+import androidx.recyclerview.widget.ListAdapter
+
+
+class MyAdapter : ListAdapter<Code, MyAdapter.ViewHolder>(CodeDiffCallback()) {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val tvTitle: TextView = view.findViewById(R.id.tvTitle)
+ val tvDescription: TextView = view.findViewById(R.id.tvDescription)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_layout, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = getItem(position) // 使用 getItem 来获取当前位置的 item
+ holder.tvTitle.text = item.type // 假设 Code 类有一个 `type` 属性
+ holder.tvDescription.text = item.code // 假设 Code 类有一个 `code` 属性
+ }
+
+ // 使用 DiffUtil 来优化列表更新
+ class CodeDiffCallback : androidx.recyclerview.widget.DiffUtil.ItemCallback<Code>() {
+ override fun areItemsTheSame(oldItem: Code, newItem: Code): Boolean {
+ return oldItem.id == newItem.id // 假设 Code 类有唯一的 id 字段
+ }
+
+ override fun areContentsTheSame(oldItem: Code, newItem: Code): Boolean {
+ return oldItem == newItem // 如果内容相同,返回 true
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/adapter/MyAdapter2.kt b/app/src/main/java/com/example/firstapp/adapter/MyAdapter2.kt
new file mode 100644
index 0000000..caea796
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/adapter/MyAdapter2.kt
@@ -0,0 +1,32 @@
+package com.example.firstapp.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.R
+import com.example.firstapp.entity.Item
+
+class MyAdapter2(private val items: List<Item>) :
+ RecyclerView.Adapter<MyAdapter2.ViewHolder>() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val tvTitle: TextView = view.findViewById(R.id.tvTitle)
+ val tvDescription: TextView = view.findViewById(R.id.tvDescription)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_layout, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.tvTitle.text = item.title
+ holder.tvDescription.text = item.description
+ }
+
+ override fun getItemCount() = items.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/adapter/MyAdapter_bak.kt b/app/src/main/java/com/example/firstapp/adapter/MyAdapter_bak.kt
new file mode 100644
index 0000000..33404e9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/adapter/MyAdapter_bak.kt
@@ -0,0 +1,33 @@
+package com.example.firstapp.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.R
+import com.example.firstapp.database.entity.Code
+import com.example.firstapp.entity.Item
+
+class MyAdapter_bak(private val items: List<Code>) :
+ RecyclerView.Adapter<MyAdapter_bak.ViewHolder>() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val tvTitle: TextView = view.findViewById(R.id.tvTitle)
+ val tvDescription: TextView = view.findViewById(R.id.tvDescription)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_layout, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.tvTitle.text = item.type
+ holder.tvDescription.text = item.code
+ }
+
+ override fun getItemCount() = items.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/core/Core.kt b/app/src/main/java/com/example/firstapp/core/Core.kt
new file mode 100644
index 0000000..f1f55b4
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/core/Core.kt
@@ -0,0 +1,29 @@
+package com.example.firstapp.core
+
+import android.app.Application
+import androidx.work.Configuration
+import com.example.firstapp.App
+import com.example.firstapp.database.repository.CodeRepository
+import com.example.firstapp.database.repository.MsgRepository
+
+import kotlinx.coroutines.launch
+
+object Core : Configuration.Provider {
+ lateinit var app: Application
+
+ val msg: MsgRepository by lazy { (app as App).msgRepository }
+ val code: CodeRepository by lazy { (app as App).codeRepository }
+
+ fun init(app: Application) {
+ this.app = app
+ }
+
+ override fun getWorkManagerConfiguration(): Configuration {
+ return Configuration.Builder().apply {
+ setDefaultProcessName(app.packageName + ":bg")
+// setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
+ setExecutor { (app as App).applicationScope.launch { it.run() } }
+ setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
+ }.build()
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/database/AppDatabase.kt b/app/src/main/java/com/example/firstapp/database/AppDatabase.kt
new file mode 100644
index 0000000..42a1399
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/AppDatabase.kt
@@ -0,0 +1,104 @@
+package com.example.firstapp
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.example.firstapp.database.dao.CodeDao
+import com.example.firstapp.database.dao.MsgDao
+import com.example.firstapp.database.entity.Code
+import com.example.firstapp.database.entity.Msg
+import com.example.firstapp.utils.DATABASE_NAME
+import com.example.firstapp.utils.SettingUtils
+import com.example.firstapp.utils.TAG_LIST
+
+import com.example.firstapp.database.ext.ConvertersDate
+
+
+
+@Database(
+ entities = [ Msg::class, Code::class],
+// views = [LogsDetail::class],
+ version = 19,
+ exportSchema = false
+)
+@TypeConverters(ConvertersDate::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun msgDao(): MsgDao
+ abstract fun codeDao(): CodeDao
+
+ companion object {
+ @Volatile
+ private var instance: AppDatabase? = null
+
+ fun getInstance(context: Context): AppDatabase {
+ return instance ?: synchronized(this) {
+ instance ?: buildDatabase(context).also { instance = it }
+ }
+ }
+
+ private fun buildDatabase(context: Context): AppDatabase {
+ val builder = Room.databaseBuilder(
+ context.applicationContext, AppDatabase::class.java, DATABASE_NAME
+ ).allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
+ .addCallback(object : Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+
+ }
+ }).addMigrations(
+
+ MIGRATION_MSG,
+ )
+
+ /*if (BuildConfig.DEBUG) {
+ builder.setQueryCallback({ sqlQuery, bindArgs ->
+ println("SQL_QUERY: $sqlQuery\nBIND_ARGS: $bindArgs")
+ }, Executors.newSingleThreadExecutor())
+ }*/
+
+ return builder.build()
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private val MIGRATION_MSG = object : Migration(19, 20) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ //database.execSQL("Create table Msg as Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
+ database.execSQL(
+ """
+CREATE TABLE "Msg" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" TEXT NOT NULL DEFAULT 'sms',
+ "from" TEXT NOT NULL DEFAULT '',
+ "content" TEXT NOT NULL DEFAULT '',
+ "sim_slot" INTEGER NOT NULL DEFAULT -1,
+ "sim_info" TEXT NOT NULL DEFAULT '',
+ "sub_id" INTEGER NOT NULL DEFAULT 0,
+ "time" INTEGER NOT NULL
+)
+""".trimIndent()
+ )
+
+ database.execSQL("CREATE UNIQUE INDEX \"index_Msg_id\" ON \"Msg\" ( \"id\" ASC)")
+
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/example/firstapp/database/AppDatabase_bak.kt b/app/src/main/java/com/example/firstapp/database/AppDatabase_bak.kt
new file mode 100644
index 0000000..0332822
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/AppDatabase_bak.kt
@@ -0,0 +1,442 @@
+package com.example.firstapp
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.example.firstapp.database.dao.MsgDao
+import com.example.firstapp.database.entity.Msg
+import com.example.firstapp.utils.DATABASE_NAME
+import com.example.firstapp.utils.SettingUtils
+import com.example.firstapp.utils.TAG_LIST
+
+import com.example.firstapp.database.ext.ConvertersDate
+
+
+
+@Database(
+ entities = [ Msg::class],
+// views = [LogsDetail::class],
+ version = 19,
+ exportSchema = false
+)
+@TypeConverters(ConvertersDate::class)
+abstract class AppDatabase_bak : RoomDatabase() {
+ abstract fun msgDao(): MsgDao
+ companion object {
+ @Volatile
+ private var instance: AppDatabase_bak? = null
+
+ fun getInstance(context: Context): AppDatabase_bak {
+ return instance ?: synchronized(this) {
+ instance ?: buildDatabase(context).also { instance = it }
+ }
+ }
+
+ private fun buildDatabase(context: Context): AppDatabase_bak {
+ val builder = Room.databaseBuilder(
+ context.applicationContext, AppDatabase_bak::class.java, DATABASE_NAME
+ ).allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
+ .addCallback(object : Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ //fillInDb(context.applicationContext)
+ db.execSQL(
+ """
+INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '[common]
+#frps服务端公网IP
+server_addr = 88.88.88.88
+#frps服务端公网端口
+server_port = 8888
+#可选,建议启用
+token = 88888888
+#连接服务端的超时时间(增大时间避免frpc在网络未就绪的情况下启动失败)
+dial_server_timeout = 60
+#第一次登陆失败后是否退出
+login_fail_exit = false
+
+#[二选一即可]每台机器不可重复,通过 http://88.88.88.88:5000 访问
+[SmsForwarder-TCP]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 5000
+#只要修改下面这一行(frps所在服务器必须暴露的公网端口)
+remote_port = 5000
+
+#[二选一即可]每台机器不可重复,通过 http://smsf.demo.com 访问
+[SmsForwarder-HTTP]
+type = http
+local_ip = 127.0.0.1
+local_port = 5000
+#只要修改下面这一行(在frps端将域名反代到vhost_http_port)
+custom_domains = smsf.demo.com
+', 0, '1651334400000')
+""".trimIndent()
+ )
+ }
+ }).addMigrations(
+ MIGRATION_1_2,
+ MIGRATION_2_3,
+ MIGRATION_3_4,
+ MIGRATION_4_5,
+ MIGRATION_5_6,
+ MIGRATION_6_7,
+ MIGRATION_7_8,
+ MIGRATION_8_9,
+ MIGRATION_9_10,
+ MIGRATION_10_11,
+ MIGRATION_11_12,
+ MIGRATION_12_13,
+ MIGRATION_13_14,
+ MIGRATION_14_15,
+ MIGRATION_15_16,
+ MIGRATION_16_17,
+ MIGRATION_17_18,
+ MIGRATION_18_19,
+ )
+
+ /*if (BuildConfig.DEBUG) {
+ builder.setQueryCallback({ sqlQuery, bindArgs ->
+ println("SQL_QUERY: $sqlQuery\nBIND_ARGS: $bindArgs")
+ }, Executors.newSingleThreadExecutor())
+ }*/
+
+ return builder.build()
+ }
+
+ //转发日志添加SIM卡槽信息
+ private val MIGRATION_1_2 = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table log add column sim_info TEXT ")
+ }
+ }
+
+ //转发规则添加SIM卡槽信息
+ private val MIGRATION_2_3 = object : Migration(2, 3) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column sim_slot TEXT NOT NULL DEFAULT 'ALL' ")
+ }
+ }
+
+ //转发日志添加转发状态与返回信息
+ private val MIGRATION_3_4 = object : Migration(3, 4) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table log add column forward_status INTEGER NOT NULL DEFAULT 1 ")
+ database.execSQL("Alter table log add column forward_response TEXT NOT NULL DEFAULT 'ok' ")
+ }
+ }
+
+ //转发规则添加规则自定义信息模板
+ private val MIGRATION_4_5 = object : Migration(4, 5) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column sms_template TEXT NOT NULL DEFAULT '' ")
+ }
+ }
+
+ //增加转发规则与日志的分类
+ private val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column type TEXT NOT NULL DEFAULT 'sms' ")
+ database.execSQL("Alter table log add column type TEXT NOT NULL DEFAULT 'sms' ")
+ }
+ }
+
+ //转发规则添加正则替换内容
+ private val MIGRATION_6_7 = object : Migration(6, 7) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column regex_replace TEXT NOT NULL DEFAULT '' ")
+ }
+ }
+
+ //更新日志表状态:0=失败,1=待处理,2=成功
+ private val MIGRATION_7_8 = object : Migration(7, 8) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("update log set forward_status = 2 where forward_status = 1 ")
+ }
+ }
+
+ //规则/通道状态:0=禁用,1=启用
+ private val MIGRATION_8_9 = object : Migration(8, 9) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column status INTEGER NOT NULL DEFAULT 1 ")
+ database.execSQL("update sender set status = 1 ")
+ }
+ }
+
+ //从SQLite迁移到 Room
+ private val MIGRATION_9_10 = object : Migration(9, 10) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ """
+CREATE TABLE "Frpc" (
+ "uid" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "config" TEXT NOT NULL,
+ "autorun" INTEGER NOT NULL DEFAULT 0,
+ "time" INTEGER NOT NULL,
+ PRIMARY KEY ("uid")
+)
+""".trimIndent()
+ )
+ database.execSQL(
+ """
+INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '
+#frps服务端公网IP
+serverAddr = "88.88.88.88"
+#frps服务端公网端口
+serverPort = 8888
+#连接服务端的超时时间(增大时间避免frpc在网络未就绪的情况下启动失败)
+transport.dialServerTimeout = 60
+#第一次登陆失败后是否退出
+loginFailExit = false
+#可选,建议启用
+auth.method = "token"
+auth.token = "88888888"
+
+#[二选一即可]每台机器的 name 和 remotePort 不可重复,通过 http://88.88.88.88:5000 访问
+[[proxies]]
+#同一个frps下,多台设备的 name 不可重复
+name = "SmsForwarder-TCP-001"
+type = "tcp"
+localIP = "127.0.0.1"
+localPort = 5000
+#只要修改下面这一行(frps所在服务器必须暴露且防火墙放行的公网端口,同一个frps下不可重复)
+remotePort = 5000
+
+#[二选一即可]每台机器的 name 和 customDomains 不可重复,通过 http://smsf.demo.com 访问
+[[proxies]]
+#同一个frps下,多台设备的 name 不可重复
+name = "SmsForwarder-HTTP-001"
+type = "http"
+localPort = 5000
+#只要修改下面这一行(在frps端将域名反代到vhost_http_port)
+customDomains = ["smsf.demo.com"]
+
+', 0, '1651334400000')
+""".trimIndent()
+ )
+
+ database.execSQL("ALTER TABLE log RENAME TO old_log")
+ database.execSQL(
+ """
+CREATE TABLE "Logs" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" TEXT NOT NULL DEFAULT 'sms',
+ "from" TEXT NOT NULL DEFAULT '',
+ "content" TEXT NOT NULL DEFAULT '',
+ "rule_id" INTEGER NOT NULL DEFAULT 0,
+ "sim_info" TEXT NOT NULL DEFAULT '',
+ "forward_status" INTEGER NOT NULL DEFAULT 1,
+ "forward_response" TEXT NOT NULL DEFAULT '',
+ "time" INTEGER NOT NULL,
+ FOREIGN KEY ("rule_id") REFERENCES "Rule" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+)
+""".trimIndent()
+ )
+ database.execSQL("CREATE UNIQUE INDEX \"index_Log_id\" ON \"Logs\" ( \"id\" ASC)")
+ database.execSQL("CREATE INDEX \"index_Log_rule_id\" ON \"Logs\" ( \"rule_id\" ASC)")
+ database.execSQL("INSERT INTO Logs (id,type,`from`,content,sim_info,rule_id,forward_status,forward_response,time) SELECT _id,type,l_from,content,sim_info,rule_id,forward_status,forward_response,strftime('%s000',time) FROM old_log")
+ database.execSQL("DROP TABLE old_log")
+
+ database.execSQL("ALTER TABLE rule RENAME TO old_rule")
+ database.execSQL(
+ """
+CREATE TABLE "Rule" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" TEXT NOT NULL DEFAULT 'sms',
+ "filed" TEXT NOT NULL DEFAULT 'transpond_all',
+ "check" TEXT NOT NULL DEFAULT 'is',
+ "value" TEXT NOT NULL DEFAULT '',
+ "sender_id" INTEGER NOT NULL DEFAULT 0,
+ "sms_template" TEXT NOT NULL DEFAULT '',
+ "regex_replace" TEXT NOT NULL DEFAULT '',
+ "sim_slot" TEXT NOT NULL DEFAULT 'ALL',
+ "status" INTEGER NOT NULL DEFAULT 1,
+ "time" INTEGER NOT NULL,
+ FOREIGN KEY ("sender_id") REFERENCES "Sender" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+)
+""".trimIndent()
+ )
+ database.execSQL("CREATE UNIQUE INDEX \"index_Rule_id\" ON \"Rule\" ( \"id\" ASC)")
+ database.execSQL("CREATE INDEX \"index_Rule_sender_id\" ON \"Rule\" ( \"sender_id\" ASC)")
+ database.execSQL("INSERT INTO Rule (id,type,filed,`check`,value,sender_id,time,sms_template,regex_replace,status,sim_slot) SELECT _id,type,filed,tcheck,value,sender_id,strftime('%s000',time),sms_template,regex_replace,status,sim_slot FROM old_rule")
+ database.execSQL("DROP TABLE old_rule")
+
+ database.execSQL("ALTER TABLE sender RENAME TO old_sender")
+ database.execSQL(
+ """
+CREATE TABLE "Sender" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" INTEGER NOT NULL DEFAULT 1,
+ "name" TEXT NOT NULL DEFAULT '',
+ "json_setting" TEXT NOT NULL DEFAULT '',
+ "status" INTEGER NOT NULL DEFAULT 1,
+ "time" INTEGER NOT NULL
+)
+""".trimIndent()
+ )
+ database.execSQL("INSERT INTO Sender (id,name,status,type,json_setting,time) SELECT _id,name,status,type,json_setting,strftime('%s000',time) FROM old_sender")
+ database.execSQL("DROP TABLE old_sender")
+ }
+ }
+
+ //转发日志添加SIM卡槽ID
+ private val MIGRATION_10_11 = object : Migration(10, 11) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table Logs add column sub_id INTEGER NOT NULL DEFAULT 0")
+ }
+ }
+
+ //单个转发规则可绑定多个发送通道
+ private val MIGRATION_11_12 = object : Migration(11, 12) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table Logs add column sender_id INTEGER NOT NULL DEFAULT 0")
+ database.execSQL("Update Logs Set sender_id = (Select sender_id from Rule where Logs.rule_id = Rule.id)")
+ database.execSQL("Alter table Rule add column sender_list TEXT NOT NULL DEFAULT ''")
+ database.execSQL("Update Rule set sender_list = sender_id")
+ database.execSQL("CREATE INDEX \"index_Rule_sender_ids\" ON \"Rule\" ( \"sender_list\" ASC)")
+ //删除字段:sender_id
+ /*database.execSQL("Create table Rule_t as Select id,type,filed,check,value,sender_list,sms_template,regex_replace,sim_slot,status,time from Rule where 1 = 1")
+ database.execSQL("Drop table Rule")
+ database.execSQL("Alter table Rule_t rename to Rule")
+ database.execSQL("CREATE UNIQUE INDEX \"index_Rule_id\" ON \"Rule\" ( \"id\" ASC)")*/
+ }
+ }
+
+ //转发规则添加发送通道逻辑
+ private val MIGRATION_12_13 = object : Migration(12, 13) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table Rule add column sender_logic TEXT NOT NULL DEFAULT 'ALL'")
+ }
+ }
+
+ //分割Logs表
+ private val MIGRATION_13_14 = object : Migration(13, 14) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ //database.execSQL("Create table Msg as Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
+ database.execSQL(
+ """
+CREATE TABLE "Msg" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" TEXT NOT NULL DEFAULT 'sms',
+ "from" TEXT NOT NULL DEFAULT '',
+ "content" TEXT NOT NULL DEFAULT '',
+ "sim_slot" INTEGER NOT NULL DEFAULT -1,
+ "sim_info" TEXT NOT NULL DEFAULT '',
+ "sub_id" INTEGER NOT NULL DEFAULT 0,
+ "time" INTEGER NOT NULL
+)
+""".trimIndent()
+ )
+ database.execSQL("INSERT INTO Msg (id,type,`from`,content,sim_slot,sim_info,sub_id,time) Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
+ database.execSQL("CREATE UNIQUE INDEX \"index_Msg_id\" ON \"Msg\" ( \"id\" ASC)")
+ database.execSQL("ALTER TABLE Logs RENAME TO Logs_old")
+ //database.execSQL("Create table Logs_new as Select id,id as msg_id,rule_id,sender_id,forward_status,forward_response,time from Logs where 1 = 1")
+ database.execSQL(
+ """
+CREATE TABLE "Logs" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" TEXT NOT NULL DEFAULT 'sms',
+ "msg_id" INTEGER NOT NULL DEFAULT 0,
+ "rule_id" INTEGER NOT NULL DEFAULT 0,
+ "sender_id" INTEGER NOT NULL DEFAULT 0,
+ "forward_status" INTEGER NOT NULL DEFAULT 1,
+ "forward_response" TEXT NOT NULL DEFAULT '',
+ "time" INTEGER NOT NULL,
+ FOREIGN KEY ("msg_id") REFERENCES "Msg" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY ("rule_id") REFERENCES "Rule" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY ("sender_id") REFERENCES "Sender" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+""".trimIndent()
+ )
+ database.execSQL("INSERT INTO Logs (id,type,msg_id,rule_id,sender_id,forward_status,forward_response,time) SELECT id,type,id as msg_id,rule_id,sender_id,forward_status,forward_response,time FROM Logs_old")
+ database.execSQL("DROP TABLE Logs_old")
+ database.execSQL("CREATE UNIQUE INDEX \"index_Logs_id\" ON \"Logs\" ( \"id\" ASC)")
+ database.execSQL("CREATE INDEX \"index_Logs_msg_id\" ON \"Logs\" ( \"msg_id\" ASC)")
+ database.execSQL("CREATE INDEX \"index_Logs_rule_id\" ON \"Logs\" ( \"rule_id\" ASC)")
+ database.execSQL("CREATE INDEX \"index_Logs_sender_id\" ON \"Logs\" ( \"sender_id\" ASC)")
+ }
+ }
+
+ // 定义数据库迁移配置
+ private val MIGRATION_14_15 = object : Migration(14, 15) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ // 这里新建一个视图(视图名称要用两个半角的间隔号括起来)
+ database.execSQL("CREATE VIEW `LogsDetail` AS SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")
+ }
+ }
+
+ //免打扰(禁用转发)时间段
+ private val MIGRATION_15_16 = object : Migration(15, 16) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table rule add column silent_period_start INTEGER NOT NULL DEFAULT 0 ")
+ database.execSQL("Alter table rule add column silent_period_end INTEGER NOT NULL DEFAULT 0 ")
+ }
+ }
+
+ //通话类型:1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
+ private val MIGRATION_16_17 = object : Migration(16, 17) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("Alter table Msg add column call_type INTEGER NOT NULL DEFAULT 0")
+ }
+ }
+
+ //自动化任务
+ private val MIGRATION_17_18 = object : Migration(17, 18) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ """
+CREATE TABLE "Task" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "type" INTEGER NOT NULL DEFAULT 1,
+ "name" TEXT NOT NULL DEFAULT '',
+ "description" TEXT NOT NULL DEFAULT '',
+ "conditions" TEXT NOT NULL DEFAULT '',
+ "actions" TEXT NOT NULL DEFAULT '',
+ "last_exec_time" INTEGER NOT NULL,
+ "next_exec_time" INTEGER NOT NULL,
+ "status" INTEGER NOT NULL DEFAULT 1
+)
+""".trimIndent()
+ )
+ }
+ }
+
+ //自定义模板可用变量统一成英文标签
+ private val MIGRATION_18_19 = object : Migration(18, 19) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ //替换自定义模板标签
+ var smsTemplate = SettingUtils.smsTemplate
+ //替换Rule.sms_template中的标签
+ var ruleColumnCN = "sms_template"
+ var ruleColumnTW = "sms_template"
+ //替换Sender.json_setting中的标签
+ var senderColumnCN = "json_setting"
+ var senderColumnTW = "json_setting"
+
+ for (i in TAG_LIST.indices) {
+ val tagCN = TAG_LIST[i]["zh_CN"].toString()
+ val tagTW = TAG_LIST[i]["zh_TW"].toString()
+ val tagEN = TAG_LIST[i]["en"].toString()
+ smsTemplate = smsTemplate.replace(tagCN, tagEN)
+ ruleColumnCN = "REPLACE($ruleColumnCN, '$tagCN', '$tagEN')"
+ ruleColumnTW = "REPLACE($ruleColumnTW, '$tagTW', '$tagEN')"
+ senderColumnCN = "REPLACE($senderColumnCN, '$tagCN', '$tagEN')"
+ senderColumnTW = "REPLACE($senderColumnTW, '$tagTW', '$tagEN')"
+ }
+
+ database.execSQL("UPDATE Rule SET sms_template = $ruleColumnCN WHERE sms_template != ''")
+ database.execSQL("UPDATE Rule SET sms_template = $ruleColumnTW WHERE sms_template != ''")
+
+ database.execSQL("UPDATE Sender SET json_setting = $senderColumnCN WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
+ database.execSQL("UPDATE Sender SET json_setting = $senderColumnTW WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
+
+ SettingUtils.smsTemplate = smsTemplate
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt
new file mode 100644
index 0000000..8df5a2b
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/dao/CodeDao.kt
@@ -0,0 +1,44 @@
+package com.example.firstapp.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import com.example.firstapp.database.entity.Code
+import com.example.firstapp.database.entity.Msg
+import io.reactivex.Completable
+
+@Dao
+interface CodeDao {
+
+ @Insert
+ fun insert(code: Code): Long
+
+ @Update
+ fun update(code: Code)
+
+ @Delete
+ fun delete(msg: Msg): Completable
+
+ @Query("DELETE FROM Code where id=:id")
+ fun delete(id: Long)
+
+
+ @Query("SELECT * FROM Code WHERE id = :id LIMIT 1")
+ fun getCodeById(id: Long): Code?
+
+ @Query("SELECT * FROM Code")
+ fun getAllCodes(): List<Code>
+
+ @Query("SELECT * FROM Code WHERE type = :type")
+ fun getCodesByType(type: String): List<Code>
+
+
+
+ @Query("DELETE FROM Code WHERE id = :id")
+ fun deleteCodeById(id: Long)
+
+ @Query("SELECT * FROM Code order by time desc")
+ abstract fun getAllCodesDesc(): List<Code>
+}
diff --git a/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt b/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt
new file mode 100644
index 0000000..5d54b7b
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/dao/MsgDao.kt
@@ -0,0 +1,57 @@
+package com.example.firstapp.database.dao
+
+
+import androidx.paging.PagingSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.RawQuery
+import androidx.room.Transaction
+import androidx.room.Update
+import androidx.sqlite.db.SupportSQLiteQuery
+import com.example.firstapp.database.entity.Msg
+
+import io.reactivex.Completable
+import io.reactivex.Single
+
+@Dao
+interface MsgDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ fun insert(msg: Msg): Long
+
+ @Delete
+ fun delete(msg: Msg): Completable
+
+ @Query("DELETE FROM Msg where id=:id")
+ fun delete(id: Long)
+
+ @RawQuery
+ fun deleteAll(sql: SupportSQLiteQuery): Int
+
+ @Query("DELETE FROM Msg")
+ fun deleteAll()
+
+ @Query("DELETE FROM Msg where time<:time")
+ fun deleteTimeAgo(time: Long)
+
+ @Update
+ fun update(msg: Msg): Completable
+
+ @Query("SELECT * FROM Msg where id=:id")
+ fun get(id: Long): Single<Msg>
+
+ @Query("SELECT count(*) FROM Msg where type=:type")
+ fun count(type: String): Single<Int>
+
+// @Transaction
+// @Query("SELECT * FROM Msg WHERE type = :type ORDER BY id DESC")
+// fun pagingSource(type: String): PagingSource<Int, MsgAndLogs>
+//
+// @Transaction
+// @RawQuery(observedEntities = [MsgAndLogs::class])
+// fun pagingSource(query: SupportSQLiteQuery): PagingSource<Int, MsgAndLogs>
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/entity/Code.kt b/app/src/main/java/com/example/firstapp/database/entity/Code.kt
new file mode 100644
index 0000000..24f5223
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/entity/Code.kt
@@ -0,0 +1,18 @@
+package com.example.firstapp.database.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.util.*
+
+@Entity(tableName = "Code")
+data class Code(
+ @PrimaryKey(autoGenerate = true) val id: Long = 0, // 自增长的 id
+ val category: String,
+ val categoryId: Long,
+ val type: String,
+ val typeId: Long,
+ val ruleId: Long,
+ val msgId: Long,
+ val code: String,
+ var time: Date = Date(),
+)
diff --git a/app/src/main/java/com/example/firstapp/database/entity/Msg.kt b/app/src/main/java/com/example/firstapp/database/entity/Msg.kt
new file mode 100644
index 0000000..ac5caf9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/entity/Msg.kt
@@ -0,0 +1,45 @@
+package com.example.firstapp.database.entity
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import com.example.firstapp.R
+import kotlinx.parcelize.Parcelize
+import java.util.Date
+
+@Parcelize
+@Entity(
+ tableName = "Msg",
+ indices = [
+ Index(value = ["id"], unique = true)
+ ]
+)
+data class Msg(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id") var id: Long,
+ @ColumnInfo(name = "type", defaultValue = "sms") var type: String,
+ @ColumnInfo(name = "from", defaultValue = "") var from: String,
+ @ColumnInfo(name = "content", defaultValue = "") var content: String,
+ @ColumnInfo(name = "sim_slot", defaultValue = "-1") var simSlot: Int = -1, //卡槽id:-1=获取失败、0=卡槽1、1=卡槽2
+ @ColumnInfo(name = "sim_info", defaultValue = "") var simInfo: String = "",
+ @ColumnInfo(name = "sub_id", defaultValue = "0") var subId: Int = 0,
+ //通话类型:1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
+ @ColumnInfo(name = "call_type", defaultValue = "0") var callType: Int = 0,
+ @ColumnInfo(name = "time") var time: Date = Date(),
+) : Parcelable {
+
+ val simImageId: Int
+ get() {
+ return when {
+ type == "app" -> R.drawable.ic_app
+ simSlot == 0 -> R.drawable.ic_sim1
+ simSlot == 1 -> R.drawable.ic_sim2
+ simInfo.isNotEmpty() && simInfo.replace("-", "").startsWith("SIM2") -> R.drawable.ic_sim2
+ simInfo.isNotEmpty() && simInfo.replace("-", "").startsWith("SIM1") -> R.drawable.ic_sim1
+ else -> R.drawable.ic_sim
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/ext/ConvertersDate.kt b/app/src/main/java/com/example/firstapp/database/ext/ConvertersDate.kt
new file mode 100644
index 0000000..14732bf
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/ext/ConvertersDate.kt
@@ -0,0 +1,16 @@
+package com.example.firstapp.database.ext
+
+import androidx.room.TypeConverter
+import java.util.Date
+
+class ConvertersDate {
+ @TypeConverter
+ fun fromTimestamp(value: Long?): Date? {
+ return value?.let { Date(it) }
+ }
+
+ @TypeConverter
+ fun dateToTimestamp(date: Date?): Long? {
+ return date?.time
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt
new file mode 100644
index 0000000..2d9eceb
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/repository/CodeRepository.kt
@@ -0,0 +1,21 @@
+package com.example.firstapp.database.repository
+
+import androidx.annotation.WorkerThread
+import com.example.firstapp.database.dao.CodeDao
+import com.example.firstapp.database.entity.Code
+
+
+class CodeRepository(private val codeDao: CodeDao) {
+
+ @WorkerThread
+ fun insert(code: Code): Long = codeDao.insert(code)
+
+ @WorkerThread
+ fun delete(id: Long) = codeDao.delete(id)
+
+ fun getAll() = codeDao.getAllCodes()
+
+ fun getAllDesc() = codeDao.getAllCodesDesc()
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/database/repository/MsgRepository.kt b/app/src/main/java/com/example/firstapp/database/repository/MsgRepository.kt
new file mode 100644
index 0000000..8d32cd3
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/database/repository/MsgRepository.kt
@@ -0,0 +1,21 @@
+package com.example.firstapp.database.repository
+
+import androidx.annotation.WorkerThread
+import com.example.firstapp.database.dao.MsgDao
+import com.example.firstapp.database.entity.Msg
+
+
+class MsgRepository(private val msgDao: MsgDao) {
+
+ @WorkerThread
+ fun insert(msg: Msg): Long = msgDao.insert(msg)
+
+ @WorkerThread
+ fun delete(id: Long) = msgDao.delete(id)
+
+ fun deleteAll() = msgDao.deleteAll()
+
+ @WorkerThread
+ fun deleteTimeAgo(time: Long) = msgDao.deleteTimeAgo(time)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/entity/Item.kt b/app/src/main/java/com/example/firstapp/entity/Item.kt
new file mode 100644
index 0000000..4f415da
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/entity/Item.kt
@@ -0,0 +1,7 @@
+package com.example.firstapp.entity
+
+data class Item(
+ val id: Int,
+ val title: String,
+ val description: String
+)
diff --git a/app/src/main/java/com/example/firstapp/entity/Rule.kt b/app/src/main/java/com/example/firstapp/entity/Rule.kt
new file mode 100644
index 0000000..3bb161e
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/entity/Rule.kt
@@ -0,0 +1,17 @@
+package com.example.firstapp.entity
+
+data class Rule(var type: String,var content: String ,var reg: String) {
+
+ // 在 Rule 类中定义提取方法
+ fun extractCodeFromMessage(message: String): String? {
+ // 如果 message 包含 content
+ if (message.contains(content)) {
+ // 使用 reg 作为正则表达式进行匹配
+ val regex = reg.toRegex()
+ val matchResult = regex.find(message)
+ return matchResult?.value // 如果找到匹配的内容,则返回
+ }
+ return null // 如果不匹配,返回 null
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/entity/SmsInfo.kt b/app/src/main/java/com/example/firstapp/entity/SmsInfo.kt
new file mode 100644
index 0000000..e2c0ef0
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/entity/SmsInfo.kt
@@ -0,0 +1,37 @@
+package com.example.firstapp.entity
+
+import com.example.firstapp.R
+import com.google.gson.annotations.SerializedName
+
+import java.io.Serializable
+
+data class SmsInfo(
+ // 联系人姓名
+ var name: String = "",
+ // 联系人号码
+ var number: String = "",
+ // 短信内容
+ var content: String = "",
+ // 短信时间
+ var date: Long = 0L,
+ // 短信类型: 1=接收, 2=发送
+ var type: Int = 1,
+ // 卡槽ID: 0=Sim1, 1=Sim2, -1=获取失败
+ @SerializedName("sim_id")
+ var simId: Int = -1,
+ // 卡槽主键
+ @SerializedName("sub_id")
+ var subId: Int = 0,
+) : Serializable {
+
+ val typeImageId: Int = R.drawable.ic_sms
+
+ val simImageId: Int
+ get() {
+ return when (simId) {
+ 0 -> R.drawable.ic_sim1
+ 1 -> R.drawable.ic_sim2
+ else -> R.drawable.ic_sim
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/receiver/CactusReceiver.kt b/app/src/main/java/com/example/firstapp/receiver/CactusReceiver.kt
new file mode 100644
index 0000000..42aeaec
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/receiver/CactusReceiver.kt
@@ -0,0 +1,33 @@
+package com.example.firstapp.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.example.firstapp.App
+import com.example.firstapp.utils.Log
+import com.gyf.cactus.Cactus
+
+//接收Cactus广播
+class CactusReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ intent.action?.apply {
+ when (this) {
+ Cactus.CACTUS_WORK -> {
+ Log.d(
+ App.TAG,
+ this + "--" + intent.getIntExtra(Cactus.CACTUS_TIMES, 0)
+ )
+ }
+ Cactus.CACTUS_STOP -> {
+ Log.d(App.TAG, this)
+ }
+ Cactus.CACTUS_BACKGROUND -> {
+ Log.d(App.TAG, this)
+ }
+ Cactus.CACTUS_FOREGROUND -> {
+ Log.d(App.TAG, this)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt
new file mode 100644
index 0000000..1c2a82f
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/receiver/SmsReceiver.kt
@@ -0,0 +1,66 @@
+package com.example.firstapp.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Telephony
+import android.telephony.SmsMessage
+import android.util.Log
+import com.example.firstapp.core.Core
+import com.example.firstapp.database.entity.Code
+import com.example.firstapp.database.entity.Msg
+import com.example.firstapp.entity.Rule
+
+
+class SmsReceiver : BroadcastReceiver() {
+
+
+ override fun onReceive(context: Context, intent: Intent) {
+ // 检查广播的 Action 是否为短信接收
+ if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION == intent.action) {
+ // 获取短信内容
+ val bundle: Bundle? = intent.extras
+ bundle?.let {
+ val pdus = it.get("pdus") as Array<*>
+ val messages = arrayOfNulls<SmsMessage>(pdus.size)
+ val messageBody = StringBuilder()
+
+ for (i in pdus.indices) {
+ messages[i] = SmsMessage.createFromPdu(pdus[i] as ByteArray)
+ messageBody.append(messages[i]?.messageBody)
+ }
+
+ // 输出短信内容到控制台
+ Log.d("SmsReceiver", "Received SMS: ${messageBody.toString()}")
+ val msg = Msg(0, "1111", "111111", messageBody.toString(),1, "111", 1, 1)
+ val msgId = Core.msg.insert(msg)
+ Log.d("SmsReceiver", "Received SMS msgId: ${msgId}")
+
+ // 这里我要写个数组,并创建个对象存放一些内容,如这个对象的属性有匹配内容,正则表达式,并循环遍历
+ val ruleList = listOf(
+ Rule("快递","京东","\\d{6}"),
+ Rule("快递","菜鸟驿站","\\d{1,2}-\\d{1,2}-\\d{4}")
+ )
+
+ // kotlin 怎么创建一个类
+ for (rule in ruleList) {
+ val code = rule.extractCodeFromMessage(messageBody.toString())
+
+ if (code!==null) {
+ Log.d("SmsReceiver", "Received SMS code: ${code}")
+ // 封装成一个Code对象,并保存在数据库中
+ val code = Code(0, rule.type,1, rule.content,1, 1, msgId, code)
+ Core.code.insert(code)
+ // 发送广播通知数据已更新
+ val updateIntent = Intent("com.example.firstapp.DATA_UPDATED")
+ context.sendBroadcast(updateIntent)
+ }else{
+ Log.d("SmsReceiver", "Received SMS code: 没有匹配到内容")
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/service/BluetoothScanService.kt b/app/src/main/java/com/example/firstapp/service/BluetoothScanService.kt
new file mode 100644
index 0000000..ee4f1ed
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/service/BluetoothScanService.kt
@@ -0,0 +1,73 @@
+package com.example.firstapp.service
+
+import android.Manifest
+import android.app.Service
+import android.bluetooth.BluetoothAdapter
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.IBinder
+import androidx.core.app.ActivityCompat
+import com.example.firstapp.utils.ACTION_RESTART
+import com.example.firstapp.utils.ACTION_START
+import com.example.firstapp.utils.ACTION_STOP
+import com.example.firstapp.utils.Log
+//import com.example.firstapp.utils.task.TaskUtils
+
+@Suppress("PrivatePropertyName", "DEPRECATION")
+class BluetoothScanService : Service() {
+
+ private val TAG: String = BluetoothScanService::class.java.simpleName
+ private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
+
+ companion object {
+ var isRunning = false
+ }
+
+ override fun onBind(p0: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) return START_NOT_STICKY
+ Log.i(TAG, "onStartCommand: ${intent.action}")
+
+ when (intent.action) {
+ ACTION_START -> startDiscovery()
+ ACTION_STOP -> stopDiscovery()
+ ACTION_RESTART -> {
+ stopDiscovery()
+ startDiscovery()
+ }
+ }
+ return START_NOT_STICKY
+ }
+
+ // 开始扫描蓝牙设备
+ private fun startDiscovery() {
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ if (isRunning) return
+ // 清空已发现设备
+// TaskUtils.discoveredDevices = mutableMapOf()
+ bluetoothAdapter?.startDiscovery()
+ isRunning = true
+ }
+
+ // 停止蓝牙扫描
+ private fun stopDiscovery() {
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ if (!isRunning) return
+ bluetoothAdapter?.cancelDiscovery()
+ isRunning = false
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isRunning = false
+ stopDiscovery()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/service/HttpServerService.kt b/app/src/main/java/com/example/firstapp/service/HttpServerService.kt
new file mode 100644
index 0000000..29fa68e
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/service/HttpServerService.kt
@@ -0,0 +1,62 @@
+package com.example.firstapp.service
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import com.example.firstapp.utils.Log
+import com.example.firstapp.utils.HTTP_SERVER_PORT
+import com.example.firstapp.utils.HTTP_SERVER_TIME_OUT
+import com.example.firstapp.utils.SettingUtils
+import com.yanzhenjie.andserver.AndServer
+import com.yanzhenjie.andserver.Server
+import java.util.concurrent.TimeUnit
+
+@Suppress("PrivatePropertyName")
+class HttpServerService : Service(), Server.ServerListener {
+
+ private val TAG: String = HttpServerService::class.java.simpleName
+ private val server by lazy {
+ AndServer.webServer(this).port(HTTP_SERVER_PORT).listener(this).timeout(HTTP_SERVER_TIME_OUT, TimeUnit.SECONDS).build()
+ }
+
+ override fun onBind(p0: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ //纯客户端模式
+ if (SettingUtils.enablePureClientMode) return
+
+ Log.i(TAG, "onCreate: ")
+ server.startup()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.i(TAG, "onStartCommand: ")
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ //纯客户端模式
+ if (SettingUtils.enablePureClientMode) return
+
+ Log.i(TAG, "onDestroy: ")
+ server.shutdown()
+ }
+
+ override fun onException(e: Exception?) {
+ Log.i(TAG, "onException: ")
+ }
+
+ override fun onStarted() {
+ Log.i(TAG, "onStarted: ")
+ }
+
+ override fun onStopped() {
+ Log.i(TAG, "onStopped: ")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
new file mode 100644
index 0000000..0be390f
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
@@ -0,0 +1,42 @@
+package com.example.firstapp.ui.dashboard
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.example.firstapp.databinding.FragmentDashboardBinding
+
+class DashboardFragment : Fragment() {
+
+ private var _binding: FragmentDashboardBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val dashboardViewModel =
+ ViewModelProvider(this).get(DashboardViewModel::class.java)
+
+ _binding = FragmentDashboardBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ val textView: TextView = binding.textDashboard
+ dashboardViewModel.text.observe(viewLifecycleOwner) {
+ textView.text = it
+ }
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt
new file mode 100644
index 0000000..e47a4a6
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/dashboard/DashboardViewModel.kt
@@ -0,0 +1,13 @@
+package com.example.firstapp.ui.dashboard
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class DashboardViewModel : ViewModel() {
+
+ private val _text = MutableLiveData<String>().apply {
+ value = "This is dashboard Fragment"
+ }
+ val text: LiveData<String> = _text
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt
new file mode 100644
index 0000000..bf4b7ec
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/home/HomeFragment.kt
@@ -0,0 +1,71 @@
+package com.example.firstapp.ui.home
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.example.firstapp.R
+import com.example.firstapp.adapter.MyAdapter
+import com.example.firstapp.core.Core
+import com.example.firstapp.databinding.FragmentHomeBinding
+
+class HomeFragment : Fragment() {
+
+ private var _binding: FragmentHomeBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ private lateinit var homeViewModel: HomeViewModel
+ private lateinit var adapter: MyAdapter
+
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ homeViewModel =
+ ViewModelProvider(this).get(HomeViewModel::class.java)
+
+ _binding = FragmentHomeBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+// val textView: TextView = binding.textHome
+// homeViewModel.text.observe(viewLifecycleOwner) {
+// textView.text = it
+// }
+
+
+ // 初始化适配器
+ adapter = MyAdapter()
+
+ // 获取数据
+// val codeList = Core.code.getAllDesc()
+
+ // 使用 binding 来访问 RecyclerView
+ val recyclerView: RecyclerView = binding.recyclerView
+ recyclerView.layoutManager = LinearLayoutManager(requireContext()) // 使用 requireContext() 获取上下文
+ recyclerView.adapter = adapter
+
+ // 观察 LiveData,当数据发生变化时,更新 RecyclerView 的内容
+ homeViewModel.codeList.observe(viewLifecycleOwner) { codeList ->
+ adapter.submitList(codeList) // 更新 RecyclerView 的数据
+ // 滚动到顶部
+ recyclerView.scrollToPosition(0)
+ }
+
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
new file mode 100644
index 0000000..630b6af
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/home/HomeViewModel.kt
@@ -0,0 +1,36 @@
+package com.example.firstapp.ui.home
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.example.firstapp.core.Core
+import com.example.firstapp.database.entity.Code
+
+class HomeViewModel : ViewModel() {
+
+ private val _text = MutableLiveData<String>().apply {
+ value = "短信主页面"
+ }
+ val text: LiveData<String> = _text
+
+ private val _codeList = MutableLiveData<List<Code>>()
+
+ val codeList: LiveData<List<Code>> get() = _codeList
+
+ init {
+ // 初始化时加载数据
+ loadData()
+ }
+
+ // 加载数据的方法
+ fun loadData() {
+ // 获取数据,并更新 LiveData
+ _codeList.value = Core.code.getAllDesc() // 假设这是获取最新的 data 的方法
+ }
+
+ // 如果需要手动更新数据,可以调用这个方法
+ fun updateData() {
+ _codeList.value = Core.code.getAllDesc() // 重新获取并更新数据
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsFragment.kt b/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsFragment.kt
new file mode 100644
index 0000000..3f7139f
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsFragment.kt
@@ -0,0 +1,42 @@
+package com.example.firstapp.ui.notifications
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.example.firstapp.databinding.FragmentNotificationsBinding
+
+class NotificationsFragment : Fragment() {
+
+ private var _binding: FragmentNotificationsBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val notificationsViewModel =
+ ViewModelProvider(this).get(NotificationsViewModel::class.java)
+
+ _binding = FragmentNotificationsBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ val textView: TextView = binding.textNotifications
+ notificationsViewModel.text.observe(viewLifecycleOwner) {
+ textView.text = it
+ }
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsViewModel.kt
new file mode 100644
index 0000000..08127d7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/ui/notifications/NotificationsViewModel.kt
@@ -0,0 +1,13 @@
+package com.example.firstapp.ui.notifications
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class NotificationsViewModel : ViewModel() {
+
+ private val _text = MutableLiveData<String>().apply {
+ value = "This is notifications Fragment"
+ }
+ val text: LiveData<String> = _text
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/AppUtils.kt b/app/src/main/java/com/example/firstapp/utils/AppUtils.kt
new file mode 100644
index 0000000..29bd5ac
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/AppUtils.kt
@@ -0,0 +1,104 @@
+package com.example.firstapp.utils
+
+import android.annotation.SuppressLint
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import com.example.firstapp.App
+
+data class AppInfo(
+ val name: String,
+ val icon: Drawable,
+ val packageName: String,
+ val packagePath: String,
+ val versionName: String,
+ val versionCode: Int,
+ val isSystem: Boolean,
+ val uid: Int
+)
+
+@SuppressLint("StaticFieldLeak")
+@Suppress("DEPRECATION")
+object AppUtils {
+
+ fun getAppsInfo(): List<AppInfo> {
+ val packageManager = App.context.packageManager ?: return emptyList()
+ val appsInfo = mutableListOf<AppInfo>()
+
+ val apps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
+ for (app in apps) {
+ try {
+ val packageInfo = packageManager.getPackageInfo(app.packageName, 0)
+ val appInfo = AppInfo(
+ app.loadLabel(packageManager).toString(),
+ app.loadIcon(packageManager),
+ app.packageName,
+ app.sourceDir,
+ packageInfo?.versionName ?: "Unknown",
+ packageInfo?.versionCode ?: 0,
+ (app.flags and ApplicationInfo.FLAG_SYSTEM) != 0,
+ app.uid
+ )
+ appsInfo.add(appInfo)
+ } catch (e: PackageManager.NameNotFoundException) {
+ e.printStackTrace()
+ Log.e("AppUtils", "getAppsInfo: ${e.message}")
+ }
+ }
+
+ return appsInfo
+ }
+
+ fun getAppVersionCode(): Int {
+ return getAppVersionCode(App.context.packageName)
+ }
+
+ private fun getAppVersionCode(packageName: String?): Int {
+ if (packageName.isNullOrBlank()) {
+ return -1
+ }
+ return try {
+ val pm: PackageManager = App.context.packageManager
+ val pi: PackageInfo = pm.getPackageInfo(packageName, 0)
+ pi.versionCode
+ } catch (e: PackageManager.NameNotFoundException) {
+ e.printStackTrace()
+ Log.e("AppUtils", "getAppVersionCode: ${e.message}")
+ -1
+ }
+ }
+
+ fun getAppPackageName(): String {
+ return App.context.packageName
+ }
+
+ fun getAppVersionName(): String {
+ return getAppVersionName(App.context.packageName)
+ }
+
+ private fun getAppVersionName(packageName: String): String {
+ if (packageName.isBlank()) {
+ return ""
+ }
+ return try {
+ val pm: PackageManager = App.context.packageManager
+ val pi: PackageInfo = pm.getPackageInfo(packageName, 0)
+ pi.versionName ?: ""
+ } catch (e: PackageManager.NameNotFoundException) {
+ e.printStackTrace()
+ Log.e("AppUtils", "getAppVersionName: ${e.message}")
+ ""
+ }
+ }
+
+ /*fun openApp(packageName: String) {
+ val packageManager = App.context.packageManager
+ val intent = packageManager.getLaunchIntentForPackage(packageName)
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ App.context.startActivity(intent)
+ }
+ }*/
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/Base64.kt b/app/src/main/java/com/example/firstapp/utils/Base64.kt
new file mode 100644
index 0000000..1744f81
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/Base64.kt
@@ -0,0 +1,88 @@
+package com.example.firstapp.utils
+
+import java.io.UnsupportedEncodingException
+
+/**
+ * Base64编码解码
+ */
+object Base64 {
+
+ private val base64EncodeChars = charArrayOf('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/')
+
+ private val base64DecodeChars = byteArrayOf(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1)
+
+ fun encode(data: ByteArray): String {
+ val sb = StringBuffer()
+ val len = data.size
+ var i = 0
+ var b1: Int
+ var b2: Int
+ var b3: Int
+ while (i < len) {
+ b1 = ((data[i++]).toInt() and 0xff)
+ if (i == len) {
+ sb.append(base64EncodeChars[b1.ushr(2)])
+ sb.append(base64EncodeChars[b1 and 0x3 shl 4])
+ sb.append("==")
+ break
+ }
+ b2 = (data[i++]).toInt() and 0xff
+ if (i == len) {
+ sb.append(base64EncodeChars[b1.ushr(2)])
+ sb.append(base64EncodeChars[b1 and 0x03 shl 4 or (b2 and 0xf0).ushr(4)])
+ sb.append(base64EncodeChars[b2 and 0x0f shl 2])
+ sb.append("=")
+ break
+ }
+ b3 = (data[i++]).toInt() and 0xff
+ sb.append(base64EncodeChars[b1.ushr(2)])
+ sb.append(base64EncodeChars[b1 and 0x03 shl 4 or (b2 and 0xf0).ushr(4)])
+ sb.append(base64EncodeChars[b2 and 0x0f shl 2 or (b3 and 0xc0).ushr(6)])
+ sb.append(base64EncodeChars[b3 and 0x3f])
+ }
+ return sb.toString()
+ }
+
+ @Throws(UnsupportedEncodingException::class)
+ fun decode(str: String): ByteArray {
+ val sb = StringBuffer()
+ val data = str.toByteArray(charset("US-ASCII"))
+ val len = data.size
+ var i = 0
+ var b1: Int
+ var b2: Int
+ var b3: Int
+ var b4: Int
+ while (i < len) {
+ /* b1 */
+ do {
+ b1 = base64DecodeChars[(data[i++]).toInt()].toInt()
+ } while (i < len && b1 == -1)
+ if (b1 == -1) break
+ /* b2 */
+ do {
+ b2 = base64DecodeChars[(data[i++]).toInt()].toInt()
+ } while (i < len && b2 == -1)
+ if (b2 == -1) break
+ sb.append((b1 shl 2 or (b2 and 0x30).ushr(4)).toChar())
+ /* b3 */
+ do {
+ b3 = data[i++].toInt()
+ if (b3 == 61) return sb.toString().toByteArray(charset("ISO-8859-1"))
+ b3 = base64DecodeChars[b3].toInt()
+ } while (i < len && b3 == -1)
+ if (b3 == -1) break
+ sb.append((b2 and 0x0f shl 4 or (b3 and 0x3c).ushr(2)).toChar())
+ /* b4 */
+ do {
+ b4 = data[i++].toInt()
+ if (b4 == 61) return sb.toString().toByteArray(charset("ISO-8859-1"))
+ b4 = base64DecodeChars[b4].toInt()
+ } while (i < len && b4 == -1)
+ if (b4 == -1) break
+ sb.append((b3 and 0x03 shl 6 or b4).toChar())
+ }
+ return sb.toString().toByteArray(charset("ISO-8859-1"))
+ }
+
+}
diff --git a/app/src/main/java/com/example/firstapp/utils/BluetoothUtils.kt b/app/src/main/java/com/example/firstapp/utils/BluetoothUtils.kt
new file mode 100644
index 0000000..aacbf19
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/BluetoothUtils.kt
@@ -0,0 +1,38 @@
+package com.example.firstapp.utils
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+
+@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate")
+object BluetoothUtils {
+
+ /**
+ * 检查应用是否具有蓝牙权限
+ */
+ fun hasBluetoothPermission(context: Context): Boolean {
+ return ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
+ }
+
+ /**
+ * 检查蓝牙是否已启用
+ */
+ fun isBluetoothEnabled(): Boolean {
+ val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ return bluetoothAdapter != null && bluetoothAdapter.isEnabled
+ }
+
+ /**
+ * 检查设备是否支持蓝牙功能
+ */
+ fun hasBluetoothCapability(context: Context): Boolean {
+ if (!hasBluetoothPermission(context)) {
+ Log.e("BluetoothUtils", "hasBluetoothCapability: no bluetooth permission")
+ return false
+ }
+
+ return context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/utils/CacheUtils.kt b/app/src/main/java/com/example/firstapp/utils/CacheUtils.kt
new file mode 100644
index 0000000..4a6b7ae
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/CacheUtils.kt
@@ -0,0 +1,108 @@
+package com.example.firstapp.utils
+
+import android.content.Context
+import android.os.Environment
+import java.io.File
+import java.math.BigDecimal
+
+@Suppress("DEPRECATION")
+class CacheUtils private constructor() {
+ companion object {
+ /**
+ * 获取缓存大小
+ *
+ * @param context 上下文
+ * @return 缓存大小
+ */
+ fun getTotalCacheSize(context: Context): String {
+ return try {
+ var cacheSize = getFolderSize(context.cacheDir)
+ if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
+ cacheSize += getFolderSize(context.externalCacheDir)
+ }
+ getFormatSize(cacheSize.toDouble())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.e("CacheUtils", "getTotalCacheSize: ${e.message}")
+ "0KB"
+ }
+ }
+
+ /***
+ * 清理所有缓存
+ * @param context 上下文
+ */
+ fun clearAllCache(context: Context) {
+ deleteDir(context.cacheDir)
+ if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
+ deleteDir(context.externalCacheDir)
+ }
+ }
+
+ private fun deleteDir(dir: File?): Boolean {
+ if (dir != null && dir.isDirectory) {
+ val children = dir.list()!!
+ for (child in children) {
+ val success = deleteDir(File(dir, child))
+ if (!success) {
+ return false
+ }
+ }
+ }
+ assert(dir != null)
+ return dir!!.delete()
+ }
+
+ // 获取文件
+ //Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/ 目录,一般放一些长时间保存的数据
+ //Context.getExternalCacheDir() --> SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据
+ private fun getFolderSize(file: File?): Long {
+ var size: Long = 0
+ try {
+ val fileList = file!!.listFiles()!!
+ for (value in fileList) {
+ // 如果下面还有文件
+ size = if (value.isDirectory) {
+ size + getFolderSize(value)
+ } else {
+ size + value.length()
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.e("CacheUtils", "getFolderSize: ${e.message}")
+ }
+ return size
+ }
+
+ /**
+ * 格式化单位
+ *
+ * @param size 文件大小
+ * @return 结果
+ */
+ private fun getFormatSize(size: Double): String {
+ val kiloByte = size / 1024
+ if (kiloByte < 1) {
+ return "0KB"
+ }
+ val megaByte = kiloByte / 1024
+ if (megaByte < 1) {
+ val result1 = BigDecimal(kiloByte.toString())
+ return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB"
+ }
+ val gigaByte = megaByte / 1024
+ if (gigaByte < 1) {
+ val result2 = BigDecimal(megaByte.toString())
+ return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB"
+ }
+ val teraBytes = gigaByte / 1024
+ if (teraBytes < 1) {
+ val result3 = BigDecimal(gigaByte.toString())
+ return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB"
+ }
+ val result4 = BigDecimal(teraBytes)
+ return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/CactusSave.kt b/app/src/main/java/com/example/firstapp/utils/CactusSave.kt
new file mode 100644
index 0000000..9c49615
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/CactusSave.kt
@@ -0,0 +1,15 @@
+package com.example.firstapp.utils
+
+object CactusSave {
+ //Cactus存活时间
+ var timer: Long by SharedPreference(CACTUS_TIMER, 0L)
+
+ //Cactus上次存活时间
+ var lastTimer: Long by SharedPreference(CACTUS_LAST_TIMER, 0L)
+
+ //Cactus运行时间
+ var date: String by SharedPreference(CACTUS_DATE, "0000-01-01 00:00:00")
+
+ //Cactus结束时间
+ var endDate: String by SharedPreference(CACTUS_END_DATE, "0000-01-01 00:00:00")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/Constants.kt b/app/src/main/java/com/example/firstapp/utils/Constants.kt
new file mode 100644
index 0000000..354972b
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/Constants.kt
@@ -0,0 +1,318 @@
+package com.example.firstapp.utils
+
+object Worker {
+ const val SEND_MSG_INFO = "send_msg_info"
+ const val UPDATE_LOGS = "update_logs"
+ const val RULE = "rule"
+ const val SENDER_INDEX = "sender_index"
+ const val MSG_ID = "msg_id"
+}
+
+object TaskWorker {
+ const val TASK_ID = "task_id"
+ const val TASK = "task"
+ const val TASK_CONDITIONS = "task_conditions"
+ const val TASK_ACTIONS = "task_actions"
+ const val CONDITION_TYPE = "condition_type"
+ const val MSG = "msg"
+ const val MSG_INFO = "msg_info"
+ const val ACTION = "action"
+}
+
+//服务相关
+const val ACTION_START = "START"
+const val ACTION_STOP = "STOP"
+const val ACTION_RESTART = "RESTART"
+const val ACTION_STOP_ALARM = "STOP_ALARM"
+const val ACTION_UPDATE_NOTIFICATION = "UPDATE_NOTIFICATION"
+const val EXTRA_UPDATE_NOTIFICATION = "EXTRA_UPDATE_NOTIFICATION"
+
+//初始化相关
+const val AUTO_CHECK_UPDATE = "auto_check_update"
+const val JOIN_PREVIEW_PROGRAM = "join_preview_program"
+const val IS_AGREE_PRIVACY_KEY = "is_agree_privacy_key"
+
+//数据库
+const val DATABASE_NAME = "sms_first_app.db"
+const val PACKAGE_NAME = "com.example.firstapp"
+
+//通用设置
+const val SP_ENABLE_SMS = "enable_sms"
+
+const val SP_ENABLE_PHONE = "enable_phone"
+const val SP_ENABLE_CALL_TYPE_1 = "enable_call_type_1"
+const val SP_ENABLE_CALL_TYPE_2 = "enable_call_type_2"
+const val SP_ENABLE_CALL_TYPE_3 = "enable_call_type_3"
+const val SP_ENABLE_CALL_TYPE_4 = "enable_call_type_4"
+const val SP_ENABLE_CALL_TYPE_5 = "enable_call_type_5"
+const val SP_ENABLE_CALL_TYPE_6 = "enable_call_type_6"
+
+const val SP_ENABLE_APP_NOTIFY = "enable_app_notify"
+const val SP_ENABLE_CANCEL_APP_NOTIFY = "enable_cancel_app_notify"
+const val SP_CANCEL_EXTRA_APP_NOTIFY = "cancel_extra_app_notify_list"
+const val SP_ENABLE_NOT_USER_PRESENT = "enable_not_user_present"
+
+const val SP_ENABLE_SMS_COMMAND = "enable_sms_command"
+const val SP_SMS_COMMAND_SAFE_PHONE = "sms_command_safe_phone"
+
+const val ENABLE_LOAD_APP_LIST = "enable_load_app_list"
+const val ENABLE_LOAD_USER_APP_LIST = "enable_load_user_app_list"
+const val ENABLE_LOAD_SYSTEM_APP_LIST = "enable_load_system_app_list"
+
+const val SP_DUPLICATE_MESSAGES_LIMITS = "duplicate_messages_limits"
+const val SP_SILENT_PERIOD_START = "silent_period_start"
+const val SP_SILENT_PERIOD_END = "silent_period_end"
+const val SP_ENABLE_SILENT_PERIOD_LOGS = "enable_silent_period_logs"
+
+const val SP_ENABLE_EXCLUDE_FROM_RECENTS = "enable_exclude_from_recents"
+const val SP_ENABLE_PLAY_SILENCE_MUSIC = "enable_play_silence_music"
+const val SP_ENABLE_ONE_PIXEL_ACTIVITY = "enable_one_pixel_activity"
+
+const val SP_REQUEST_RETRY_TIMES = "request_retry_times"
+const val SP_REQUEST_DELAY_TIME = "request_delay_time"
+const val SP_REQUEST_TIMEOUT = "request_timeout"
+
+const val SP_NOTIFY_CONTENT = "notify_content"
+const val SP_EXTRA_DEVICE_MARK = "extra_device_mark"
+const val SP_SUBID_SIM1 = "subid_sim1"
+const val SP_SUBID_SIM2 = "subid_sim2"
+const val SP_EXTRA_SIM1 = "extra_sim1"
+const val SP_EXTRA_SIM2 = "extra_sim2"
+const val SP_ENABLE_SMS_TEMPLATE = "enable_sms_template"
+const val SP_SMS_TEMPLATE = "sms_template"
+
+const val SP_PURE_CLIENT_MODE = "enable_pure_client_mode"
+const val SP_PURE_TASK_MODE = "enable_pure_task_mode"
+const val SP_DEBUG_MODE = "enable_debug_mode"
+
+//const val SP_IS_FLOW_SYSTEM_LANGUAGE = "is_flow_system_language"
+const val SP_LOCATION = "enable_location"
+const val SP_LOCATION_ACCURACY = "location_accuracy"
+const val SP_LOCATION_POWER_REQUIREMENT = "location_power_requirement"
+const val SP_LOCATION_MIN_INTERVAL = "location_min_interval_time"
+const val SP_LOCATION_MIN_DISTANCE = "location_min_distance"
+
+const val SP_BLUETOOTH = "enable_bluetooth"
+const val SP_BLUETOOTH_SCAN_INTERVAL = "bluetooth_scan_interval"
+const val SP_BLUETOOTH_IGNORE_ANONYMOUS = "bluetooth_ignore_anonymous"
+
+const val SP_ENABLE_CACTUS = "enable_cactus"
+const val CACTUS_TIMER = "cactus_timer"
+const val CACTUS_LAST_TIMER = "cactus_last_timer"
+const val CACTUS_DATE = "cactus_date"
+const val CACTUS_END_DATE = "cactus_end_date"
+
+//规则相关
+const val STATUS_ON = 1
+const val STATUS_OFF = 0
+const val FILED_TRANSPOND_ALL = "transpond_all"
+const val FILED_PHONE_NUM = "phone_num"
+const val FILED_CALL_TYPE = "call_type"
+const val FILED_PACKAGE_NAME = "package_name"
+const val FILED_UID = "uid"
+const val FILED_MSG_CONTENT = "msg_content"
+const val FILED_INFORM_CONTENT = "inform_content"
+const val FILED_MULTI_MATCH = "multi_match"
+const val CHECK_IS = "is"
+const val CHECK_CONTAIN = "contain"
+const val CHECK_NOT_CONTAIN = "notcontain"
+const val CHECK_START_WITH = "startwith"
+const val CHECK_END_WITH = "endwith"
+const val CHECK_NOT_IS = "notis"
+const val CHECK_REGEX = "regex"
+const val CHECK_SIM_SLOT_ALL = "ALL"
+const val CHECK_SIM_SLOT_1 = "SIM1"
+const val CHECK_SIM_SLOT_2 = "SIM2"
+
+//发送通道执行逻辑:ALL=全部执行, UntilFail=失败即终止, UntilSuccess=成功即终止
+const val SENDER_LOGIC_ALL = "ALL"
+const val SENDER_LOGIC_UNTIL_FAIL = "UntilFail"
+const val SENDER_LOGIC_UNTIL_SUCCESS = "UntilSuccess"
+const val SENDER_LOGIC_RETRY = "Retry"
+
+//发送通道
+const val TYPE_DINGTALK_GROUP_ROBOT = 0
+const val TYPE_EMAIL = 1
+const val TYPE_BARK = 2
+const val TYPE_WEBHOOK = 3
+const val TYPE_WEWORK_ROBOT = 4
+const val TYPE_WEWORK_AGENT = 5
+const val TYPE_SERVERCHAN = 6
+const val TYPE_TELEGRAM = 7
+const val TYPE_SMS = 8
+const val TYPE_FEISHU = 9
+const val TYPE_PUSHPLUS = 10
+const val TYPE_GOTIFY = 11
+const val TYPE_DINGTALK_INNER_ROBOT = 12
+const val TYPE_FEISHU_APP = 13
+const val TYPE_URL_SCHEME = 14
+const val TYPE_SOCKET = 15
+
+//前台服务
+const val FRONT_NOTIFY_ID = 0x1010
+const val FRONT_CHANNEL_ID = "com.example.firstapp"
+const val FRONT_CHANNEL_NAME = "SmsForwarder Foreground Service"
+
+//Frp内网穿透
+const val FRPC_LIB_DOWNLOAD_URL = "https://xupdate.ppps.cn/uploads/%s/%s/libgojni.so"
+const val FRPC_LIB_VERSION = "0.57.0"
+const val EVENT_FRPC_UPDATE_CONFIG = "EVENT_FRPC_UPDATE_CONFIG"
+const val EVENT_FRPC_DELETE_CONFIG = "EVENT_FRPC_DELETE_CONFIG"
+const val EVENT_FRPC_RUNNING_ERROR = "EVENT_FRPC_RUNNING_ERROR"
+const val EVENT_FRPC_RUNNING_SUCCESS = "EVENT_FRPC_RUNNING_SUCCESS"
+const val INTENT_FRPC_EDIT_FILE = "INTENT_FRPC_EDIT_FILE"
+const val INTENT_FRPC_APPLY_FILE = "INTENT_FRPC_APPLY_FILE"
+
+//声音警报
+const val EVENT_ALARM_ACTION = "EVENT_ALARM_ACTION"
+
+//吐司监听
+const val EVENT_TOAST_SUCCESS = "key_toast_success"
+const val EVENT_TOAST_ERROR = "key_toast_error"
+const val EVENT_TOAST_INFO = "key_toast_info"
+const val EVENT_TOAST_WARNING = "key_toast_warning"
+
+const val KEY_SENDER_ID = "key_sender_id"
+const val KEY_SENDER_TYPE = "key_sender_type"
+const val KEY_SENDER_CLONE = "key_sender_clone"
+const val KEY_SENDER_TEST = "key_sender_test"
+
+const val KEY_RULE_ID = "key_rule_id"
+const val KEY_RULE_TYPE = "key_rule_type"
+const val KEY_RULE_CLONE = "key_rule_clone"
+const val KEY_DEFAULT_SELECTION = "key_default_selection"
+
+const val KEY_TASK_ID = "key_task_id"
+const val KEY_TASK_TYPE = "key_task_type"
+const val KEY_TASK_CLONE = "key_task_clone"
+
+const val EVENT_LOAD_APP_LIST = "EVENT_LOAD_APP_LIST"
+
+const val EVENT_KEY_SIM_SLOT = "EVENT_KEY_SIM_SLOT"
+const val EVENT_KEY_PHONE_NUMBERS = "EVENT_KEY_PHONE_NUMBERS"
+
+//在线升级&预览计划URL
+const val KEY_UPDATE_URL = "https://xupdate.ppps.cn/update/checkVersion"
+const val KEY_PREVIEW_URL = "https://xupdate.ppps.cn/preview/checkVersion"
+
+//HttpServer相关
+const val HTTP_SERVER_PORT = 5000
+const val HTTP_SERVER_TIME_OUT = 10
+const val HTTP_SUCCESS_CODE: Int = 200
+const val HTTP_FAILURE_CODE: Int = 500
+const val SP_ENABLE_SERVER_AUTORUN = "enable_server_autorun"
+const val SP_SERVER_SAFETY_MEASURES = "server_safety_measures"
+const val SP_SERVER_SIGN_KEY = "server_sign_key"
+const val SP_SERVER_TIME_TOLERANCE = "server_time_tolerance"
+const val SP_SERVER_SM4_KEY = "server_sm4_key"
+const val SP_SERVER_PUBLIC_KEY = "server_public_key"
+const val SP_SERVER_PRIVATE_KEY = "server_private_key"
+const val SP_SERVER_WEB_PATH = "server_web_path"
+const val SP_ENABLE_API_CLONE = "enable_api_clone"
+const val SP_ENABLE_API_SMS_SEND = "enable_api_sms_send"
+const val SP_ENABLE_API_SMS_QUERY = "enable_api_sms_query"
+const val SP_ENABLE_API_CALL_QUERY = "enable_api_call_query"
+const val SP_ENABLE_API_CONTACT_QUERY = "enable_api_contact_query"
+const val SP_ENABLE_API_CONTACT_ADD = "enable_api_contact_add"
+const val SP_ENABLE_API_BATTERY_QUERY = "enable_api_battery_query"
+const val SP_ENABLE_API_WOL = "enable_api_wol"
+const val SP_ENABLE_API_LOCATION = "enable_api_location"
+const val SP_API_LOCATION_CACHE = "api_location_cache"
+const val SP_WOL_HISTORY = "wol_history"
+const val SP_SERVER_ADDRESS = "server_address"
+const val SP_SERVER_HISTORY = "server_history"
+const val SP_SERVER_CONFIG = "server_config"
+const val SP_CLIENT_SAFETY_MEASURES = "client_safety_measures"
+const val SP_CLIENT_SIGN_KEY = "client_sign_key"
+
+
+//自动任务
+const val MAX_SETTING_NUM = 5 //最大条件/动作设置条数
+const val KEY_TEST_CONDITION = "key_test_condition"
+const val KEY_EVENT_DATA_CONDITION = "event_data_condition"
+const val KEY_EVENT_PARAMS_CONDITION = "event_params_condition"
+const val KEY_BACK_CODE_CONDITION = 1000
+const val KEY_BACK_DATA_CONDITION = "back_data_condition"
+const val KEY_BACK_DESCRIPTION_CONDITION = "back_description_condition"
+
+const val KEY_EVENT_DATA_ACTION = "event_data_action"
+const val KEY_BACK_CODE_ACTION = 2000
+const val KEY_BACK_DATA_ACTION = "back_data_action"
+const val KEY_BACK_DESCRIPTION_ACTION = "back_description_action"
+
+//注意:TASK_CONDITION_XXX 枚举值 等于 TASK_CONDITION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_CONDITION,不可改变
+const val TASK_CONDITION_CRON = 1000
+const val TASK_CONDITION_TO_ADDRESS = 1001
+const val TASK_CONDITION_LEAVE_ADDRESS = 1002
+const val TASK_CONDITION_NETWORK = 1003
+const val TASK_CONDITION_SIM = 1004
+const val TASK_CONDITION_BATTERY = 1005
+const val TASK_CONDITION_CHARGE = 1006
+const val TASK_CONDITION_LOCK_SCREEN = 1007
+const val TASK_CONDITION_SMS = 1008
+const val TASK_CONDITION_CALL = 1009
+const val TASK_CONDITION_APP = 1010
+const val TASK_CONDITION_BLUETOOTH = 1011
+
+//注意:TASK_ACTION_XXX 枚举值 等于 TASK_ACTION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_ACTION,不可改变
+const val TASK_ACTION_SENDSMS = 2000
+const val TASK_ACTION_NOTIFICATION = 2001
+const val TASK_ACTION_CLEANER = 2002
+const val TASK_ACTION_SETTINGS = 2003
+const val TASK_ACTION_FRPC = 2004
+const val TASK_ACTION_HTTPSERVER = 2005
+const val TASK_ACTION_RULE = 2006
+const val TASK_ACTION_SENDER = 2007
+const val TASK_ACTION_ALARM = 2008
+const val TASK_ACTION_RESEND = 2009
+const val TASK_ACTION_TASK = 2010
+
+const val SP_BATTERY_INFO = "battery_info"
+const val SP_BATTERY_STATUS = "battery_status"
+const val SP_BATTERY_LEVEL = "battery_level"
+const val SP_BATTERY_PCT = "battery_pct"
+const val SP_BATTERY_PLUGGED = "battery_plugged"
+
+const val SP_NETWORK_STATE = "network_state"
+const val SP_DATA_SIM_SLOT = "data_sim_slot"
+const val SP_WIFI_SSID = "wifi_ssid"
+const val SP_IPV4 = "ipv4"
+const val SP_IPV6 = "ipv6"
+const val SP_IP_LIST = "ip_list"
+const val SP_SIM_STATE = "sim_state"
+const val SP_LOCATION_INFO_OLD = "location_info_old"
+const val SP_LOCATION_INFO_NEW = "location_info_new"
+const val SP_LOCK_SCREEN_ACTION = "lock_screen_action"
+const val SP_CONNECTED_DEVICE = "connected_device"
+const val SP_DISCOVERED_DEVICES = "discovered_devices"
+const val SP_BLUETOOTH_STATE = "bluetooth_state"
+
+//SIM卡已准备就绪时,延迟5秒(给够搜索信号时间)才执行任务
+const val DELAY_TIME_AFTER_SIM_READY = 5000L
+
+//切换语言需要替换的自定义模板标签列表
+//val TAG_LANG = arrayOf("zh_CN", "zh_TW", "en")
+val TAG_LIST = arrayOf(
+ mapOf("zh_CN" to "{{来源号码}}", "zh_TW" to "{{來源號碼}}", "en" to "{{FROM}}"),
+ mapOf("zh_CN" to "{{短信内容}}", "zh_TW" to "{{簡訊內容}}", "en" to "{{SMS}}"),
+ mapOf("zh_CN" to "{{APP包名}}", "zh_TW" to "{{APP包名}}", "en" to "{{PACKAGE_NAME}}"),
+ mapOf("zh_CN" to "{{APP名称}}", "zh_TW" to "{{APP名稱}}", "en" to "{{APP_NAME}}"),
+ mapOf("zh_CN" to "{{通知内容}}", "zh_TW" to "{{通知內容}}", "en" to "{{MSG}}"),
+ mapOf("zh_CN" to "{{卡槽信息}}", "zh_TW" to "{{卡槽信息}}", "en" to "{{CARD_SLOT}}"),
+ mapOf("zh_CN" to "{{卡槽主键}}", "zh_TW" to "{{卡槽主鍵}}", "en" to "{{CARD_SUBID}}"),
+ mapOf("zh_CN" to "{{接收时间}}", "zh_TW" to "{{接收時間}}", "en" to "{{RECEIVE_TIME}}"),
+ mapOf("zh_CN" to "{{当前时间}}", "zh_TW" to "{{當前時間}}", "en" to "{{CURRENT_TIME}}"),
+ mapOf("zh_CN" to "{{设备名称}}", "zh_TW" to "{{設備名稱}}", "en" to "{{DEVICE_NAME}}"),
+ mapOf("zh_CN" to "{{当前应用版本号}}", "zh_TW" to "{{當前應用版本號}}", "en" to "{{APP_VERSION}}"),
+ mapOf("zh_CN" to "{{通知标题}}", "zh_TW" to "{{通知標題}}", "en" to "{{TITLE}}"),
+ mapOf("zh_CN" to "{{通知Scheme}}", "zh_TW" to "{{通知Scheme}}", "en" to "{{SCHEME}}"),
+ mapOf("zh_CN" to "{{通话类型}}", "zh_TW" to "{{通話類型}}", "en" to "{{CALL_TYPE}}"),
+ mapOf("zh_CN" to "{{定位信息}}", "zh_TW" to "{{定位信息}}", "en" to "{{LOCATION}}"),
+ mapOf("zh_CN" to "{{定位信息_经度}}", "zh_TW" to "{{定位信息_經度}}", "en" to "{{LOCATION_LONGITUDE}}"),
+ mapOf("zh_CN" to "{{定位信息_纬度}}", "zh_TW" to "{{定位信息_緯度}}", "en" to "{{LOCATION_LATITUDE}}"),
+ mapOf("zh_CN" to "{{定位信息_地址}}", "zh_TW" to "{{定位信息_地址}}", "en" to "{{LOCATION_ADDRESS}}"),
+ mapOf("zh_CN" to "{{电池电量}}", "zh_TW" to "{{電池電量}}", "en" to "{{BATTERY_PCT}}"),
+ mapOf("zh_CN" to "{{电池状态}}", "zh_TW" to "{{電池狀態}}", "en" to "{{BATTERY_STATUS}}"),
+ mapOf("zh_CN" to "{{充电方式}}", "zh_TW" to "{{充電方式}}", "en" to "{{BATTERY_PLUGGED}}"),
+ mapOf("zh_CN" to "{{电池信息}}", "zh_TW" to "{{電池信息}}", "en" to "{{BATTERY_INFO}}")
+)
diff --git a/app/src/main/java/com/example/firstapp/utils/DrawableUtils.kt b/app/src/main/java/com/example/firstapp/utils/DrawableUtils.kt
new file mode 100644
index 0000000..73e183f
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/DrawableUtils.kt
@@ -0,0 +1,59 @@
+package com.example.firstapp.utils
+
+import android.graphics.drawable.GradientDrawable
+
+/**
+ * @author xuexiang
+ * @since 2019/4/7 下午12:57
+ */
+@Suppress("unused")
+class DrawableUtils private constructor() {
+ companion object {
+ /**
+ * 矩形
+ */
+ fun createRectangleDrawable(color: Int, cornerRadius: Float): GradientDrawable {
+ val gradientDrawable = GradientDrawable()
+ gradientDrawable.shape = GradientDrawable.RECTANGLE
+ gradientDrawable.cornerRadius = cornerRadius
+ gradientDrawable.setColor(color)
+ return gradientDrawable
+ }
+
+ /**
+ * 矩形
+ */
+ fun createRectangleDrawable(colors: IntArray?, cornerRadius: Float): GradientDrawable {
+ val gradientDrawable = GradientDrawable()
+ gradientDrawable.shape = GradientDrawable.RECTANGLE
+ gradientDrawable.cornerRadius = cornerRadius
+ gradientDrawable.colors = colors
+ return gradientDrawable
+ }
+
+ /**
+ * 圆形
+ */
+ fun createOvalDrawable(color: Int): GradientDrawable {
+ val gradientDrawable = GradientDrawable()
+ gradientDrawable.shape = GradientDrawable.OVAL
+ gradientDrawable.setColor(color)
+ return gradientDrawable
+ }
+
+ /**
+ * 圆形
+ */
+ fun createOvalDrawable(colors: IntArray?): GradientDrawable {
+ val gradientDrawable = GradientDrawable()
+ gradientDrawable.shape = GradientDrawable.OVAL
+ gradientDrawable.colors = colors
+ return gradientDrawable
+ }
+ }
+
+ init {
+ throw UnsupportedOperationException("Can not be instantiated.")
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/FrpcUtils.kt b/app/src/main/java/com/example/firstapp/utils/FrpcUtils.kt
new file mode 100644
index 0000000..88d48de
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/FrpcUtils.kt
@@ -0,0 +1,51 @@
+package com.example.firstapp.utils
+
+import android.app.ActivityManager
+import android.content.Context
+import io.reactivex.Completable
+import io.reactivex.Observable
+import io.reactivex.ObservableEmitter
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.util.concurrent.TimeUnit
+
+@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate")
+class FrpcUtils private constructor() {
+
+ companion object {
+
+ fun waitService(serviceName: String, context: Context): Completable {
+ return Completable.fromObservable(
+ Observable.interval(3, 1, TimeUnit.SECONDS)
+ .takeUntil { isServiceRunning(serviceName, context) }
+ )
+ }
+
+ fun isServiceRunning(serviceName: String, context: Context): Boolean {
+ val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val runningServices = am.getRunningServices(Int.MAX_VALUE) //获取运行的服务,参数表示最多返回的数量
+ for (runningServiceInfo in runningServices) {
+ val className = runningServiceInfo.service.className
+ if (className == serviceName) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun getStringFromRaw(context: Context, rawName: Int): Observable<String?> {
+ return Observable.create { emitter: ObservableEmitter<String?> ->
+ val reader = BufferedReader(InputStreamReader(context.resources.openRawResource(rawName)))
+ var line: String?
+ val result = StringBuilder()
+ while (reader.readLine().also { line = it } != null) {
+ result.append(line).append("\n")
+ }
+ reader.close()
+ emitter.onNext(result.toString())
+ emitter.onComplete()
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/HistoryUtils.kt b/app/src/main/java/com/example/firstapp/utils/HistoryUtils.kt
new file mode 100644
index 0000000..5c56dc2
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/HistoryUtils.kt
@@ -0,0 +1,115 @@
+package com.example.firstapp.utils
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import java.io.*
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * 转发历史工具类
+ *
+ * @author pppscn
+ * @since 2022年5月9日
+ */
+@Suppress("UNCHECKED_CAST", "unused")
+class HistoryUtils<T>(private val name: String, private val default: T) : ReadWriteProperty<Any?, T> {
+
+ companion object {
+ lateinit var preference: SharedPreferences
+
+ fun init(context: Context) {
+ preference = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val directBootContext: Context = context.createDeviceProtectedStorageContext()
+ directBootContext.getSharedPreferences(context.packageName + ".history", Context.MODE_PRIVATE)
+ } else {
+ context.getSharedPreferences(context.packageName + ".history", Context.MODE_PRIVATE)
+ }
+ }
+
+ //删除全部数据
+ fun clearPreference() = preference.edit().clear().apply()
+
+ //根据key删除存储数据
+ fun clearPreference(key: String) = preference.edit().remove(key).commit()
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ return putPreference(name, value)
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return getPreference(name, default)
+ }
+
+ /**
+ * 查找数据 返回给调用方法一个具体的对象
+ * 如果查找不到类型就采用反序列化方法来返回类型
+ * default是默认对象 以防止会返回空对象的异常
+ * 即如果name没有查找到value 就返回默认的序列化对象,然后经过反序列化返回
+ */
+ private fun getPreference(name: String, default: T): T = with(preference) {
+ val res: Any = when (default) {
+ is Long -> getLong(name, default)
+ is String -> this.getString(name, default)!!
+ is Int -> getInt(name, default)
+ is Boolean -> getBoolean(name, default)
+ is Float -> getFloat(name, default)
+ //else -> throw IllegalArgumentException("This type can be get from Preferences")
+ else -> deSerialization(getString(name, serialize(default)).toString())
+ }
+ return res as T
+ }
+
+ private fun putPreference(name: String, value: T) = with(preference.edit()) {
+ when (value) {
+ is Long -> putLong(name, value)
+ is Int -> putInt(name, value)
+ is String -> putString(name, value)
+ is Boolean -> putBoolean(name, value)
+ is Float -> putFloat(name, value)
+ //else -> throw IllegalArgumentException("This type can be saved into Preferences")
+ else -> putString(name, serialize(value))
+ }.apply()
+ }
+
+ /**
+ * 序列化对象
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ private fun <T> serialize(obj: T): String {
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ val objectOutputStream = ObjectOutputStream(
+ byteArrayOutputStream
+ )
+ objectOutputStream.writeObject(obj)
+ var serStr = byteArrayOutputStream.toString("ISO-8859-1")
+ serStr = java.net.URLEncoder.encode(serStr, "UTF-8")
+ objectOutputStream.close()
+ byteArrayOutputStream.close()
+ return serStr
+ }
+
+ /**
+ * 反序列化对象
+ * @param str
+ * @throws IOException
+ * @throws ClassNotFoundException
+ */
+ @Throws(IOException::class, ClassNotFoundException::class)
+ private fun <T> deSerialization(str: String): T {
+ val redStr = java.net.URLDecoder.decode(str, "UTF-8")
+ val byteArrayInputStream = ByteArrayInputStream(
+ redStr.toByteArray(charset("ISO-8859-1"))
+ )
+ val objectInputStream = ObjectInputStream(
+ byteArrayInputStream
+ )
+ val obj = objectInputStream.readObject() as T
+ objectInputStream.close()
+ byteArrayInputStream.close()
+ return obj
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/KeepAliveUtils.kt b/app/src/main/java/com/example/firstapp/utils/KeepAliveUtils.kt
new file mode 100644
index 0000000..c1bf255
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/KeepAliveUtils.kt
@@ -0,0 +1,52 @@
+package com.example.firstapp.utils
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.os.Build
+import android.os.PowerManager
+import android.provider.Settings
+import androidx.annotation.RequiresApi
+import com.example.firstapp.R
+
+@Suppress("DEPRECATION")
+class KeepAliveUtils private constructor() {
+
+ companion object {
+ fun isIgnoreBatteryOptimization(activity: Activity): Boolean {
+ //安卓6.0以下没有忽略电池优化
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ true
+ } else try {
+ val powerManager: PowerManager = activity.getSystemService(Context.POWER_SERVICE) as PowerManager
+ powerManager.isIgnoringBatteryOptimizations(activity.packageName)
+ } catch (e: Exception) {
+// XToastUtils.error(R.string.unsupport)
+ false
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ fun ignoreBatteryOptimization(activity: Activity) {
+ try {
+ if (isIgnoreBatteryOptimization(activity)) {
+ return
+ }
+ @SuppressLint("BatteryLife") val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
+ intent.data = Uri.parse("package:" + activity.packageName)
+ val resolveInfo: ResolveInfo? = activity.packageManager.resolveActivity(intent, 0)
+ if (resolveInfo != null) {
+ activity.startActivity(intent)
+ } else {
+// XToastUtils.error(R.string.unsupport)
+ }
+ } catch (e: Exception) {
+// XToastUtils.error(R.string.unsupport)
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/LocationUtils.kt b/app/src/main/java/com/example/firstapp/utils/LocationUtils.kt
new file mode 100644
index 0000000..2e17340
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/LocationUtils.kt
@@ -0,0 +1,72 @@
+package com.example.firstapp.utils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.location.LocationManager
+import android.os.Build
+import android.provider.Settings
+import androidx.core.content.ContextCompat
+
+@Suppress("DEPRECATION")
+object LocationUtils {
+
+ private const val LOCATION_MODE_OFF = 0
+
+ private fun hasLocationPermission(context: Context): Boolean {
+ val hasPermission = ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ com.example.firstapp.utils.Log.d("LocationUtils", "hasLocationPermission: $hasPermission")
+ return hasPermission
+ }
+
+ fun isLocationEnabled(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
+ com.example.firstapp.utils.Log.d(
+ "LocationUtils",
+ "isLocationEnabled: ${locationManager?.isLocationEnabled}"
+ )
+ locationManager?.isLocationEnabled == true
+ } else {
+ try {
+ val locationMode = Settings.Secure.getInt(
+ context.contentResolver,
+ Settings.Secure.LOCATION_MODE
+ )
+ com.example.firstapp.utils.Log.d(
+ "LocationUtils",
+ "isLocationEnabled: locationMode=$locationMode"
+ )
+ locationMode != com.example.firstapp.utils.LocationUtils.LOCATION_MODE_OFF
+ } catch (e: Settings.SettingNotFoundException) {
+ false
+ }
+ }
+ }
+
+ fun hasLocationCapability(context: Context): Boolean {
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
+
+ // 检查是否有位置权限
+ if (!com.example.firstapp.utils.LocationUtils.hasLocationPermission(context)) {
+ com.example.firstapp.utils.Log.e(
+ "LocationUtils",
+ "hasLocationCapability: no location permission"
+ )
+ return false
+ }
+
+ // 检查是否有定位能力
+ val hasGpsProvider = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true
+ val hasNetworkProvider = locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true
+ val hasPassiveProvider = locationManager?.isProviderEnabled(LocationManager.PASSIVE_PROVIDER) == true
+
+ com.example.firstapp.utils.Log.d(
+ "LocationUtils",
+ "hasLocationCapability: hasGpsProvider=$hasGpsProvider, hasNetworkProvider=$hasNetworkProvider, hasPassiveProvider=$hasPassiveProvider"
+ )
+ return hasGpsProvider || hasNetworkProvider || hasPassiveProvider
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/utils/Log.kt b/app/src/main/java/com/example/firstapp/utils/Log.kt
new file mode 100644
index 0000000..52467d6
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/Log.kt
@@ -0,0 +1,161 @@
+package com.example.firstapp.utils
+
+import android.content.Context
+import android.os.Build
+import com.example.firstapp.App
+import java.io.File
+import java.io.FileWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import android.util.Log as AndroidLog
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+object Log {
+ const val ASSERT = 7
+ const val DEBUG = 3
+ const val ERROR = 6
+ const val INFO = 4
+ const val VERBOSE = 2
+ const val WARN = 5
+
+ private const val TAG = "Logger"
+ private var logFile: File? = null
+ private lateinit var appContext: Context
+ private var initDate: String = ""
+
+ fun init(context: Context) {
+ appContext = context
+ createLogFile()
+ }
+
+ private fun createLogFile() {
+ val currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
+ if (currentDate != initDate || logFile == null || !logFile!!.exists()) {
+ initDate = currentDate
+ val logPath = appContext.cacheDir.absolutePath + "/logs"
+ val logDir = File(logPath)
+ if (!logDir.exists()) logDir.mkdirs()
+ logFile = File(logPath, "log_$currentDate.txt")
+ }
+ }
+
+ fun logToFile(level: String, tag: String, message: String) {
+ if (Build.DEVICE == null) return
+
+ if (!::appContext.isInitialized) {
+ throw IllegalStateException("Log not initialized. Call init(context) first.")
+ }
+
+ if (!App.isDebug) return
+
+ Thread {
+ try {
+ createLogFile()
+ logFile?.let { file ->
+ try {
+ val logTimeStamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
+ val logWriter = FileWriter(file, true)
+ logWriter.append("$logTimeStamp | $level | $tag | $message\n\n")
+ logWriter.close()
+ } catch (e: Exception) {
+ AndroidLog.e(TAG, "Error writing to file: ${e.message}")
+ }
+ }
+ } catch (e: Exception) {
+ AndroidLog.e(TAG, "Error writing to file: ${e.message}")
+ }
+ }.start()
+ }
+
+ fun v(tag: String, message: String) {
+ AndroidLog.v(tag, message)
+ logToFile("V", tag, message)
+ }
+
+ fun v(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.v(tag, logMessage)
+ logToFile("V", tag, logMessage)
+ }
+
+ fun d(tag: String, message: String) {
+ AndroidLog.d(tag, message)
+ logToFile("D", tag, message)
+ }
+
+ fun d(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.d(tag, logMessage)
+ logToFile("D", tag, logMessage)
+ }
+
+ fun i(tag: String, message: String) {
+ AndroidLog.d(tag, message)
+ logToFile("I", tag, message)
+ }
+
+ fun i(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.d(tag, logMessage)
+ logToFile("I", tag, logMessage)
+ }
+
+ fun w(tag: String, message: String) {
+ AndroidLog.w(tag, message)
+ logToFile("W", tag, message)
+ }
+
+ fun w(tag: String, throwable: Throwable) {
+ val logMessage = getStackTraceString(throwable)
+ AndroidLog.w(tag, logMessage)
+ logToFile("W", tag, logMessage)
+ }
+
+ fun w(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.w(tag, logMessage)
+ logToFile("W", tag, logMessage)
+ }
+
+ fun e(tag: String, message: String) {
+ AndroidLog.e(tag, message)
+ logToFile("E", tag, message)
+ }
+
+ fun e(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.e(tag, logMessage)
+ logToFile("E", tag, logMessage)
+ }
+
+ fun wtf(tag: String, message: String) {
+ AndroidLog.wtf(tag, message)
+ logToFile("WTF", tag, message)
+ }
+
+ fun wtf(tag: String, throwable: Throwable) {
+ val logMessage = getStackTraceString(throwable)
+ AndroidLog.wtf(tag, logMessage)
+ logToFile("WTF", tag, logMessage)
+ }
+
+ fun wtf(tag: String, message: String, throwable: Throwable) {
+ val logMessage = "${message}\n${getStackTraceString(throwable)}"
+ AndroidLog.wtf(tag, logMessage)
+ logToFile("WTF", tag, logMessage)
+ }
+
+ fun getStackTraceString(throwable: Throwable): String {
+ return AndroidLog.getStackTraceString(throwable)
+ }
+
+ fun isLoggable(tag: String?, level: Int): Boolean {
+ return AndroidLog.isLoggable(tag, level)
+ }
+
+ fun println(priority: Int, tag: String, message: String) {
+ AndroidLog.println(priority, tag, message)
+ logToFile("P", tag, message)
+ }
+}
diff --git a/app/src/main/java/com/example/firstapp/utils/RSACrypt.kt b/app/src/main/java/com/example/firstapp/utils/RSACrypt.kt
new file mode 100644
index 0000000..1668ce7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/RSACrypt.kt
@@ -0,0 +1,209 @@
+package com.example.firstapp.utils
+
+import java.io.ByteArrayOutputStream
+import java.security.KeyFactory
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.spec.X509EncodedKeySpec
+import javax.crypto.Cipher
+
+/**
+ * 非对称加密RSA加密和解密
+ */
+object RSACrypt {
+
+ private const val TRANSFORMATION = "RSA"
+ private const val ENCRYPT_MAX_SIZE = 245
+ private const val DECRYPT_MAX_SIZE = 256
+
+ /**
+ * 私钥加密
+ * @param input 原文
+ * @param privateKey 私钥
+ */
+ fun encryptByPrivateKey(input: String, privateKey: PrivateKey): String {
+
+ //创建cipher对象
+ val cipher = Cipher.getInstance(TRANSFORMATION)
+ //初始化cipher
+ cipher.init(Cipher.ENCRYPT_MODE, privateKey)
+
+ //****非对称加密****
+ val byteArray = input.toByteArray()
+
+ //分段加密
+ var temp: ByteArray?
+ var offset = 0 //当前偏移的位置
+
+ val outputStream = ByteArrayOutputStream()
+
+ //拆分input
+ while (byteArray.size - offset > 0) {
+ //每次最大加密245个字节
+ if (byteArray.size - offset >= ENCRYPT_MAX_SIZE) {
+ //剩余部分大于245
+ //加密完整245
+ temp = cipher.doFinal(byteArray, offset, ENCRYPT_MAX_SIZE)
+ //重新计算偏移位置
+ offset += ENCRYPT_MAX_SIZE
+ } else {
+ //加密最后一块
+ temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
+ //重新计算偏移位置
+ offset = byteArray.size
+ }
+ //存储到临时的缓冲区
+ outputStream.write(temp)
+ }
+ outputStream.close()
+
+ return Base64.encode(outputStream.toByteArray())
+
+ }
+
+ /**
+ * 公钥加密
+ * @param input 原文
+ * @param publicKey 公钥
+ */
+ fun encryptByPublicKey(input: String, publicKey: PublicKey): String {
+
+ //创建cipher对象
+ val cipher = Cipher.getInstance(TRANSFORMATION)
+ //初始化cipher
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey)
+
+ //****非对称加密****
+ val byteArray = input.toByteArray()
+
+ var temp: ByteArray?
+ var offset = 0 //当前偏移的位置
+
+ val outputStream = ByteArrayOutputStream()
+
+ //拆分input
+ while (byteArray.size - offset > 0) {
+ //每次最大加密117个字节
+ if (byteArray.size - offset >= ENCRYPT_MAX_SIZE) {
+ //剩余部分大于117
+ //加密完整117
+ temp = cipher.doFinal(byteArray, offset, ENCRYPT_MAX_SIZE)
+ //重新计算偏移位置
+ offset += ENCRYPT_MAX_SIZE
+ } else {
+ //加密最后一块
+ temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
+ //重新计算偏移位置
+ offset = byteArray.size
+ }
+ //存储到临时的缓冲区
+ outputStream.write(temp)
+ }
+ outputStream.close()
+
+ return Base64.encode(outputStream.toByteArray())
+
+ }
+
+ /**
+ * 私钥解密
+ * @param input 秘文
+ * @param privateKey 私钥
+ */
+ fun decryptByPrivateKey(input: String, privateKey: PrivateKey): String {
+
+ //创建cipher对象
+ val cipher = Cipher.getInstance(TRANSFORMATION)
+ //初始化cipher
+ cipher.init(Cipher.DECRYPT_MODE, privateKey)
+
+ //****非对称加密****
+ val byteArray = Base64.decode(input)
+
+ //分段解密
+ var temp: ByteArray?
+ var offset = 0 //当前偏移的位置
+
+ val outputStream = ByteArrayOutputStream()
+
+ //拆分input
+ while (byteArray.size - offset > 0) {
+ //每次最大解密256个字节
+ if (byteArray.size - offset >= DECRYPT_MAX_SIZE) {
+
+ temp = cipher.doFinal(byteArray, offset, DECRYPT_MAX_SIZE)
+ //重新计算偏移位置
+ offset += DECRYPT_MAX_SIZE
+ } else {
+ //加密最后一块
+ temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
+ //重新计算偏移位置
+ offset = byteArray.size
+ }
+ //存储到临时的缓冲区
+ outputStream.write(temp)
+ }
+ outputStream.close()
+
+ return String(outputStream.toByteArray())
+
+ }
+
+ /**
+ * 公钥解密
+ * @param input 秘文
+ * @param publicKey 公钥
+ */
+ fun decryptByPublicKey(input: String, publicKey: PublicKey): String {
+
+ //创建cipher对象
+ val cipher = Cipher.getInstance(TRANSFORMATION)
+ //初始化cipher
+ cipher.init(Cipher.DECRYPT_MODE, publicKey)
+
+ //****非对称加密****
+ val byteArray = Base64.decode(input)
+
+ //分段解密
+ var temp: ByteArray?
+ var offset = 0 //当前偏移的位置
+
+ val outputStream = ByteArrayOutputStream()
+
+ //拆分input
+ while (byteArray.size - offset > 0) {
+ //每次最大解密256个字节
+ if (byteArray.size - offset >= DECRYPT_MAX_SIZE) {
+
+ temp = cipher.doFinal(byteArray, offset, DECRYPT_MAX_SIZE)
+ //重新计算偏移位置
+ offset += DECRYPT_MAX_SIZE
+ } else {
+ //加密最后一块
+ temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
+ //重新计算偏移位置
+ offset = byteArray.size
+ }
+ //存储到临时的缓冲区
+ outputStream.write(temp)
+ }
+ outputStream.close()
+
+ return String(outputStream.toByteArray())
+
+ }
+
+ fun getPrivateKey(privateKeyStr: String): PrivateKey {
+ //字符串转成秘钥对对象
+ val generator = KeyFactory.getInstance("RSA")
+ return generator.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privateKeyStr)))
+ }
+
+ fun getPublicKey(publicKeyStr: String): PublicKey {
+ //字符串转成秘钥对对象
+ val kf = KeyFactory.getInstance("RSA")
+ return kf.generatePublic(X509EncodedKeySpec(Base64.decode(publicKeyStr)))
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/RandomUtils.kt b/app/src/main/java/com/example/firstapp/utils/RandomUtils.kt
new file mode 100644
index 0000000..ac098df
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/RandomUtils.kt
@@ -0,0 +1,293 @@
+package com.example.firstapp.utils
+
+import android.graphics.Color
+import android.text.TextUtils
+import java.util.*
+
+/**
+ * <pre>
+ * desc : Random Utils
+ * author : xuexiang
+ * time : 2018/4/28 上午12:41
+</pre> *
+ *
+ * Shuffling algorithm
+ * * [.shuffle] Shuffling algorithm, Randomly permutes the specified array using a default source of
+ * randomness
+ * * [.shuffle] Shuffling algorithm, Randomly permutes the specified array
+ * * [.shuffle] Shuffling algorithm, Randomly permutes the specified int array using a default source of
+ * randomness
+ * * [.shuffle] Shuffling algorithm, Randomly permutes the specified int array
+ *
+ *
+ * get random int
+ * * [.getRandom] get random int between 0 and max
+ * * [.getRandom] get random int between min and max
+ *
+ *
+ * get random numbers or letters
+ * * [.getRandomCapitalLetters] get a fixed-length random string, its a mixture of uppercase letters
+ * * [.getRandomLetters] get a fixed-length random string, its a mixture of uppercase and lowercase letters
+ *
+ * * [.getRandomLowerCaseLetters] get a fixed-length random string, its a mixture of lowercase letters
+ * * [.getRandomNumbers] get a fixed-length random string, its a mixture of numbers
+ * * [.getRandomNumbersAndLetters] get a fixed-length random string, its a mixture of uppercase, lowercase
+ * letters and numbers
+ * * [.getRandom] get a fixed-length random string, its a mixture of chars in source
+ * * [.getRandom] get a fixed-length random string, its a mixture of chars in sourceChar
+ *
+ *
+ */
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+class RandomUtils private constructor() {
+ companion object {
+ private const val NUMBERS_AND_LETTERS =
+ "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ private const val NUMBERS = "0123456789"
+ private const val LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ private const val CAPITAL_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ private const val LOWER_CASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"
+
+ /**
+ * 在数字和英文字母中获取一个定长的随机字符串
+ *
+ * @param length 长度
+ * @return 随机字符串
+ * @see RandomUtils.getRandom
+ */
+ @JvmStatic
+ fun getRandomNumbersAndLetters(length: Int): String? {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ com.example.firstapp.utils.RandomUtils.Companion.NUMBERS_AND_LETTERS,
+ length
+ )
+ }
+
+ /**
+ * 在数字中获取一个定长的随机字符串
+ *
+ * @param length 长度
+ * @return 随机数字符串
+ * @see RandomUtils.getRandom
+ */
+ fun getRandomNumbers(length: Int): String? {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ com.example.firstapp.utils.RandomUtils.Companion.NUMBERS,
+ length
+ )
+ }
+
+ /**
+ * 在英文字母中获取一个定长的随机字符串
+ *
+ * @param length 长度
+ * @return 随机字母字符串
+ * @see RandomUtils.getRandom
+ */
+ fun getRandomLetters(length: Int): String? {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ com.example.firstapp.utils.RandomUtils.Companion.LETTERS,
+ length
+ )
+ }
+
+ /**
+ * 在大写英文字母中获取一个定长的随机字符串
+ *
+ * @param length 长度
+ * @return 随机字符串 只包含大写字母
+ * @see RandomUtils.getRandom
+ */
+ fun getRandomCapitalLetters(length: Int): String? {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ com.example.firstapp.utils.RandomUtils.Companion.CAPITAL_LETTERS,
+ length
+ )
+ }
+
+ /**
+ * 在小写英文字母中获取一个定长的随机字符串
+ *
+ * @param length 长度
+ * @return 随机字符串 只包含小写字母
+ * @see RandomUtils.getRandom
+ */
+ fun getRandomLowerCaseLetters(length: Int): String? {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ com.example.firstapp.utils.RandomUtils.Companion.LOWER_CASE_LETTERS,
+ length
+ )
+ }
+
+ /**
+ * 在一个字符数组源中获取一个定长的随机字符串
+ *
+ * @param source 源字符串
+ * @param length 长度
+ * @return
+ * * if source is null or empty, return null
+ * * else see [RandomUtils.getRandom]
+ *
+ */
+ fun getRandom(source: String, length: Int): String? {
+ return if (TextUtils.isEmpty(source)) null else com.example.firstapp.utils.RandomUtils.Companion.getRandom(
+ source.toCharArray(),
+ length
+ )
+ }
+
+ /**
+ * 在一个字符数组源中获取一个定长的随机字符串
+ *
+ * @param sourceChar 字符数组源
+ * @param length 长度
+ * @return
+ * * if sourceChar is null or empty, return null
+ * * if length less than 0, return null
+ *
+ */
+ fun getRandom(sourceChar: CharArray?, length: Int): String? {
+ if (sourceChar == null || sourceChar.isEmpty() || length < 0) {
+ return null
+ }
+ val str = StringBuilder(length)
+ val random = Random()
+ for (i in 0 until length) {
+ str.append(sourceChar[random.nextInt(sourceChar.size)])
+ }
+ return str.toString()
+ }
+
+ /**
+ * get random int between 0 and max
+ *
+ * @param max 最大随机数
+ * @return
+ * * if max <= 0, return 0
+ * * else return random int between 0 and max
+ *
+ */
+ fun getRandom(max: Int): Int {
+ return com.example.firstapp.utils.RandomUtils.Companion.getRandom(0, max)
+ }
+
+ /**
+ * get random int between min and max
+ *
+ * @param min 最小随机数
+ * @param max 最大随机数
+ * @return
+ * * if min > max, return 0
+ * * if min == max, return min
+ * * else return random int between min and max
+ *
+ */
+ fun getRandom(min: Int, max: Int): Int {
+ if (min > max) {
+ return 0
+ }
+ return if (min == max) {
+ min
+ } else min + Random().nextInt(max - min)
+ }
+
+ /**
+ * 获取随机颜色
+ *
+ * @return
+ */
+ val randomColor: Int
+ get() {
+ val random = Random()
+ val r = random.nextInt(256)
+ val g = random.nextInt(256)
+ val b = random.nextInt(256)
+ return Color.rgb(r, g, b)
+ }
+
+ /**
+ * 随机打乱数组中的内容
+ *
+ * @param objArray
+ * @return
+ */
+ fun shuffle(objArray: Array<Any?>?): Boolean {
+ return if (objArray == null) {
+ false
+ } else com.example.firstapp.utils.RandomUtils.Companion.shuffle(
+ objArray,
+ com.example.firstapp.utils.RandomUtils.Companion.getRandom(objArray.size)
+ )
+ }
+
+ /**
+ * 随机打乱数组中的内容
+ *
+ * @param objArray
+ * @param shuffleCount
+ * @return
+ */
+ private fun shuffle(objArray: Array<Any?>?, shuffleCount: Int): Boolean {
+ var length = 0
+ if (objArray == null || shuffleCount < 0 || objArray.size.also {
+ length = it
+ } < shuffleCount) {
+ return false
+ }
+ for (i in 1..shuffleCount) {
+ val random = com.example.firstapp.utils.RandomUtils.Companion.getRandom(length - i)
+ val temp = objArray[length - i]
+ objArray[length - i] = objArray[random]
+ objArray[random] = temp
+ }
+ return true
+ }
+
+ /**
+ * 随机打乱数组中的内容
+ *
+ * @param intArray
+ * @return
+ */
+ fun shuffle(intArray: IntArray?): IntArray? {
+ return if (intArray == null) {
+ null
+ } else com.example.firstapp.utils.RandomUtils.Companion.shuffle(
+ intArray,
+ com.example.firstapp.utils.RandomUtils.Companion.getRandom(intArray.size)
+ )
+ }
+
+ /**
+ * 随机打乱数组中的内容
+ *
+ * @param intArray
+ * @param shuffleCount
+ * @return
+ */
+ fun shuffle(intArray: IntArray?, shuffleCount: Int): IntArray? {
+ var length = 0
+ if (intArray == null || shuffleCount < 0 || intArray.size.also {
+ length = it
+ } < shuffleCount) {
+ return null
+ }
+ val out = IntArray(shuffleCount)
+ for (i in 1..shuffleCount) {
+ val random = com.example.firstapp.utils.RandomUtils.Companion.getRandom(length - i)
+ out[i - 1] = intArray[random]
+ val temp = intArray[length - i]
+ intArray[length - i] = intArray[random]
+ intArray[random] = temp
+ }
+ return out
+ }
+ }
+
+ /**
+ * Don't let anyone instantiate this class.
+ */
+ init {
+ throw Error("Do not need instantiate!")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/SM4Crypt.kt b/app/src/main/java/com/example/firstapp/utils/SM4Crypt.kt
new file mode 100644
index 0000000..1cde9f7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/SM4Crypt.kt
@@ -0,0 +1,62 @@
+package com.example.firstapp.utils
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import java.security.SecureRandom
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * SM4分组密码算法是我国自主设计的分组对称密码算法
+ */
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+object SM4Crypt {
+
+ const val SM4_CBC_NOPADDING = "SM4/CBC/NoPadding"
+ const val SM4_CBC_PKCS5 = "SM4/CBC/PKCS5Padding"
+ const val SM4_CBC_PKCS7 = "SM4/CBC/PKCS7Padding"
+ const val SM4_ECB_NOPADDING = "SM4/ECB/NoPadding"
+ const val SM4_ECB_PKCS5 = "SM4/ECB/PKCS5Padding"
+ const val SM4_ECB_PKCS7 = "SM4/ECB/PKCS7Padding"
+ private val BC_PROVIDER = BouncyCastleProvider()
+ private val SM4_CBC_IV = byteArrayOf(3, 5, 6, 9, 6, 9, 5, 9, 3, 5, 6, 9, 6, 9, 5, 9)
+
+ /**
+ * 获取随机密钥
+ */
+ fun createSM4Key(): ByteArray {
+ val seed = ByteArray(16)
+ val random = SecureRandom()
+ random.nextBytes(seed)
+ return seed
+ }
+
+ @JvmOverloads
+ fun encrypt(source: ByteArray, key: ByteArray, mode: String = SM4_CBC_PKCS7, iv: ByteArray? = SM4_CBC_IV): ByteArray {
+ return doSM4(true, source, key, mode, iv)
+ }
+
+ @JvmOverloads
+ fun decrypt(source: ByteArray, key: ByteArray, mode: String = SM4_CBC_PKCS7, iv: ByteArray? = SM4_CBC_IV): ByteArray {
+ return doSM4(false, source, key, mode, iv)
+ }
+
+ private fun doSM4(forEncryption: Boolean, source: ByteArray, key: ByteArray, mode: String, iv: ByteArray?): ByteArray {
+ return try {
+ val cryptMode = if (forEncryption) 1 else 2
+ val sm4Key = SecretKeySpec(key, "SM4")
+ val cipher = Cipher.getInstance(mode, BC_PROVIDER)
+ if (iv == null) {
+ cipher.init(cryptMode, sm4Key)
+ } else {
+ val ivParameterSpec = IvParameterSpec(iv)
+ cipher.init(cryptMode, sm4Key, ivParameterSpec)
+ }
+ cipher.doFinal(source)
+ } catch (var9: Exception) {
+ var9.printStackTrace()
+ ByteArray(0)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/SettingUtils.kt b/app/src/main/java/com/example/firstapp/utils/SettingUtils.kt
new file mode 100644
index 0000000..02dd9a9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/SettingUtils.kt
@@ -0,0 +1,166 @@
+package com.example.firstapp.utils
+
+import android.location.Criteria
+import com.example.firstapp.R
+
+import com.xuexiang.xutil.resource.ResUtils.getString
+
+class SettingUtils private constructor() {
+ companion object {
+
+ //是否启动时检查更新
+ var autoCheckUpdate: Boolean by SharedPreference(AUTO_CHECK_UPDATE, true)
+
+ //是否加入SmsF预览体验计划
+ var joinPreviewProgram: Boolean by SharedPreference(JOIN_PREVIEW_PROGRAM, false)
+
+ //是否同意隐私政策
+ var isAgreePrivacy: Boolean by SharedPreference(IS_AGREE_PRIVACY_KEY, false)
+
+ //是否转发短信
+ var enableSms: Boolean by SharedPreference(SP_ENABLE_SMS, false)
+
+ //是否转发通话
+ var enablePhone: Boolean by SharedPreference(SP_ENABLE_PHONE, false)
+
+ //是否转发通话——来电挂机
+ var enableCallType1: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_1, false)
+
+ //是否转发通话——去电挂机
+ var enableCallType2: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_2, false)
+
+ //是否转发通话——未接来电
+ var enableCallType3: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_3, false)
+
+ //是否转发通话——来电提醒
+ var enableCallType4: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_4, false)
+
+ //是否转发通话——来电接通
+ var enableCallType5: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_5, false)
+
+ //是否转发通话——去电拨出
+ var enableCallType6: Boolean by SharedPreference(SP_ENABLE_CALL_TYPE_6, false)
+
+ //是否转发应用通知
+ var enableAppNotify: Boolean by SharedPreference(SP_ENABLE_APP_NOTIFY, false)
+
+ //是否接受短信指令
+ var enableSmsCommand: Boolean by SharedPreference(SP_ENABLE_SMS_COMMAND, false)
+ var smsCommandSafePhone: String by SharedPreference(SP_SMS_COMMAND_SAFE_PHONE, "")
+
+ //是否转发应用通知——自动消除通知
+ var enableCancelAppNotify: Boolean by SharedPreference(SP_ENABLE_CANCEL_APP_NOTIFY, false)
+
+ //是否转发应用通知——自动消除额外APP通知
+ var cancelExtraAppNotify: String by SharedPreference(SP_CANCEL_EXTRA_APP_NOTIFY, "")
+
+ //是否转发应用通知——仅锁屏状态
+ var enableNotUserPresent: Boolean by SharedPreference(SP_ENABLE_NOT_USER_PRESENT, false)
+
+ //是否加载应用列表
+ var enableLoadAppList: Boolean by SharedPreference(ENABLE_LOAD_APP_LIST, false)
+
+ //是否加载应用列表——用户应用
+ var enableLoadUserAppList: Boolean by SharedPreference(ENABLE_LOAD_USER_APP_LIST, false)
+
+ //是否加载应用列表——系统应用
+ var enableLoadSystemAppList: Boolean by SharedPreference(ENABLE_LOAD_SYSTEM_APP_LIST, false)
+
+ //过滤多久内重复消息
+ var duplicateMessagesLimits: Int by SharedPreference(SP_DUPLICATE_MESSAGES_LIMITS, 0)
+
+ //免打扰(禁用转发)时间段——开始
+ var silentPeriodStart: Int by SharedPreference(SP_SILENT_PERIOD_START, 0)
+
+ //免打扰(禁用转发)时间段——结束
+ var silentPeriodEnd: Int by SharedPreference(SP_SILENT_PERIOD_END, 0)
+
+ //免打扰(禁用转发)时间段——记录日志
+ var enableSilentPeriodLogs: Boolean by SharedPreference(SP_ENABLE_SILENT_PERIOD_LOGS, false)
+
+ //是否不在最近任务列表中显示
+ var enableExcludeFromRecents: Boolean by SharedPreference(SP_ENABLE_EXCLUDE_FROM_RECENTS, false)
+
+ //是否转发应用通知
+ var enableCactus: Boolean by SharedPreference(SP_ENABLE_CACTUS, false)
+
+ //是否播放静音音乐
+ var enablePlaySilenceMusic: Boolean by SharedPreference(SP_ENABLE_PLAY_SILENCE_MUSIC, false)
+
+ //是否启用1像素
+ var enableOnePixelActivity: Boolean by SharedPreference(SP_ENABLE_ONE_PIXEL_ACTIVITY, false)
+
+ //请求接口失败重试次数
+ var requestRetryTimes: Int by SharedPreference(SP_REQUEST_RETRY_TIMES, 0)
+
+ //请求接口失败重试间隔(秒)
+ var requestDelayTime: Int by SharedPreference(SP_REQUEST_DELAY_TIME, 1)
+
+ //请求接口失败超时时间(秒)
+ var requestTimeout: Int by SharedPreference(SP_REQUEST_TIMEOUT, 10)
+
+ //通知内容
+ var notifyContent: String by SharedPreference(SP_NOTIFY_CONTENT, getString(R.string.notification_content))
+
+ //设备名称
+ var extraDeviceMark: String by SharedPreference(SP_EXTRA_DEVICE_MARK, "")
+
+ //SIM1主键
+ var subidSim1: Int by SharedPreference(SP_SUBID_SIM1, 0)
+
+ //SIM2主键
+ var subidSim2: Int by SharedPreference(SP_SUBID_SIM2, 0)
+
+ //SIM1备注
+ var extraSim1: String by SharedPreference(SP_EXTRA_SIM1, "")
+
+ //SIM2备注
+ var extraSim2: String by SharedPreference(SP_EXTRA_SIM2, "")
+
+ //是否启用自定义模板
+ var enableSmsTemplate: Boolean by SharedPreference(SP_ENABLE_SMS_TEMPLATE, false)
+
+ //自定义模板
+ var smsTemplate: String by SharedPreference(SP_SMS_TEMPLATE, "")
+
+ //是否纯客户端模式
+ var enablePureClientMode: Boolean by SharedPreference(SP_PURE_CLIENT_MODE, false)
+
+ //是否纯任务模式
+ var enablePureTaskMode: Boolean by SharedPreference(SP_PURE_TASK_MODE, false)
+
+ //是否调试模式
+ var enableDebugMode: Boolean by SharedPreference(SP_DEBUG_MODE, false)
+
+ //是否启用定位功能
+ var enableLocation: Boolean by SharedPreference(SP_LOCATION, false)
+
+ //设置位置精度:高精度
+ var locationAccuracy: Int by SharedPreference(SP_LOCATION_ACCURACY, Criteria.ACCURACY_FINE)
+
+ //设置电量消耗:低电耗
+ var locationPowerRequirement: Int by SharedPreference(SP_LOCATION_POWER_REQUIREMENT, Criteria.POWER_LOW)
+
+ //设置位置更新最小时间间隔(单位:毫秒); 默认间隔:10000毫秒,最小间隔:1000毫秒
+ var locationMinInterval: Long by SharedPreference(SP_LOCATION_MIN_INTERVAL, 10000L)
+
+ //设置位置更新最小距离(单位:米);默认距离:0米
+ var locationMinDistance: Int by SharedPreference(SP_LOCATION_MIN_DISTANCE, 0)
+
+ //是否跟随系统语言
+ //var isFlowSystemLanguage: Boolean by SharedPreference(SP_IS_FLOW_SYSTEM_LANGUAGE, false)
+
+ //是否启用发现蓝牙设备服务
+ var enableBluetooth: Boolean by SharedPreference(SP_BLUETOOTH, false)
+
+ //扫描蓝牙设备间隔
+ var bluetoothScanInterval: Long by SharedPreference(SP_BLUETOOTH_SCAN_INTERVAL, 10000L)
+
+ //是否忽略匿名设备
+ var bluetoothIgnoreAnonymous: Boolean by SharedPreference(SP_BLUETOOTH_IGNORE_ANONYMOUS, true)
+ }
+
+ init {
+ throw UnsupportedOperationException("u can't instantiate me...")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/SharedPreference.kt b/app/src/main/java/com/example/firstapp/utils/SharedPreference.kt
new file mode 100644
index 0000000..733a6b5
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/SharedPreference.kt
@@ -0,0 +1,131 @@
+package com.example.firstapp.utils
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import java.io.*
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+@Suppress("unused", "UNCHECKED_CAST")
+class SharedPreference<T>(private val name: String, private val default: T) : ReadWriteProperty<Any?, T> {
+
+ companion object {
+ lateinit var preference: SharedPreferences
+
+ fun init(context: Context) {
+ preference = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ val directBootContext: Context = context.createDeviceProtectedStorageContext()
+ directBootContext.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+ } else {
+ context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+ }
+ }
+
+ //删除全部数据
+ fun clearPreference() = preference.edit().clear().apply()
+
+ //根据key删除存储数据
+ fun clearPreference(key: String) = preference.edit().remove(key).commit()
+
+ //导出全部数据
+ fun exportPreference(): String {
+ return serialize(preference.all)
+ }
+
+ //导入全部数据
+ fun importPreference(data: String) {
+ val map = deSerialization<Map<String, Any>>(data)
+ val editor = preference.edit()
+ for ((key, value) in map) {
+ when (value) {
+ is Long -> editor.putLong(key, value)
+ is Int -> editor.putInt(key, value)
+ is String -> editor.putString(key, value)
+ is Boolean -> editor.putBoolean(key, value)
+ is Float -> editor.putFloat(key, value)
+ else -> editor.putString(key, serialize(value))
+ }
+ }
+ editor.apply()
+ }
+
+ /**
+ * 序列化对象
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ private fun <T> serialize(obj: T): String {
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ val objectOutputStream = ObjectOutputStream(
+ byteArrayOutputStream
+ )
+ objectOutputStream.writeObject(obj)
+ var serStr = byteArrayOutputStream.toString("ISO-8859-1")
+ serStr = java.net.URLEncoder.encode(serStr, "UTF-8")
+ objectOutputStream.close()
+ byteArrayOutputStream.close()
+ return serStr
+ }
+
+ /**
+ * 反序列化对象
+ * @param str
+ * @throws IOException
+ * @throws ClassNotFoundException
+ */
+ @Throws(IOException::class, ClassNotFoundException::class)
+ private fun <T> deSerialization(str: String): T {
+ val redStr = java.net.URLDecoder.decode(str, "UTF-8")
+ val byteArrayInputStream = ByteArrayInputStream(
+ redStr.toByteArray(charset("ISO-8859-1"))
+ )
+ val objectInputStream = ObjectInputStream(
+ byteArrayInputStream
+ )
+ val obj = objectInputStream.readObject() as T
+ objectInputStream.close()
+ byteArrayInputStream.close()
+ return obj
+ }
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ return putPreference(name, value)
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ return getPreference(name, default)
+ }
+
+ /**
+ * 查找数据 返回给调用方法一个具体的对象
+ * 如果查找不到类型就采用反序列化方法来返回类型
+ * default是默认对象 以防止会返回空对象的异常
+ * 即如果name没有查找到value 就返回默认的序列化对象,然后经过反序列化返回
+ */
+ private fun getPreference(name: String, default: T): T = with(preference) {
+ val res: Any = when (default) {
+ is Long -> getLong(name, default)
+ is String -> this.getString(name, default)!!
+ is Int -> getInt(name, default)
+ is Boolean -> getBoolean(name, default)
+ is Float -> getFloat(name, default)
+ //else -> throw IllegalArgumentException("This type can be get from Preferences")
+ else -> deSerialization(getString(name, serialize(default)).toString())
+ }
+ return res as T
+ }
+
+ private fun putPreference(name: String, value: T) = with(preference.edit()) {
+ when (value) {
+ is Long -> putLong(name, value)
+ is Int -> putInt(name, value)
+ is String -> putString(name, value)
+ is Boolean -> putBoolean(name, value)
+ is Float -> putFloat(name, value)
+ //else -> throw IllegalArgumentException("This type can be saved into Preferences")
+ else -> putString(name, serialize(value))
+ }.apply()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/mail/EmailSender.kt b/app/src/main/java/com/example/firstapp/utils/mail/EmailSender.kt
new file mode 100644
index 0000000..54cb26e
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/mail/EmailSender.kt
@@ -0,0 +1,174 @@
+package com.example.firstapp.utils.mail
+
+import android.text.Html
+import android.text.Spanned
+import com.example.firstapp.utils.Log
+import org.bouncycastle.openpgp.PGPPublicKeyRing
+import org.bouncycastle.openpgp.PGPSecretKeyRing
+import java.io.File
+import java.security.PrivateKey
+import java.security.cert.X509Certificate
+import java.util.Properties
+import javax.mail.Authenticator
+import javax.mail.PasswordAuthentication
+
+@Suppress("PrivatePropertyName", "DEPRECATION")
+class EmailSender(
+ // SMTP参数
+ private val host: String, // SMTP服务器地址
+ private val port: String, // SMTP服务器端口
+ private val from: String, // 发件人邮箱
+ private val password: String, // 发件人邮箱密码/授权码
+ // 邮件参数
+ private val nickname: String, // 发件人昵称
+ private val subject: String, // 邮件主题
+ private val body: CharSequence, // 邮件正文
+ private val attachFiles: MutableList<File> = mutableListOf(), // 附件
+ // 收件人参数
+ private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
+ private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
+ private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
+ // 监听器
+ private val listener: EmailTaskListener? = null,
+ // 安全选项
+ private val openSSL: Boolean = false, //是否开启ssl验证 默认关闭
+ private val sslFactory: String = "javax.net.ssl.SSLSocketFactory", //SSL构建类名
+ private val startTls: Boolean = false, //是否开启starttls加密方式 默认关闭
+ // 邮件加密方式: S/MIME、OpenPGP、Plain(不传证书)
+ private val encryptionProtocol: String = "S/MIME",
+ // 邮件 S/MIME 加密和签名
+ private val recipientX509Cert: X509Certificate? = null, //收件人公钥(用于加密)
+ private val senderPrivateKey: PrivateKey? = null, //发件人私玥(用于签名)
+ private val senderX509Cert: X509Certificate? = null, //发件人公玥(用于签名)
+ //邮件 PGP 加密和签名
+ private var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null, // 收件人公钥(用于加密)
+ private var senderPGPSecretKeyRing: PGPSecretKeyRing? = null, // 发件人私钥(用于签名)
+ private val senderPGPSecretKeyPassword: String = "", // 发件人私钥密码
+) {
+
+ private val TAG: String = EmailSender::class.java.simpleName
+
+ private val properties: Properties = Properties().apply {
+ // 设置邮件服务器的主机名
+ put("mail.smtp.host", host)
+ // 设置邮件服务器的端口号
+ put("mail.smtp.port", port)
+ // 设置是否需要身份验证
+ put("mail.smtp.auth", "true")
+ // 设置是否启用 SSL 连接
+ if (openSSL) {
+ put("mail.smtp.ssl.enable", "true")
+ put("mail.smtp.socketFactory.class", sslFactory)
+ }
+ // 设置是否启用 TLS 连接
+ if (startTls) {
+ put("mail.smtp.starttls.enable", "true")
+ }
+ }
+
+ suspend fun sendEmail() {
+ try {
+ val authenticator = MailAuthenticator(from, password)
+ // 邮件正文
+ val html = try {
+ if (body is Spanned) Html.toHtml(body) else body.toString()
+ } catch (e: Exception) {
+ body.toString()
+ }
+
+ // 发送 S/MIME 邮件
+ when (encryptionProtocol) {
+ "S/MIME" -> {
+ val smimeUtils = SmimeUtils(
+ properties,
+ authenticator,
+ from,
+ nickname,
+ subject,
+ html,
+ attachFiles,
+ toAddress,
+ ccAddress,
+ bccAddress,
+ recipientX509Cert,
+ senderPrivateKey,
+ senderX509Cert,
+ )
+ val isEncrypt: Boolean = recipientX509Cert != null
+ val isSign: Boolean = senderX509Cert != null && senderPrivateKey != null
+ Log.d(TAG, "isEncrypt=$isEncrypt, isSign=$isSign")
+ val result = when {
+ isEncrypt && isSign -> smimeUtils.sendSignedAndEncryptedEmail()
+ isEncrypt -> smimeUtils.sendEncryptedEmail()
+ isSign -> smimeUtils.sendSignedEmail()
+ else -> smimeUtils.sendPlainEmail()
+ }
+ listener?.onEmailSent(result.first, result.second)
+ }
+
+ "OpenPGP" -> {
+ // 发送 PGP 邮件
+ val pgpEmail = PgpUtils(
+ properties,
+ authenticator,
+ from,
+ nickname,
+ subject,
+ html,
+ attachFiles,
+ toAddress,
+ ccAddress,
+ bccAddress,
+ recipientPGPPublicKeyRing,
+ senderPGPSecretKeyRing,
+ senderPGPSecretKeyPassword,
+ )
+ val isEncrypt: Boolean = recipientPGPPublicKeyRing != null
+ val isSign: Boolean = senderPGPSecretKeyRing != null
+ Log.d(TAG, "isEncrypt=$isEncrypt, isSign=$isSign")
+ val result = when {
+ isEncrypt && isSign -> pgpEmail.sendSignedAndEncryptedEmail()
+ isEncrypt -> pgpEmail.sendEncryptedEmail()
+ isSign -> pgpEmail.sendSignedEmail()
+ else -> pgpEmail.sendPlainEmail()
+ }
+ listener?.onEmailSent(result.first, result.second)
+ }
+
+ else -> {
+ // 发送普通邮件
+ val simpleEmail = SmimeUtils(
+ properties,
+ authenticator,
+ from,
+ nickname,
+ subject,
+ html,
+ attachFiles,
+ toAddress,
+ ccAddress,
+ bccAddress,
+ )
+ val result = simpleEmail.sendPlainEmail()
+ listener?.onEmailSent(result.first, result.second)
+ }
+ }
+ } catch (e: Exception) {
+ listener?.onEmailSent(false, "Error sending email: ${e.message}")
+ }
+ }
+
+ interface EmailTaskListener {
+ fun onEmailSent(success: Boolean, message: String)
+ }
+
+ /**
+ * 发件箱auth校验
+ */
+ private class MailAuthenticator(username: String, private var password: String) : Authenticator() {
+ private var userName: String? = username
+ override fun getPasswordAuthentication(): PasswordAuthentication {
+ return PasswordAuthentication(userName, password)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/mail/PgpUtils.kt b/app/src/main/java/com/example/firstapp/utils/mail/PgpUtils.kt
new file mode 100644
index 0000000..7f336e7
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/mail/PgpUtils.kt
@@ -0,0 +1,247 @@
+package com.example.firstapp.utils.mail
+
+import com.example.firstapp.utils.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.openpgp.PGPPublicKeyRing
+import org.bouncycastle.openpgp.PGPSecretKeyRing
+import org.bouncycastle.util.io.Streams
+import org.pgpainless.PGPainless
+import org.pgpainless.algorithm.DocumentSignatureType
+import org.pgpainless.algorithm.HashAlgorithm
+import org.pgpainless.encryption_signing.EncryptionOptions
+import org.pgpainless.encryption_signing.ProducerOptions
+import org.pgpainless.encryption_signing.SigningOptions
+import org.pgpainless.key.protection.SecretKeyRingProtector
+import org.pgpainless.util.Passphrase
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.InputStream
+import java.security.Security
+import java.util.Date
+import java.util.Properties
+import javax.activation.DataHandler
+import javax.activation.FileDataSource
+import javax.mail.Authenticator
+import javax.mail.Message
+import javax.mail.Session
+import javax.mail.Transport
+import javax.mail.internet.InternetAddress
+import javax.mail.internet.MimeBodyPart
+import javax.mail.internet.MimeMessage
+import javax.mail.internet.MimeMultipart
+import javax.mail.internet.MimeUtility
+import javax.mail.util.ByteArrayDataSource
+
+
+@Suppress("PrivatePropertyName")
+class PgpUtils(
+ private val properties: Properties,
+ private val authenticator: Authenticator,
+ // 邮件参数
+ private val from: String, // 发件人邮箱
+ private val nickname: String, // 发件人昵称
+ private val subject: String, // 邮件主题
+ private val body: String, // 邮件正文
+ private val attachFiles: MutableList<File> = mutableListOf(), // 附件
+ // 收件人参数
+ private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
+ private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
+ private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
+ //邮件 PGP 加密和签名
+ private var recipientPGPPublicKeyRing: PGPPublicKeyRing? = null, // 收件人公钥(用于加密)
+ private var senderPGPSecretKeyRing: PGPSecretKeyRing? = null, // 发件人私钥(用于签名)
+ private val senderPGPSecretKeyPassword: String = "", // 发件人私钥密码
+) {
+
+ private val TAG: String = PgpUtils::class.java.simpleName
+
+ init {
+ Security.addProvider(BouncyCastleProvider())
+ }
+
+ // 发送明文邮件
+ suspend fun sendPlainEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendPlainEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ Transport.send(originalMessage)
+ Pair(true, "Email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send email: ${e.message}")
+ }
+ }
+
+ // 发送签名后的邮件
+ suspend fun sendSignedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendSignedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val secretKeyDecryptor = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(senderPGPSecretKeyPassword))
+ val producerOptions = ProducerOptions.sign(
+ SigningOptions()
+ .addInlineSignature(secretKeyDecryptor, senderPGPSecretKeyRing!!, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
+ .overrideHashAlgorithm(HashAlgorithm.SHA256)
+ ).setAsciiArmor(true)
+ val signedMessage = getEncryptedAndOrSignedMessage(originalMessage, producerOptions)
+ Transport.send(signedMessage)
+ Pair(true, "Email signed and sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to sign and send email: ${e.message}")
+ }
+ }
+
+ // 发送加密邮件
+ suspend fun sendEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendEncryptedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val producerOptions = ProducerOptions.encrypt(
+ EncryptionOptions.encryptCommunications().addRecipient(recipientPGPPublicKeyRing!!)
+ ).setAsciiArmor(true)
+ val encryptedMessage = getEncryptedAndOrSignedMessage(originalMessage, producerOptions)
+ Transport.send(encryptedMessage)
+ Pair(true, "Encrypted email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send encrypted email: ${e.message}")
+ }
+ }
+
+ // 发送签名加密邮件
+ suspend fun sendSignedAndEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendSignedAndEncryptedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val secretKeyDecryptor = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(senderPGPSecretKeyPassword))
+ val producerOptions = ProducerOptions.signAndEncrypt(
+ EncryptionOptions.encryptCommunications().addRecipient(recipientPGPPublicKeyRing!!),
+ SigningOptions()
+ .addInlineSignature(secretKeyDecryptor, senderPGPSecretKeyRing!!, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
+ .overrideHashAlgorithm(HashAlgorithm.SHA256)
+ ).setAsciiArmor(true)
+ val encryptedMessage = getEncryptedAndOrSignedMessage(originalMessage, producerOptions)
+ Transport.send(encryptedMessage)
+ Pair(true, "Signed and encrypted email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send signed and encrypted email: ${e.message}")
+ }
+ }
+
+ // 获取原始邮件
+ private fun getOriginalMessage(): MimeMessage {
+ val session = Session.getInstance(properties, authenticator)
+ session.debug = true
+ val message = MimeMessage(session)
+ // 设置直接接收者收件箱
+ val toAddress = toAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.TO, toAddress)
+ // 设置抄送者收件箱
+ val ccAddress = ccAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.CC, ccAddress)
+ // 设置密送者收件箱
+ val bccAddress = bccAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.BCC, bccAddress)
+ // 设置发件箱
+ when {
+ nickname.isEmpty() -> message.setFrom(InternetAddress(from))
+ else -> try {
+ var name = nickname.replace(":", "-").replace("\n", "-")
+ name = MimeUtility.encodeText(name)
+ message.setFrom(InternetAddress("$name <$from>"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ message.setFrom(InternetAddress(from))
+ }
+ }
+ // 邮件主题
+ try {
+ message.subject = MimeUtility.encodeText(subject.replace(":", "-").replace("\n", "-"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ message.subject = subject
+ }
+
+ // 邮件内容
+ val contentPart = MimeMultipart("mixed")
+
+ // 邮件正文
+ val textBodyPart = MimeBodyPart()
+ textBodyPart.setContent(body, "text/html;charset=UTF-8")
+ contentPart.addBodyPart(textBodyPart)
+
+ // 邮件附件
+ attachFiles.forEach {
+ val fileBodyPart = MimeBodyPart()
+ val ds = FileDataSource(it)
+ val dh = DataHandler(ds)
+ fileBodyPart.dataHandler = dh
+ fileBodyPart.fileName = MimeUtility.encodeText(dh.name)
+ contentPart.addBodyPart(fileBodyPart)
+ }
+
+ message.setContent(contentPart)
+ message.sentDate = Date()
+ message.saveChanges()
+ return message
+ }
+
+ // 获取加密或且签名邮件: https://datatracker.ietf.org/doc/html/rfc3156#section-4
+ private fun getEncryptedAndOrSignedMessage(originalMessage: MimeMessage, producerOptions: ProducerOptions): MimeMessage {
+ // 将原始消息写入InputStream
+ val baos = ByteArrayOutputStream()
+ originalMessage.writeTo(baos)
+ val inputStream: InputStream = ByteArrayInputStream(baos.toByteArray())
+
+ // 加密数据
+ val outputStream = ByteArrayOutputStream()
+ val encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(producerOptions)
+ Streams.pipeAll(inputStream, encryptionStream)
+ encryptionStream.close()
+ val result = encryptionStream.result
+ Log.d(TAG, result.toString())
+
+ // The first body part contains the control information necessary to
+ // decrypt the data in the second body part and is labeled according to
+ // the value of the protocol parameter.
+ val versionPart = MimeBodyPart().apply {
+ setText("Version: 1")
+ addHeader("Content-Type", "application/pgp-encrypted")
+ addHeader("Content-Description", "PGP/MIME version identification")
+ //addHeader("Content-Transfer-Encoding", "base64")
+ }
+
+ // The second body part contains the data which was encrypted
+ // and is always labeled application/octet-stream.
+ val encryptedPart = MimeBodyPart().apply {
+ dataHandler = DataHandler(ByteArrayDataSource(outputStream.toByteArray(), "application/octet-stream"))
+ fileName = "encrypted.asc"
+ addHeader("Content-Type", "application/octet-stream; name=\"encrypted.asc\"")
+ addHeader("Content-Description", "OpenPGP encrypted message")
+ addHeader("Content-Disposition", "inline; filename=\"encrypted.asc\"")
+ }
+
+ val encryptedMultiPart = MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\"")
+ encryptedMultiPart.addBodyPart(versionPart, 0)
+ encryptedMultiPart.addBodyPart(encryptedPart, 1)
+
+ val encryptedMessage = MimeMessage(originalMessage.session)
+ encryptedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
+ encryptedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
+ encryptedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
+ encryptedMessage.addFrom(originalMessage.from)
+ encryptedMessage.subject = originalMessage.subject
+ encryptedMessage.sentDate = originalMessage.sentDate
+ encryptedMessage.setContent(encryptedMultiPart)
+ encryptedMessage.saveChanges()
+
+ return encryptedMessage
+ }
+
+}
+
diff --git a/app/src/main/java/com/example/firstapp/utils/mail/SmimeUtils.kt b/app/src/main/java/com/example/firstapp/utils/mail/SmimeUtils.kt
new file mode 100644
index 0000000..de148eb
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/mail/SmimeUtils.kt
@@ -0,0 +1,252 @@
+package com.example.firstapp.utils.mail
+
+import com.example.firstapp.utils.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.bouncycastle.cert.jcajce.JcaCertStore
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder
+import org.bouncycastle.cms.CMSAlgorithm
+import org.bouncycastle.cms.CMSEnvelopedDataGenerator
+import org.bouncycastle.cms.CMSProcessableByteArray
+import org.bouncycastle.cms.CMSSignedDataGenerator
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
+import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder
+import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.operator.OutputEncryptor
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.security.PrivateKey
+import java.security.Security
+import java.security.cert.X509Certificate
+import java.util.Date
+import java.util.Properties
+import javax.activation.DataHandler
+import javax.activation.FileDataSource
+import javax.mail.Authenticator
+import javax.mail.Message
+import javax.mail.Session
+import javax.mail.Transport
+import javax.mail.internet.InternetAddress
+import javax.mail.internet.MimeBodyPart
+import javax.mail.internet.MimeMessage
+import javax.mail.internet.MimeMultipart
+import javax.mail.internet.MimeUtility
+
+@Suppress("PrivatePropertyName")
+class SmimeUtils(
+ private val properties: Properties,
+ private val authenticator: Authenticator,
+ // 邮件参数
+ private val from: String, // 发件人邮箱
+ private val nickname: String, // 发件人昵称
+ private val subject: String, // 邮件主题
+ private val body: String, // 邮件正文
+ private val attachFiles: MutableList<File> = mutableListOf(), // 附件
+ // 收件人参数
+ private val toAddress: MutableList<String> = mutableListOf(), // 收件人邮箱
+ private val ccAddress: MutableList<String> = mutableListOf(), // 抄送者邮箱
+ private val bccAddress: MutableList<String> = mutableListOf(), // 密送者邮箱
+ // 邮件 S/MIME 加密和签名
+ private val recipientX509Cert: X509Certificate? = null, //收件人公钥(用于加密)
+ private val senderPrivateKey: PrivateKey? = null, //发件人私玥(用于签名)
+ private val senderX509Cert: X509Certificate? = null, //发件人公玥(用于签名)
+) {
+
+ private val TAG: String = SmimeUtils::class.java.simpleName
+
+ init {
+ Security.addProvider(BouncyCastleProvider())
+ }
+
+ // 发送明文邮件
+ suspend fun sendPlainEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendPlainEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ Transport.send(originalMessage)
+ Pair(true, "Email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send email: ${e.message}")
+ }
+ }
+
+ // 发送签名后的邮件
+ suspend fun sendSignedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendSignedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val signedMessage = getSignedMessage(originalMessage)
+ Transport.send(signedMessage)
+ Pair(true, "Email signed and sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to sign and send email: ${e.message}")
+ }
+ }
+
+ // 发送加密邮件
+ suspend fun sendEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendEncryptedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val encryptedMessage = getEncryptedMessage(originalMessage)
+ Transport.send(encryptedMessage)
+ Pair(true, "Encrypted email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send encrypted email: ${e.message}")
+ }
+ }
+
+ // 发送签名加密邮件
+ suspend fun sendSignedAndEncryptedEmail(): Pair<Boolean, String> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "sendSignedAndEncryptedEmail")
+ try {
+ val originalMessage = getOriginalMessage()
+ val signedMessage = getSignedMessage(originalMessage)
+ val encryptedMessage = getEncryptedMessage(signedMessage)
+ Transport.send(encryptedMessage)
+ Pair(true, "Signed and encrypted email sent successfully")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Pair(false, "Failed to send signed and encrypted email: ${e.message}")
+ }
+ }
+
+ // 获取原始邮件
+ private fun getOriginalMessage(): MimeMessage {
+ val session = Session.getInstance(properties, authenticator)
+ session.debug = true
+ val message = MimeMessage(session)
+ // 设置直接接收者收件箱
+ val toAddress = toAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.TO, toAddress)
+ // 设置抄送者收件箱
+ val ccAddress = ccAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.CC, ccAddress)
+ // 设置密送者收件箱
+ val bccAddress = bccAddress.map { InternetAddress(it) }.toTypedArray()
+ message.setRecipients(Message.RecipientType.BCC, bccAddress)
+ // 设置发件箱
+ when {
+ nickname.isEmpty() -> message.setFrom(InternetAddress(from))
+ else -> try {
+ var name = nickname.replace(":", "-").replace("\n", "-")
+ name = MimeUtility.encodeText(name)
+ message.setFrom(InternetAddress("$name <$from>"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ message.setFrom(InternetAddress(from))
+ }
+ }
+ // 邮件主题
+ try {
+ message.subject = MimeUtility.encodeText(subject.replace(":", "-").replace("\n", "-"))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ message.subject = subject
+ }
+
+ // 邮件内容
+ val contentPart = MimeMultipart("mixed")
+
+ // 邮件正文
+ val textBodyPart = MimeBodyPart()
+ textBodyPart.setContent(body, "text/html;charset=UTF-8")
+ contentPart.addBodyPart(textBodyPart)
+
+ // 邮件附件
+ attachFiles.forEach {
+ val fileBodyPart = MimeBodyPart()
+ val ds = FileDataSource(it)
+ val dh = DataHandler(ds)
+ fileBodyPart.dataHandler = dh
+ fileBodyPart.fileName = MimeUtility.encodeText(dh.name)
+ contentPart.addBodyPart(fileBodyPart)
+ }
+
+ message.setContent(contentPart)
+ message.sentDate = Date()
+ message.saveChanges()
+ return message
+ }
+
+ // 获取签名邮件
+ private fun getSignedMessage(originalMessage: MimeMessage): MimeMessage {
+ // 创建签名者信息生成器
+ val contentSigner = JcaContentSignerBuilder("SHA256withRSA").build(senderPrivateKey)
+ val certificateHolder = JcaX509CertificateHolder(senderX509Cert)
+ val signerInfoGenerator = JcaSignerInfoGeneratorBuilder(
+ JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider()).build()
+ ).build(contentSigner, certificateHolder)
+
+ // 创建 CMSSignedDataGenerator 并添加签名者信息和证书
+ val generator = CMSSignedDataGenerator()
+ generator.addSignerInfoGenerator(signerInfoGenerator)
+ val certStore = JcaCertStore(listOf(senderX509Cert))
+ generator.addCertificates(certStore)
+
+ // 将邮件内容转换为 CMSSignedData
+ val outputStream = ByteArrayOutputStream()
+ originalMessage.writeTo(outputStream)
+ val contentData = CMSProcessableByteArray(outputStream.toByteArray())
+ val signedData = generator.generate(contentData, true)
+
+ // 创建 MimeMessage 并设置签名后的内容
+ val signedMessage = MimeMessage(originalMessage.session, ByteArrayInputStream(signedData.encoded))
+ /*
+ //TODO: 为什么不需要再设置这些?
+ signedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
+ signedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
+ signedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
+ signedMessage.addFrom(originalMessage.from)
+ signedMessage.subject = originalMessage.subject
+ signedMessage.sentDate = originalMessage.sentDate
+ */
+ signedMessage.setContent(signedData.encoded, "application/pkcs7-mime; name=smime.p7m; smime-type=signed-data")
+ signedMessage.saveChanges()
+
+ return signedMessage
+ }
+
+ // 获取加密邮件
+ private fun getEncryptedMessage(originalMessage: MimeMessage): MimeMessage {
+ // 使用收件人的证书进行加密
+ val cmsEnvelopedDataGenerator = CMSEnvelopedDataGenerator()
+ val recipientInfoGenerator = JceKeyTransRecipientInfoGenerator(recipientX509Cert)
+ cmsEnvelopedDataGenerator.addRecipientInfoGenerator(recipientInfoGenerator)
+
+ // 使用 3DES 加密
+ val outputEncryptor: OutputEncryptor = JceCMSContentEncryptorBuilder(CMSAlgorithm.DES_EDE3_CBC).build()
+ val originalContent = ByteArrayOutputStream()
+ originalMessage.writeTo(originalContent)
+ val inputStream = originalContent.toByteArray()
+ val cmsEnvelopedData = cmsEnvelopedDataGenerator.generate(
+ CMSProcessableByteArray(inputStream),
+ outputEncryptor
+ )
+
+ // 创建加密邮件
+ val encryptedMessage = MimeMessage(originalMessage.session)
+ encryptedMessage.setRecipients(Message.RecipientType.TO, originalMessage.getRecipients(Message.RecipientType.TO))
+ encryptedMessage.setRecipients(Message.RecipientType.CC, originalMessage.getRecipients(Message.RecipientType.CC))
+ encryptedMessage.setRecipients(Message.RecipientType.BCC, originalMessage.getRecipients(Message.RecipientType.BCC))
+ encryptedMessage.addFrom(originalMessage.from)
+ encryptedMessage.subject = originalMessage.subject
+ encryptedMessage.sentDate = originalMessage.sentDate
+ encryptedMessage.setContent(cmsEnvelopedData.encoded, "application/pkcs7-mime; name=smime.p7m; smime-type=enveloped-data")
+ encryptedMessage.setHeader("Content-Type", "application/pkcs7-mime; name=smime.p7m; smime-type=enveloped-data")
+ encryptedMessage.setHeader("Content-Disposition", "attachment; filename=smime.p7m")
+ encryptedMessage.setHeader("Content-Description", "S/MIME Encrypted Message")
+ encryptedMessage.addHeader("Content-Transfer-Encoding", "base64")
+ encryptedMessage.saveChanges()
+
+ return encryptedMessage
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/tinker/ShareReflectUtil.kt b/app/src/main/java/com/example/firstapp/utils/tinker/ShareReflectUtil.kt
new file mode 100644
index 0000000..4d05073
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/tinker/ShareReflectUtil.kt
@@ -0,0 +1,240 @@
+package com.example.firstapp.utils.tinker
+
+import android.annotation.SuppressLint
+import android.content.Context
+import java.lang.reflect.Constructor
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+
+@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "UNCHECKED_CAST", "unused")
+object ShareReflectUtil {
+ /**
+ * Locates a given field anywhere in the class inheritance hierarchy.
+ *
+ * @param instance an object to search the field into.
+ * @param name field name
+ * @return a field object
+ * @throws NoSuchFieldException if the field cannot be located
+ */
+ @Throws(NoSuchFieldException::class)
+ fun findField(instance: Any, name: String): Field {
+ var clazz: Class<*>? = instance.javaClass
+ while (clazz != null) {
+ try {
+ val field = clazz.getDeclaredField(name)
+ if (!field.isAccessible) {
+ field.isAccessible = true
+ }
+ return field
+ } catch (e: NoSuchFieldException) {
+ // ignore and search next
+ }
+ clazz = clazz.superclass
+ }
+ throw NoSuchFieldException("Field " + name + " not found in " + instance.javaClass)
+ }
+
+ @Throws(NoSuchFieldException::class)
+ fun findField(originClazz: Class<*>, name: String): Field {
+ var clazz: Class<*>? = originClazz
+ while (clazz != null) {
+ try {
+ val field = clazz.getDeclaredField(name)
+ if (!field.isAccessible) {
+ field.isAccessible = true
+ }
+ return field
+ } catch (e: NoSuchFieldException) {
+ // ignore and search next
+ }
+ clazz = clazz.superclass
+ }
+ throw NoSuchFieldException("Field $name not found in $originClazz")
+ }
+
+ /**
+ * Locates a given method anywhere in the class inheritance hierarchy.
+ *
+ * @param instance an object to search the method into.
+ * @param name method name
+ * @param parameterTypes method parameter types
+ * @return a method object
+ * @throws NoSuchMethodException if the method cannot be located
+ */
+ @Throws(NoSuchMethodException::class)
+ fun findMethod(instance: Any, name: String, vararg parameterTypes: Class<*>?): Method {
+ var clazz: Class<*>? = instance.javaClass
+ while (clazz != null) {
+ try {
+ val method = clazz.getDeclaredMethod(name, *parameterTypes)
+ if (!method.isAccessible) {
+ method.isAccessible = true
+ }
+ return method
+ } catch (e: NoSuchMethodException) {
+ // ignore and search next
+ }
+ clazz = clazz.superclass
+ }
+ throw NoSuchMethodException(
+ "Method "
+ + name
+ + " with parameters "
+ + listOf(*parameterTypes)
+ + " not found in " + instance.javaClass
+ )
+ }
+
+ /**
+ * Locates a given method anywhere in the class inheritance hierarchy.
+ *
+ * @param clazz a class to search the method into.
+ * @param name method name
+ * @param parameterTypes method parameter types
+ * @return a method object
+ * @throws NoSuchMethodException if the method cannot be located
+ */
+ @Throws(NoSuchMethodException::class)
+ fun findMethod(clazz: Class<*>?, name: String, vararg parameterTypes: Class<*>?): Method {
+ var tClazz = clazz
+ while (tClazz != null) {
+ try {
+ val method = tClazz.getDeclaredMethod(name, *parameterTypes)
+ if (!method.isAccessible) {
+ method.isAccessible = true
+ }
+ return method
+ } catch (e: NoSuchMethodException) {
+ // ignore and search next
+ }
+ tClazz = tClazz.superclass
+ }
+ throw NoSuchMethodException(
+ "Method "
+ + name
+ + " with parameters "
+ + listOf(*parameterTypes)
+ + " not found in tClazz"
+ )
+ }
+
+ /**
+ * Locates a given constructor anywhere in the class inheritance hierarchy.
+ *
+ * @param instance an object to search the constructor into.
+ * @param parameterTypes constructor parameter types
+ * @return a constructor object
+ * @throws NoSuchMethodException if the constructor cannot be located
+ */
+ @Throws(NoSuchMethodException::class)
+ fun findConstructor(instance: Any, vararg parameterTypes: Class<*>?): Constructor<*> {
+ var clazz: Class<*>? = instance.javaClass
+ while (clazz != null) {
+ try {
+ val constructor = clazz.getDeclaredConstructor(*parameterTypes)
+ if (!constructor.isAccessible) {
+ constructor.isAccessible = true
+ }
+ return constructor
+ } catch (e: NoSuchMethodException) {
+ // ignore and search next
+ }
+ clazz = clazz.superclass
+ }
+ throw NoSuchMethodException(
+ "Constructor"
+ + " with parameters "
+ + listOf(*parameterTypes)
+ + " not found in " + instance.javaClass
+ )
+ }
+
+ /**
+ * Replace the value of a field containing a non null array, by a new array containing the
+ * elements of the original array plus the elements of extraElements.
+ *
+ * @param instance the instance whose field is to be modified.
+ * @param fieldName the field to modify.
+ * @param extraElements elements to append at the end of the array.
+ */
+ @Throws(NoSuchFieldException::class, IllegalArgumentException::class, IllegalAccessException::class)
+ fun expandFieldArray(instance: Any, fieldName: String, extraElements: Array<Any?>) {
+ val jlrField = findField(instance, fieldName)
+ val original = jlrField[instance] as Array<Any>
+ val combined = java.lang.reflect.Array.newInstance(original.javaClass.componentType, original.size + extraElements.size) as Array<Any>
+
+ // NOTE: changed to copy extraElements first, for patch load first
+ System.arraycopy(extraElements, 0, combined, 0, extraElements.size)
+ System.arraycopy(original, 0, combined, extraElements.size, original.size)
+ jlrField[instance] = combined
+ }
+
+ /**
+ * Replace the value of a field containing a non null array, by a new array containing the
+ * elements of the original array plus the elements of extraElements.
+ *
+ * @param instance the instance whose field is to be modified.
+ * @param fieldName the field to modify.
+ */
+ @Throws(NoSuchFieldException::class, IllegalArgumentException::class, IllegalAccessException::class)
+ fun reduceFieldArray(instance: Any, fieldName: String, reduceSize: Int) {
+ if (reduceSize <= 0) {
+ return
+ }
+ val jlrField = findField(instance, fieldName)
+ val original = jlrField[instance] as Array<Any>
+ val finalLength = original.size - reduceSize
+ if (finalLength <= 0) {
+ return
+ }
+ val combined = java.lang.reflect.Array.newInstance(original.javaClass.componentType, finalLength) as Array<Any>
+ System.arraycopy(original, reduceSize, combined, 0, finalLength)
+ jlrField[instance] = combined
+ }
+
+ @SuppressLint("PrivateApi")
+ fun getActivityThread(
+ context: Context?,
+ activityThread: Class<*>?,
+ ): Any? {
+ var tActivityThread = activityThread
+ return try {
+ if (tActivityThread == null) {
+ tActivityThread = Class.forName("android.app.ActivityThread")
+ }
+ val m = tActivityThread!!.getMethod("currentActivityThread")
+ m.isAccessible = true
+ var currentActivityThread = m.invoke(null)
+ if (currentActivityThread == null && context != null) {
+ // In older versions of Android (prior to frameworks/base 66a017b63461a22842)
+ // the currentActivityThread was built on thread locals, so we'll need to try
+ // even harder
+ val mLoadedApk = context.javaClass.getField("mLoadedApk")
+ mLoadedApk.isAccessible = true
+ val apk = mLoadedApk[context]
+ val mActivityThreadField = apk.javaClass.getDeclaredField("mActivityThread")
+ mActivityThreadField.isAccessible = true
+ currentActivityThread = mActivityThreadField[apk]
+ }
+ currentActivityThread
+ } catch (ignore: Throwable) {
+ null
+ }
+ }
+
+ /**
+ * Handy method for fetching hidden integer constant value in system classes.
+ *
+ * @param clazz
+ * @param fieldName
+ * @return
+ */
+ fun getValueOfStaticIntField(clazz: Class<*>, fieldName: String, defVal: Int): Int {
+ return try {
+ val field = findField(clazz, fieldName)
+ field.getInt(null)
+ } catch (thr: Throwable) {
+ defVal
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/firstapp/utils/tinker/TinkerLoadLibrary.kt b/app/src/main/java/com/example/firstapp/utils/tinker/TinkerLoadLibrary.kt
new file mode 100644
index 0000000..0695dc9
--- /dev/null
+++ b/app/src/main/java/com/example/firstapp/utils/tinker/TinkerLoadLibrary.kt
@@ -0,0 +1,196 @@
+/*
+ * Tencent is pleased to support the open source community by making Tinker available.
+ *
+ * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.firstapp.utils.tinker
+
+import android.annotation.SuppressLint
+import android.os.Build
+import com.example.firstapp.utils.Log
+import java.io.File
+import java.io.IOException
+
+/**
+ * Created by zhangshaowen on 17/1/5.
+ * Thanks for Android Fragmentation
+ */
+@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "UNCHECKED_CAST", "SENSELESS_COMPARISON")
+object TinkerLoadLibrary {
+ private const val TAG = "Tinker.LoadLibrary"
+
+ @SuppressLint("ObsoleteSdkInt")
+ @Throws(Throwable::class)
+ fun installNativeLibraryPath(classLoader: ClassLoader, folder: File?) {
+ if (folder == null || !folder.exists()) {
+ Log.e(TAG, String.format("installNativeLibraryPath, folder %s is illegal", folder))
+ return
+ }
+ // android o sdk_int 26
+ // for android o preview sdk_int 25
+ if (Build.VERSION.SDK_INT == 25 && Build.VERSION.PREVIEW_SDK_INT != 0
+ || Build.VERSION.SDK_INT > 25
+ ) {
+ try {
+ V25.install(classLoader, folder)
+ } catch (throwable: Throwable) {
+ // install fail, try to treat it as v23
+ // some preview N version may go here
+ Log.e(
+ TAG, String.format(
+ "installNativeLibraryPath, v25 fail, sdk: %d, error: %s, try to fallback to V23",
+ Build.VERSION.SDK_INT, throwable.message
+ )
+ )
+ V23.install(classLoader, folder)
+ }
+ } else if (Build.VERSION.SDK_INT >= 23) {
+ try {
+ V23.install(classLoader, folder)
+ } catch (throwable: Throwable) {
+ // install fail, try to treat it as v14
+ Log.e(
+ TAG, String.format(
+ "installNativeLibraryPath, v23 fail, sdk: %d, error: %s, try to fallback to V14",
+ Build.VERSION.SDK_INT, throwable.message
+ )
+ )
+ V14.install(classLoader, folder)
+ }
+ } else if (Build.VERSION.SDK_INT >= 14) {
+ V14.install(classLoader, folder)
+ } else {
+ V4.install(classLoader, folder)
+ }
+ }
+
+ object V4 {
+ @Throws(Throwable::class)
+ fun install(classLoader: ClassLoader, folder: File) {
+ val addPath = folder.path
+ val pathField = ShareReflectUtil.findField(classLoader, "libPath")
+ val origLibPaths = pathField[classLoader] as String
+ val origLibPathSplit = origLibPaths.split(":".toRegex()).toTypedArray()
+ val newLibPaths = StringBuilder(addPath)
+ for (origLibPath in origLibPathSplit) {
+ if (origLibPath == null || addPath == origLibPath) {
+ continue
+ }
+ newLibPaths.append(':').append(origLibPath)
+ }
+ pathField[classLoader] = newLibPaths.toString()
+ val libraryPathElementsFiled = ShareReflectUtil.findField(classLoader, "libraryPathElements")
+ val libraryPathElements = libraryPathElementsFiled[classLoader] as MutableList<String>
+ val libPathElementIt = libraryPathElements.iterator()
+ while (libPathElementIt.hasNext()) {
+ val libPath = libPathElementIt.next()
+ if (addPath == libPath) {
+ libPathElementIt.remove()
+ break
+ }
+ }
+ libraryPathElements.add(0, addPath)
+ libraryPathElementsFiled[classLoader] = libraryPathElements
+ }
+ }
+
+ object V14 {
+ @Throws(Throwable::class)
+ fun install(classLoader: ClassLoader, folder: File) {
+ val pathListField = ShareReflectUtil.findField(classLoader, "pathList")
+ val dexPathList = pathListField[classLoader]
+ val nativeLibDirField = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories")
+ val origNativeLibDirs = nativeLibDirField[dexPathList] as Array<File>
+ val newNativeLibDirList: MutableList<File> = ArrayList(origNativeLibDirs.size + 1)
+ newNativeLibDirList.add(folder)
+ for (origNativeLibDir in origNativeLibDirs) {
+ if (folder != origNativeLibDir) {
+ newNativeLibDirList.add(origNativeLibDir)
+ }
+ }
+ nativeLibDirField[dexPathList] = newNativeLibDirList.toTypedArray()
+ }
+ }
+
+ object V23 {
+ @Throws(Throwable::class)
+ fun install(classLoader: ClassLoader, folder: File) {
+ val pathListField = ShareReflectUtil.findField(classLoader, "pathList")
+ val dexPathList = pathListField[classLoader]
+ val nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories")
+ var origLibDirs = nativeLibraryDirectories[dexPathList] as MutableList<File>
+ if (origLibDirs == null) {
+ origLibDirs = ArrayList(2)
+ }
+ val libDirIt = origLibDirs.iterator()
+ while (libDirIt.hasNext()) {
+ val libDir = libDirIt.next()
+ if (folder == libDir) {
+ libDirIt.remove()
+ break
+ }
+ }
+ origLibDirs.add(0, folder)
+ val systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories")
+ var origSystemLibDirs = systemNativeLibraryDirectories[dexPathList] as List<File>
+ if (origSystemLibDirs == null) {
+ origSystemLibDirs = ArrayList(2)
+ }
+ val newLibDirs: MutableList<File> = ArrayList(origLibDirs.size + origSystemLibDirs.size + 1)
+ newLibDirs.addAll(origLibDirs)
+ newLibDirs.addAll(origSystemLibDirs)
+ val makeElements = ShareReflectUtil.findMethod(
+ dexPathList,
+ "makePathElements", MutableList::class.java, File::class.java, MutableList::class.java
+ )
+ val suppressedExceptions = ArrayList<IOException>()
+ val elements = makeElements.invoke(dexPathList, newLibDirs, null, suppressedExceptions) as Array<Any>
+ val nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements")
+ nativeLibraryPathElements[dexPathList] = elements
+ }
+ }
+
+ object V25 {
+ @Throws(Throwable::class)
+ fun install(classLoader: ClassLoader, folder: File) {
+ val pathListField = ShareReflectUtil.findField(classLoader, "pathList")
+ val dexPathList = pathListField[classLoader]
+ val nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories")
+ var origLibDirs = nativeLibraryDirectories[dexPathList] as MutableList<File>
+ if (origLibDirs == null) {
+ origLibDirs = ArrayList(2)
+ }
+ val libDirIt = origLibDirs.iterator()
+ while (libDirIt.hasNext()) {
+ val libDir = libDirIt.next()
+ if (folder == libDir) {
+ libDirIt.remove()
+ break
+ }
+ }
+ origLibDirs.add(0, folder)
+ val systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories")
+ var origSystemLibDirs = systemNativeLibraryDirectories[dexPathList] as List<File>
+ if (origSystemLibDirs == null) {
+ origSystemLibDirs = ArrayList(2)
+ }
+ val newLibDirs: MutableList<File> = ArrayList(origLibDirs.size + origSystemLibDirs.size + 1)
+ newLibDirs.addAll(origLibDirs)
+ newLibDirs.addAll(origSystemLibDirs)
+ val makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", MutableList::class.java)
+ val elements = makeElements.invoke(dexPathList, newLibDirs) as Array<Any>
+ val nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements")
+ nativeLibraryPathElements[dexPathList] = elements
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_app.xml b/app/src/main/res/drawable/ic_app.xml
new file mode 100644
index 0000000..6e94dfa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_app.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dip"
+ android:height="50.0dip"
+ android:viewportWidth="40.0"
+ android:viewportHeight="50.0">
+ <path
+ android:fillAlpha="0.8"
+ android:fillColor="#BEC2C7"
+ android:fillType="evenOdd"
+ android:pathData="M24.6702,0C26.9147,0 29.0561,0.9429 30.5715,2.5987L37.9014,10.6071C39.2513,12.0821 40,14.009 40,16.0084V44C40,47.3137 37.3137,50 34,50H6C2.6863,50 0,47.3137 0,44V6C0,2.6863 2.6863,0 6,0H24.6702Z"
+ android:strokeAlpha="0.8" />
+ <group
+ android:scaleX="2"
+ android:scaleY="2"
+ android:translateX="-4.2"
+ android:translateY="2">
+ <path
+ android:fillColor="#ffffffff"
+ android:fillType="evenOdd"
+ android:pathData="M7.984,5.837C8.418,5.586 8.974,5.735 9.224,6.169L9.779,7.13C10.518,6.712 11.372,6.473 12.282,6.473C13.199,6.473 14.06,6.715 14.803,7.14L15.342,6.207C15.592,5.772 16.148,5.623 16.582,5.874C17.017,6.125 17.165,6.68 16.915,7.115L16.212,8.331C16.862,9.121 17.278,10.11 17.355,11.194H7.21C7.287,10.103 7.708,9.107 8.366,8.314L7.651,7.077C7.401,6.643 7.55,6.087 7.984,5.837ZM17.368,12.647H7.198V17.732C7.198,18.535 7.848,19.185 8.651,19.185H15.915C16.718,19.185 17.368,18.535 17.368,17.732V12.647Z" />
+ </group>
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml
new file mode 100644
index 0000000..46fc8de
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml
new file mode 100644
index 0000000..f8bb0b5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml
new file mode 100644
index 0000000..78b75c3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_sim.xml b/app/src/main/res/drawable/ic_sim.xml
new file mode 100644
index 0000000..04e7cd5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sim.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dip"
+ android:height="50.0dip"
+ android:viewportWidth="40.0"
+ android:viewportHeight="50.0">
+ <path
+ android:fillAlpha="0.8"
+ android:fillColor="#BEC2C7"
+ android:fillType="evenOdd"
+ android:pathData="M24.6702,0C26.9147,0 29.0561,0.9429 30.5715,2.5987L37.9014,10.6071C39.2513,12.0821 40,14.009 40,16.0084V44C40,47.3137 37.3137,50 34,50H6C2.6863,50 0,47.3137 0,44V6C0,2.6863 2.6863,0 6,0H24.6702Z
+ M18,43L14,43v-4h4v4z
+ M34,43h-4v-4h4v4z
+ M18,35L14,35v-8h4v8z
+ M26,43h-4v-8h4v8z
+ M26,31h-4v-4h4v4z
+ M34,35h-4v-8h4v8z"
+ android:strokeAlpha="0.8" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_sim1.xml b/app/src/main/res/drawable/ic_sim1.xml
new file mode 100644
index 0000000..a7b33bf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sim1.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dip"
+ android:height="50.0dip"
+ android:viewportWidth="40.0"
+ android:viewportHeight="50.0">
+ <path
+ android:fillAlpha="0.8"
+ android:fillColor="#BEC2C7"
+ android:fillType="evenOdd"
+ android:pathData="M24.6702,0C26.9147,0 29.0561,0.9429 30.5715,2.5987L37.9014,10.6071C39.2513,12.0821 40,14.009 40,16.0084V44C40,47.3137 37.3137,50 34,50H6C2.6863,50 0,47.3137 0,44V6C0,2.6863 2.6863,0 6,0H24.6702ZM22.785,13.8333H18.8717L13.33,15.4483V20.3117L17.7633,19.045V38H22.785V13.8333Z"
+ android:strokeAlpha="0.8" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_sim2.xml b/app/src/main/res/drawable/ic_sim2.xml
new file mode 100644
index 0000000..a2c81d6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sim2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dip"
+ android:height="50.0dip"
+ android:viewportWidth="40.0"
+ android:viewportHeight="50.0">
+ <path
+ android:fillAlpha="0.8"
+ android:fillColor="#BEC2C7"
+ android:fillType="evenOdd"
+ android:pathData="M30.5715,2.5987C29.0561,0.9429 26.9147,0 24.6702,0H6C2.6863,0 0,2.6863 0,6V44C0,47.3137 2.6863,50 6,50H34C37.3137,50 40,47.3137 40,44V16.0084C40,14.009 39.2513,12.0821 37.9014,10.6071L30.5715,2.5987ZM20.1471,13C17.7983,13 15.7604,13.7514 14.3071,15.1044C12.8518,16.4593 12,18.4019 12,20.7467V20.9918H16.7129V20.7467C16.7129,19.6857 17.0933,18.8354 17.7036,18.2496C18.3155,17.6623 19.1728,17.3274 20.1471,17.3274C22.0709,17.3274 23.3606,18.4956 23.3606,20.0899C23.3606,21.317 22.9263,22.2977 21.5103,23.9022L21.5087,23.9041L12.3677,34.4825V38H27.7794V33.7763H18.6271L24.6667,26.89C26.6563,24.638 28,22.6766 28,20.0899C28,16.0219 24.5918,13 20.1471,13Z"
+ android:strokeAlpha="0.8" />
+</vector>
diff --git a/app/src/main/res/drawable/ic_sms.xml b/app/src/main/res/drawable/ic_sms.xml
new file mode 100644
index 0000000..2d12564
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sms.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#BEC2C7"
+ android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z" />
+</vector>
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..06ea6ca
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="?attr/actionBarSize">
+
+ <com.google.android.material.bottomnavigation.BottomNavigationView
+ android:id="@+id/nav_view"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="0dp"
+ android:layout_marginEnd="0dp"
+ android:background="?android:attr/windowBackground"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:menu="@menu/bottom_nav_menu" />
+
+ <fragment
+ android:id="@+id/nav_host_fragment_activity_main"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toTopOf="@id/nav_view"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/mobile_navigation" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml
new file mode 100644
index 0000000..166ab0e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_dashboard.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 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"
+ tools:context=".ui.dashboard.DashboardFragment">
+
+ <TextView
+ android:id="@+id/text_dashboard"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:textAlignment="center"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..0a6dc93
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 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"
+ tools:context=".ui.home.HomeFragment">
+
+<!-- <TextView-->
+<!-- android:id="@+id/text_home"-->
+<!-- android:layout_width="match_parent"-->
+<!-- android:layout_height="wrap_content"-->
+<!-- android:layout_marginStart="8dp"-->
+<!-- android:layout_marginTop="8dp"-->
+<!-- android:layout_marginEnd="8dp"-->
+<!-- android:textAlignment="center"-->
+<!-- android:textSize="20sp"-->
+<!-- app:layout_constraintBottom_toBottomOf="parent"-->
+<!-- app:layout_constraintEnd_toEndOf="parent"-->
+<!-- app:layout_constraintStart_toStartOf="parent"-->
+<!-- app:layout_constraintTop_toTopOf="parent" />-->
+
+ <androidx.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="8dp"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml
new file mode 100644
index 0000000..d417935
--- /dev/null
+++ b/app/src/main/res/layout/fragment_notifications.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout 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"
+ tools:context=".ui.notifications.NotificationsFragment">
+
+ <TextView
+ android:id="@+id/text_notifications"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:textAlignment="center"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_layout.xml b/app/src/main/res/layout/item_layout.xml
new file mode 100644
index 0000000..22792ce
--- /dev/null
+++ b/app/src/main/res/layout/item_layout.xml
@@ -0,0 +1,22 @@
+<?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:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/tvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:textStyle="bold"/>
+
+ <TextView
+ android:id="@+id/tvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:layout_marginTop="4dp"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/sms_code.xml b/app/src/main/res/layout/sms_code.xml
new file mode 100644
index 0000000..b070112
--- /dev/null
+++ b/app/src/main/res/layout/sms_code.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+<!-- 线性布局-->
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
new file mode 100644
index 0000000..fb6d040
--- /dev/null
+++ b/app/src/main/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/navigation_home"
+ android:icon="@drawable/ic_home_black_24dp"
+ android:title="@string/title_home" />
+
+ <item
+ android:id="@+id/navigation_dashboard"
+ android:icon="@drawable/ic_dashboard_black_24dp"
+ android:title="@string/title_dashboard" />
+
+ <item
+ android:id="@+id/navigation_notifications"
+ android:icon="@drawable/ic_notifications_black_24dp"
+ android:title="@string/title_notifications" />
+
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
new file mode 100644
index 0000000..9a3d6a7
--- /dev/null
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation 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/mobile_navigation"
+ app:startDestination="@+id/navigation_home">
+
+ <fragment
+ android:id="@+id/navigation_home"
+ android:name="com.example.firstapp.ui.home.HomeFragment"
+ android:label="@string/title_home"
+ tools:layout="@layout/fragment_home" />
+
+ <fragment
+ android:id="@+id/navigation_dashboard"
+ android:name="com.example.firstapp.ui.dashboard.DashboardFragment"
+ android:label="@string/title_dashboard"
+ tools:layout="@layout/fragment_dashboard" />
+
+ <fragment
+ android:id="@+id/navigation_notifications"
+ android:name="com.example.firstapp.ui.notifications.NotificationsFragment"
+ android:label="@string/title_notifications"
+ tools:layout="@layout/fragment_notifications" />
+</navigation>
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..64e57ea
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.FirstApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_200</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/black</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_200</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..e00c2dd
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6776d63
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<resources>
+ <string name="app_name">FirstApp</string>
+ <string name="title_home">Home</string>
+ <string name="title_dashboard">Dashboard</string>
+ <string name="title_notifications">Notifications</string>
+
+
+ <string name="notification_content">短信监听小程序</string>
+
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..1fe83df
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.FirstApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_500</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/white</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_700</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Sample backup rules file; uncomment and customize as necessary.
+ See https://developer.android.com/guide/topics/data/autobackup
+ for details.
+ Note: This file is ignored for devices older that API 31
+ See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+ <!--
+ <include domain="sharedpref" path="."/>
+ <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Sample data extraction rules file; uncomment and customize as necessary.
+ See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+ for details.
+-->
+<data-extraction-rules>
+ <cloud-backup>
+ <!-- TODO: Use <include> and <exclude> to control what is backed up.
+ <include .../>
+ <exclude .../>
+ -->
+ </cloud-backup>
+ <!--
+ <device-transfer>
+ <include .../>
+ <exclude .../>
+ </device-transfer>
+ -->
+</data-extraction-rules>
\ No newline at end of file
diff --git a/app/src/test/java/com/example/firstapp/ExampleUnitTest.kt b/app/src/test/java/com/example/firstapp/ExampleUnitTest.kt
new file mode 100644
index 0000000..ced19ac
--- /dev/null
+++ b/app/src/test/java/com/example/firstapp/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.firstapp
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/app/x-library.gradle b/app/x-library.gradle
new file mode 100644
index 0000000..de42c83
--- /dev/null
+++ b/app/x-library.gradle
@@ -0,0 +1,66 @@
+apply plugin: 'com.xuexiang.xrouter'
+apply plugin: 'kotlin-kapt'
+//apply plugin: 'android-aspectjx'
+apply plugin: 'com.xuexiang.xaop'
+
+//自动添加依赖
+configurations.each { configuration ->
+ def dependencies = getProject().dependencies
+ if (configuration.name == "implementation") {
+ //为Project加入X-Library依赖
+ //XUI框架
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xui))
+ configuration.dependencies.add(dependencies.create(deps.androidx.appcompat))
+ configuration.dependencies.add(dependencies.create(deps.androidx.recyclerview))
+ configuration.dependencies.add(dependencies.create(deps.androidx.design))
+ configuration.dependencies.add(dependencies.create(deps.glide))
+ //XUtil工具类
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xutil_core))
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xutil_sub))
+ //XAOP切片
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xaop_runtime))
+ //XUpdate版本更新
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xupdate))
+ //XHttp2
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xhttp2))
+ configuration.dependencies.add(dependencies.create(deps.rxjava2))
+ configuration.dependencies.add(dependencies.create(deps.rxandroid))
+ configuration.dependencies.add(dependencies.create(deps.okhttp3))
+ configuration.dependencies.add(dependencies.create(deps.gson))
+ //XPage
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xpage_lib))
+ //页面路由
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xrouter_runtime))
+ }
+
+ if (configuration.name == "kapt") {
+ //XPage
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xpage_compiler))
+ //页面路由
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xrouter_compiler))
+ }
+
+ if (isNeedLeakcanary.toBoolean() && configuration.name == "debugImplementation") {
+ //内存泄漏监测leak
+ configuration.dependencies.add(dependencies.create(deps.leakcanary))
+ }
+}
+
+configurations.configureEach {
+ resolutionStrategy.force deps.okhttp3
+ //总是拉取最新的 build 版本
+ resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
+}
+
+/*
+aspectjx {
+ // 这里需要修改包名
+ include 'com.example.firstapp'
+ // 排除所有package路径中包含`android.support`的class文件及库(jar文件)
+ exclude 'android.support'
+ // 移除kotlin相关,编译错误和提升速度
+ exclude 'kotlin.jvm', 'kotlin.internal'
+ exclude 'kotlinx.coroutines.internal', 'kotlinx.coroutines.android'
+ exclude '*.jar', '*.aar', '*.so'
+}
+*/
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..c47d555
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,45 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ apply from: './versions.gradle'
+ addRepos(repositories) //增加代码仓库
+ dependencies {
+ classpath deps.android_gradle_plugin
+ classpath deps.android_maven_gradle_plugin
+ //图片压缩
+ classpath 'com.chenenyu:img-optimizer:1.3.0'
+ //美团多渠道打包
+ //classpath 'com.meituan.android.walle:plugin:1.1.6'
+ //滴滴的质量优化框架
+ if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
+ classpath deps.booster.gradle_plugin
+ classpath deps.booster.task_processed_res
+ classpath deps.booster.task_resource_deredundancy
+ }
+ //AndServer
+ classpath 'cn.ppps.andserver:plugin:2.1.12'
+ }
+}
+
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.jetbrains.kotlin.android) apply false
+}
+
+
+
+//allprojects {
+// repositories {
+//// google()
+//// mavenCentral()
+//// jcenter()
+//// maven { url 'https://maven.aliyun.com/repository/google' }
+//// maven { url 'https://maven.aliyun.com/repository/central' }
+//// maven { url 'https://maven.aliyun.com/repository/public' }
+//// maven { url 'https://repo1.maven.org/maven2/' }
+//// maven { url 'https://oss.sonatype.org/content/repositories/public' }
+//// maven { url "https://jitpack.io" }
+// }
+//
+// // 将构建文件统一输出到项目根目录下的 build 文件夹
+// setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
+//}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..15a3aae
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,39 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+#org.gradle.jvmargs=-Xmx1024m -Dfile.encoding=UTF-8
+org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
+ --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+ --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+
+# ?? KAPT ?? JDK ????
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+
+isNeedPackage=true
+isNeedClean=true
+excludeFrpclib=true
+isNeedLeakcanary=false
+isUseBooster=false
+android.precompileDependenciesResources=false
+android.enableJetifier=true
+#android.enableD8=true
+org.gradle.java.home=D\:\\tanjiyaun\\jdk\\jdk1.8\\jdk-17.0.12
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..8108deb
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,32 @@
+[versions]
+agp = "8.5.1"
+kotlin = "1.9.0"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+constraintlayout = "2.1.4"
+lifecycleLivedataKtx = "2.6.1"
+lifecycleViewmodelKtx = "2.6.1"
+navigationFragmentKtx = "2.6.0"
+navigationUiKtx = "2.6.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
+androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
+androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
+androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a94a9b8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Thu Jan 16 11:18:21 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+#distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
+#distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..f09bb6b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,39 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url "https://jitpack.io" }
+ google()
+ jcenter()
+ maven { url 'https://maven.aliyun.com/repository/google' }
+ maven { url 'https://maven.aliyun.com/repository/central' }
+ maven { url 'https://maven.aliyun.com/repository/public' }
+ maven { url 'https://repo1.maven.org/maven2/' }
+ maven { url 'https://oss.sonatype.org/content/repositories/public' }
+
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ jcenter()
+ maven { url 'https://maven.aliyun.com/repository/google' }
+ maven { url 'https://maven.aliyun.com/repository/central' }
+ maven { url 'https://maven.aliyun.com/repository/public' }
+ maven { url 'https://repo1.maven.org/maven2/' }
+ maven { url 'https://oss.sonatype.org/content/repositories/public' }
+ }
+}
+
+rootProject.name = "FirstApp"
+include ':app'
diff --git a/versions.gradle b/versions.gradle
new file mode 100644
index 0000000..8e3c229
--- /dev/null
+++ b/versions.gradle
@@ -0,0 +1,223 @@
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+def build_versions = [:]
+build_versions.version_code = 53
+build_versions.version_name = "3.3.2"
+build_versions.min_sdk = 19
+build_versions.target_sdk = 33
+build_versions.build_tools = "33.0.1"
+ext.build_versions = build_versions
+
+ext.deps = [:]
+def versions = [:]
+versions.android_gradle_plugin = '7.2.2'
+versions.android_maven_gradle_plugin = "2.1"
+versions.gradle_bintray_plugin = "1.8.0"
+versions.booster = "3.1.0"
+versions.booster_all = "1.1.1"
+versions.support = "28.0.0"
+versions.annotation = "1.3.0"
+versions.androidx = "1.4.1"
+versions.recyclerview = "1.2.1"
+versions.material = "1.5.0"
+versions.junit = "4.13.2"
+versions.espresso = '3.5.1'
+versions.constraint_layout = "2.1.3"
+versions.glide = "4.13.1"
+versions.rxjava2 = "2.2.21"
+versions.rxandroid = "2.1.1"
+versions.rxbinding = "2.2.0"
+versions.butterknife = "10.2.3"
+versions.runner = "1.4.0"
+versions.gson = "2.10.1" //https://github.com/google/gson
+versions.okhttp3 = "3.12.13" //不可升级,为了支持 API 19
+versions.leakcanary = "2.13" //https://github.com/square/leakcanary
+versions.lifecycle = "2.2.0"
+//versions.kotlin = '1.7.21'
+versions.kotlin = '1.6.21'
+
+//========xlibrary start========//
+
+versions.xui = "dev~1.2.2-SNAPSHOT"
+versions.xupdate = "2.1.5"
+versions.xaop = "dab6b77b2f" //1.1.0
+versions.xutil = "2.0.0"
+versions.xhttp2 = "9115cfcd5a" //2.0.9
+versions.xpage = "3.3.0"
+versions.xrouter = "1.0.1"
+
+//========xlibrary end========//
+
+ext.versions = versions
+
+def deps = [:]
+
+def support = [:]
+support.annotations = "com.android.support:support-annotations:$versions.support"
+support.app_compat = "com.android.support:appcompat-v7:$versions.support"
+support.recyclerview = "com.android.support:recyclerview-v7:$versions.support"
+support.cardview = "com.android.support:cardview-v7:$versions.support"
+support.design = "com.android.support:design:$versions.support"
+support.v4 = "com.android.support:support-v4:$versions.support"
+support.core_utils = "com.android.support:support-core-utils:$versions.support"
+deps.support = support
+
+def androidx = [:]
+androidx.annotations = "androidx.annotation:annotation:$versions.annotation"
+androidx.appcompat = "androidx.appcompat:appcompat:$versions.androidx"
+androidx.recyclerview = "androidx.recyclerview:recyclerview:$versions.recyclerview"
+androidx.design = "com.google.android.material:material:$versions.material"
+androidx.multidex = 'androidx.multidex:multidex:2.0.1'
+deps.androidx = androidx
+
+def booster = [:]
+booster.gradle_plugin = "com.didiglobal.booster:booster-gradle-plugin:$versions.booster"
+booster.task_all = "com.didiglobal.booster:booster-task-all:$versions.booster_all"
+booster.transform_all = "com.didiglobal.booster:booster-transform-all:$versions.booster_all"
+//采用 cwebp 对资源进行压缩
+booster.task_compression_cwebp = "com.didiglobal.booster:booster-task-compression-cwebp:$versions.booster"
+//采用 pngquant 对资源进行压缩
+booster.task_compression_pngquant = "com.didiglobal.booster:booster-task-compression-pngquant:$versions.booster"
+//ap_ 文件压缩
+booster.task_processed_res = "com.didiglobal.booster:booster-task-compression-processed-res:$versions.booster"
+//去冗余资源
+booster.task_resource_deredundancy = "com.didiglobal.booster:booster-task-resource-deredundancy:$versions.booster"
+//检查 SNAPSHOT 版本
+booster.task_check_snapshot = "com.didiglobal.booster:booster-task-check-snapshot:$versions.booster"
+//性能瓶颈检测
+booster.transform_lint = "com.didiglobal.booster:booster-transform-lint:$versions.booster"
+//多线程优化
+booster.transform_thread = "com.didiglobal.booster:booster-transform-thread:$versions.booster"
+//资源索引内联
+booster.transform_r_inline = "com.didiglobal.booster:booster-transform-r-inline:$versions.booster"
+//WebView 预加载
+booster.transform_webview = "com.didiglobal.booster:booster-transform-webview:$versions.booster"
+//SharedPreferences 优化
+booster.transform_shared_preferences = "com.didiglobal.booster:booster-transform-shared-preferences:$versions.booster"
+//检查覆盖安装导致的 Resources 和 Assets 未加载的 Bug
+booster.transform_res_check = "com.didiglobal.booster:booster-transform-res-check:$versions.booster"
+//修复 Toast 在 Android 7.1 上的 Bug
+booster.transform_toast = "com.didiglobal.booster:booster-transform-toast:$versions.booster"
+//处理系统 Crash
+booster.transform_activity_thread = "com.didiglobal.booster:booster-transform-activity-thread:$versions.booster"
+deps.booster = booster
+
+def butterknife = [:]
+butterknife.runtime = "com.jakewharton:butterknife:$versions.butterknife"
+butterknife.compiler = "com.jakewharton:butterknife-compiler:$versions.butterknife"
+
+deps.butterknife = butterknife
+
+def espresso = [:]
+espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso"
+espresso.contrib = "androidx.test.espresso:espresso-contrib:$versions.espresso"
+espresso.intents = "androidx.test.espresso:espresso-intents:$versions.espresso"
+deps.espresso = espresso
+
+deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin"
+deps.android_maven_gradle_plugin = "com.github.dcendents:android-maven-gradle-plugin:$versions.android_maven_gradle_plugin"
+deps.gradle_bintray_plugin = "com.jfrog.bintray.gradle:gradle-bintray-plugin:$versions.gradle_bintray_plugin"
+deps.glide = "com.github.bumptech.glide:glide:$versions.glide"
+deps.constraint_layout = "androidx.constraint:constraint-layout:$versions.constraint_layout"
+deps.junit = "junit:junit:$versions.junit"
+deps.runner = "androidx.test:runner:$versions.runner"
+deps.rxjava2 = "io.reactivex.rxjava2:rxjava:$versions.rxjava2"
+deps.rxandroid = "io.reactivex.rxjava2:rxandroid:$versions.rxandroid"
+deps.rxbinding = "com.jakewharton.rxbinding2:rxbinding:$versions.rxbinding"
+deps.gson = "com.google.code.gson:gson:$versions.gson"
+deps.okhttp3 = "com.squareup.okhttp3:okhttp:$versions.okhttp3"
+deps.leakcanary = "com.squareup.leakcanary:leakcanary-android:$versions.leakcanary"
+
+//========xlibrary start=================//
+
+def xlibrary = [:]
+
+xlibrary.xui = "com.github.pppscn:XUI:$versions.xui" //com.github.xuexiangjys:XUI
+xlibrary.xupdate = "com.github.xuexiangjys:XUpdate:$versions.xupdate"
+xlibrary.xaop_runtime = "com.github.pppscn.XAOP:xaop-runtime:$versions.xaop" //com.github.xuexiangjys.XAOP
+xlibrary.xaop_plugin = "com.github.pppscn.XAOP:xaop-plugin:$versions.xaop" //com.github.xuexiangjys.XAOP
+xlibrary.xutil_core = "com.github.xuexiangjys.XUtil:xutil-core:$versions.xutil"
+xlibrary.xutil_sub = "com.github.xuexiangjys.XUtil:xutil-sub:$versions.xutil"
+xlibrary.xhttp2 = "com.github.pppscn:XHttp2:$versions.xhttp2" //com.github.xuexiangjys:XHttp2
+xlibrary.xpage_lib = "com.github.xuexiangjys.XPage:xpage-lib:$versions.xpage"
+xlibrary.xpage_compiler = "com.github.xuexiangjys.XPage:xpage-compiler:$versions.xpage"
+xlibrary.xrouter_runtime = "com.github.xuexiangjys.XRouter:xrouter-runtime:$versions.xrouter"
+xlibrary.xrouter_compiler = "com.github.xuexiangjys.XRouter:xrouter-compiler:$versions.xrouter"
+xlibrary.xrouter_plugin = "com.github.xuexiangjys.XRouter:xrouter-plugin:$versions.xrouter"
+
+deps.xlibrary = xlibrary
+
+//========xlibrary end=================//
+
+def lifecycle = [:]
+lifecycle.runtime = "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle"
+lifecycle.java8 = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle"
+lifecycle.compiler = "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
+lifecycle.viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle"
+lifecycle.livedata_ktx = "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle"
+deps.lifecycle = lifecycle
+
+ext.deps = deps
+
+/**
+ * @return 是否为release
+ */
+def isRelease() {
+ Gradle gradle = getGradle()
+ String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
+
+ Pattern pattern
+ if (tskReqStr.contains("assemble")) {
+ println tskReqStr
+ pattern = Pattern.compile("assemble(\\w*)(Release|Debug)")
+ } else {
+ pattern = Pattern.compile("generate(\\w*)(Release|Debug)")
+ }
+ Matcher matcher = pattern.matcher(tskReqStr)
+
+ if (matcher.find()) {
+ String task = matcher.group(0).toLowerCase()
+ println("[BuildType] Current task: " + task)
+ return task.contains("release")
+ } else {
+ println "[BuildType] NO MATCH FOUND"
+ return true
+ }
+}
+
+ext.isRelease = this.&isRelease
+
+//默认添加代码仓库路径
+static def addRepos(RepositoryHandler handler) {
+ handler.mavenLocal()
+ //handler.google { url 'https://maven.aliyun.com/repository/google' }
+ //handler.mavenCentral { url 'https://maven.aliyun.com/repository/central' }
+ handler.google()
+ handler.mavenCentral()
+ handler.maven { url 'https://maven.aliyun.com/repository/google' }
+ handler.maven { url 'https://maven.aliyun.com/repository/central' }
+ handler.maven { url 'https://maven.aliyun.com/repository/public' }
+ handler.maven { url "https://repo1.maven.org/maven2/" }
+ handler.maven { url 'https://oss.sonatype.org/content/repositories/public' }
+ handler.maven { url "https://jitpack.io" }
+ //Add the Local repository
+ handler.maven { url 'LocalRepository' }
+}
+
+ext.addRepos = this.&addRepos
+
+//自动添加XAOP和XRouter插件
+project.buildscript.configurations.each { configuration ->
+ def dependencies = getProject().dependencies
+ if (configuration.name == "classpath") {
+ //XAOP插件
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xaop_plugin))
+ //XRouter插件
+ configuration.dependencies.add(dependencies.create(deps.xlibrary.xrouter_plugin))
+ //AspectJX
+ //configuration.dependencies.add(dependencies.create('com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'))
+ //kotlin插件
+ configuration.dependencies.add(dependencies.create("org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"))
+ }
+}
\ No newline at end of file
--
Gitblit v1.9.3