| | |
| | | *.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 |
对比新文件 |
| | |
| | | # Default ignored files |
| | | /shelf/ |
| | | /workspace.xml |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <component name="ProjectCodeStyleConfiguration"> |
| | | <state> |
| | | <option name="USE_PER_PROJECT_SETTINGS" value="true" /> |
| | | </state> |
| | | </component> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project version="4"> |
| | | <component name="CompilerConfiguration"> |
| | | <bytecodeTargetLevel target="17" /> |
| | | </component> |
| | | </project> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <project version="4"> |
| | | <component name="KotlinJpsPluginSettings"> |
| | | <option name="version" value="1.9.0" /> |
| | | </component> |
| | | </project> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | # Default ignored files |
| | | /shelf/ |
| | | /workspace.xml |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | 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") |
| | | |
| | | |
| | | } |
对比新文件 |
| | |
| | | # 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 |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | 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", |
| | | // ) |
| | | // ) |
| | | // } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
对比新文件 |
| | |
| | | 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() |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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)") |
| | | |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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> |
| | | } |
对比新文件 |
| | |
| | | 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> |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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(), |
| | | ) |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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() |
| | | |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | |
| | | } |
对比新文件 |
| | |
| | | package com.example.firstapp.entity |
| | | |
| | | data class Item( |
| | | val id: Int, |
| | | val title: String, |
| | | val description: String |
| | | ) |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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: 没有匹配到内容") |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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() |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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: ") |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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() // 重新获取并更新数据 |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | }*/ |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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")) |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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" |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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") |
| | | } |
对比新文件 |
| | |
| | | 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}}") |
| | | ) |
对比新文件 |
| | |
| | | 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.") |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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() |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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))) |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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!") |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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...") |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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() |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
| | | |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | |
| | | } |
对比新文件 |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | /* |
| | | * 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 |
| | | } |
| | | } |
| | | } |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <resources> |
| | | <!-- Default screen margins, per the Android Design guidelines. --> |
| | | <dimen name="activity_horizontal_margin">16dp</dimen> |
| | | <dimen name="activity_vertical_margin">16dp</dimen> |
| | | </resources> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | <?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> |
对比新文件 |
| | |
| | | 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) |
| | | } |
| | | } |
对比新文件 |
| | |
| | | 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' |
| | | } |
| | | */ |
对比新文件 |
| | |
| | | // 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(':', '/')}")) |
| | | //} |
对比新文件 |
| | |
| | | # 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 |
| | | |
对比新文件 |
| | |
| | | [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" } |
| | | |
对比新文件 |
| | |
| | | #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 |
对比新文件 |
| | |
| | | #!/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" "$@" |
对比新文件 |
| | |
| | | @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 |
对比新文件 |
| | |
| | | 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' |
对比新文件 |
| | |
| | | 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")) |
| | | } |
| | | } |