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