diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..1afa24c5 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/LocalRepository +/keystores +/local.properties +/.idea/caches +/.idea/codeStyles +/.idea/inspectionProfiles +/.idea/libraries +/.idea/dictionaries +/.idea/markdown-navigator +/.idea/*.xml +.DS_Store +/build +/captures +.externalNativeBuild \ No newline at end of file diff --git a/android/.idea/copyright/profiles_settings.xml b/android/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..066b2557 --- /dev/null +++ b/android/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/android/.idea/copyright/xuexiang.xml b/android/.idea/copyright/xuexiang.xml new file mode 100644 index 00000000..d785b8e0 --- /dev/null +++ b/android/.idea/copyright/xuexiang.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/android/LICENSE b/android/LICENSE new file mode 100644 index 00000000..44cc482b --- /dev/null +++ b/android/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "{}" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2018 xuexiangjys + + 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 + + http://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. \ No newline at end of file diff --git a/android/移动端.txt b/android/README.md similarity index 100% rename from android/移动端.txt rename to android/README.md diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..603984ae --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,131 @@ +apply plugin: 'com.android.application' +apply plugin: 'img-optimizer' +//打包时,记得设置true启用 +if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) { + apply plugin: 'com.didiglobal.booster' +} + +android { + compileSdkVersion build_versions.target_sdk + buildToolsVersion build_versions.build_tools + + defaultConfig { + applicationId "com.kerwin.wumei" + minSdkVersion 17 + targetSdkVersion build_versions.target_sdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + multiDexEnabled true + vectorDrawables.useSupportLibrary = true + + javaCompileOptions { + annotationProcessorOptions { + arguments = [ moduleName : project.getName() ] + } + } + } + + signingConfigs { + if (isNeedPackage.toBoolean()) { + release { + storeFile file(app_release.storeFile) + storePassword app_release.storePassword + keyAlias app_release.keyAlias + keyPassword app_release.keyPassword + } + } + + debug { + storeFile file("./debug.jks") + storePassword "123456" + keyAlias "debug" + keyPassword "123456" + } + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + if (isNeedPackage.toBoolean()) { + signingConfig signingConfigs.release + + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + def appID = properties.getProperty("APP_ID_UMENG") + if (appID != null) { + buildConfigField "String", "APP_ID_UMENG", appID + } else { + buildConfigField "String", "APP_ID_UMENG", '""' + } + } else { + signingConfig signingConfigs.debug + buildConfigField "String", "APP_ID_UMENG", '""' + } + } + + debug { + debuggable true + minifyEnabled false + + signingConfig signingConfigs.debug + buildConfigField "String", "APP_ID_UMENG", '""' + } + } + + lintOptions { + abortOnError false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(path: ':esptouch') + testImplementation deps.junit + androidTestImplementation deps.runner + androidTestImplementation deps.espresso.core + + //分包 + implementation deps.androidx.multidex + + implementation 'com.alibaba.android:vlayout:1.2.36' + //下拉刷新 + implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.5' + implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.5' + //WebView + implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.0' + implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.0'//选填 + //腾讯的键值对存储mmkv + implementation 'com.tencent:mmkv:1.0.22' + //屏幕适配AutoSize + implementation 'me.jessyan:autosize:1.1.2' + //umeng统计 + implementation 'com.umeng.umsdk:analytics:8.0.2' + implementation 'com.umeng.umsdk:common:2.0.2' + + //预加载占位控件 + implementation 'me.samlss:broccoli:1.0.0' + + implementation 'com.zzhoujay.richtext:richtext:3.0.8' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + + //ANR异常捕获 + implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0' + + //美团多渠道打包 + implementation 'com.meituan.android.walle:library:1.1.6' +} +//自动添加X-Library依赖 +apply from: 'x-library.gradle' +//walle多渠道打包 +apply from: 'multiple-channel.gradle' + + diff --git a/android/app/channel b/android/app/channel new file mode 100644 index 00000000..10c8afd1 --- /dev/null +++ b/android/app/channel @@ -0,0 +1,25 @@ +# 美团 +meituan +# 三星 +samsungapps +# 小米 +xiaomi +# 91助手 +91com +# 魅族 +meizu +# 豌豆荚 +wandou +# Google Play +googleplay +# 百度 +baidu +# 360 +360cn +# 应用宝 +myapp +# 华为 +huawei +# 蒲公英 +pgyer +github \ No newline at end of file diff --git a/android/app/debug.jks b/android/app/debug.jks new file mode 100644 index 00000000..d49fb2b1 Binary files /dev/null and b/android/app/debug.jks differ diff --git a/android/app/multiple-channel.gradle b/android/app/multiple-channel.gradle new file mode 100644 index 00000000..5dc6dbf1 --- /dev/null +++ b/android/app/multiple-channel.gradle @@ -0,0 +1,10 @@ +apply plugin: 'walle' + +walle { + // 指定渠道包的输出路径 + apkOutputFolder = new File("${project.buildDir}/outputs/channels") + // 定制渠道包的APK的文件名称 + apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk' + // 渠道配置文件 + channelFile = new File("${project.getProjectDir()}/channel") +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..2b565e20 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,269 @@ +#=========================================基础不变的混淆配置=========================================## +#指定代码的压缩级别 +-optimizationpasses 5 +#包名不混合大小写 +-dontusemixedcaseclassnames +#不去忽略非公共的库类 +-dontskipnonpubliclibraryclasses +# 指定不去忽略非公共的库的类的成员 +-dontskipnonpubliclibraryclassmembers +#优化 不优化输入的类文件 +-dontoptimize +#预校验 +-dontpreverify +#混淆时是否记录日志 +-verbose +# 混淆时所采用的算法 +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* +#保护注解 +-keepattributes *Annotation* +#忽略警告 +-ignorewarnings + +##记录生成的日志数据,gradle build时在本项目根目录输出## +#apk 包内所有 class 的内部结构 +-dump class_files.txt +#未混淆的类和成员 +-printseeds seeds.txt +#列出从 apk 中删除的代码 +-printusage unused.txt +#混淆前后的映射 +-printmapping mapping.txt +# 并保留源文件名为"Proguard"字符串,而非原始的类名 并保留行号 +-keepattributes SourceFile,LineNumberTable +########记录生成的日志数据,gradle build时 在本项目根目录输出-end##### + +#需要保留的东西 +# 保持哪些类不被混淆 +-keep public class * extends android.app.Fragment +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class * extends android.support.v4.** +-keep public class com.android.vending.licensing.ILicensingService + +#如果有引用v4包可以添加下面这行 +-keep public class * extends android.support.v4.app.Fragment + +##########JS接口类不混淆,否则执行不了 +-dontwarn com.android.JsInterface.** +-keep class com.android.JsInterface.** {*; } + +#极光推送和百度lbs android sdk一起使用proguard 混淆的问题#http的类被混淆后,导致apk定位失败,保持apache 的http类不被混淆就好了 +-dontwarn org.apache.** +-keep class org.apache.**{ *; } + +-keep public class * extends android.view.View { + public (android.content.Context); + public (android.content.Context, android.util.AttributeSet); + public (android.content.Context, android.util.AttributeSet, int); + public void set*(...); + } + +#保持 native 方法不被混淆 +-keepclasseswithmembernames class * { + native ; +} + +#保持自定义控件类不被混淆 +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +#保持自定义控件类不被混淆 +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +#保持 Parcelable 不被混淆 +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +#保持 Serializable 不被混淆 +-keepnames class * implements java.io.Serializable + +#保持 Serializable 不被混淆并且enum 类也不被混淆 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +#保持枚举 enum 类不被混淆 如果混淆报错,建议直接使用上面的 -keepclassmembers class * implements java.io.Serializable即可 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepclassmembers class * { + public void *ButtonClicked(android.view.View); +} + +#不混淆资源类 +-keep class **.R$* {*;} + +#===================================混淆保护自己项目的部分代码以及引用的第三方jar包library=============================####### +#如果引用了v4或者v7包 +-dontwarn android.support.** + + +# AndroidX 防止混淆 +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** +-keep class com.google.android.material.** {*;} +-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-keepclassmembers class * { + @androidx.annotation.Keep *; +} + +# zxing +-dontwarn com.google.zxing.** +-keep class com.google.zxing.**{*;} + +#SignalR推送 +-keep class microsoft.aspnet.signalr.** { *; } + +# 极光推送混淆 +-dontoptimize +-dontpreverify +-dontwarn cn.jpush.** +-keep class cn.jpush.** { *; } +-dontwarn cn.jiguang.** +-keep class cn.jiguang.** { *; } + +# 数据库框架OrmLite +-keepattributes *DatabaseField* +-keepattributes *DatabaseTable* +-keepattributes *SerializedName* +-keep class com.j256.** +-keepclassmembers class com.j256.** { *; } +-keep enum com.j256.** +-keepclassmembers enum com.j256.** { *; } +-keep interface com.j256.** +-keepclassmembers interface com.j256.** { *; } + +#XHttp2 +-keep class com.xuexiang.xhttp2.model.** { *; } +-keep class com.xuexiang.xhttp2.cache.model.** { *; } +-keep class com.xuexiang.xhttp2.cache.stategy.**{*;} +-keep class com.xuexiang.xhttp2.annotation.** { *; } + +#okhttp +-dontwarn com.squareup.okhttp3.** +-keep class com.squareup.okhttp3.** { *;} +-dontwarn okio.** +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault +-dontwarn javax.annotation.** + +#如果用到Gson解析包的,直接添加下面这几行就能成功混淆,不然会报错 +-keepattributes Signature +-keep class com.google.gson.stream.** { *; } +-keepattributes EnclosingMethod +-keep class org.xz_sale.entity.**{*;} +-keep class com.google.gson.** {*;} +-keep class com.google.**{*;} +-keep class sun.misc.Unsafe { *; } +-keep class com.google.gson.stream.** { *; } +-keep class com.google.gson.examples.android.model.** { *; } + +# Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# Retrofit +-dontwarn retrofit2.** +-keep class retrofit2.** { *; } +-keepattributes Exceptions + +# RxJava RxAndroid +-dontwarn sun.misc.** +-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { + long producerIndex; + long consumerIndex; +} +-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { + rx.internal.util.atomic.LinkedQueueNode producerNode; +} +-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { + rx.internal.util.atomic.LinkedQueueNode consumerNode; +} + +-dontwarn okio.** +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault +-dontwarn javax.annotation.** + +# fastjson +-dontwarn com.alibaba.fastjson.** +-keep class com.alibaba.fastjson.** { *; } +-keepattributes Signature + +# xpage +-keep class com.xuexiang.xpage.annotation.** { *; } +-keep class com.xuexiang.xpage.config.** { *; } + +# xaop +-keep @com.xuexiang.xaop.annotation.* class * {*;} +-keep @org.aspectj.lang.annotation.* class * {*;} +-keep class * { + @com.xuexiang.xaop.annotation.* ; + @org.aspectj.lang.annotation.* ; +} +-keepclassmembers class * { + @com.xuexiang.xaop.annotation.* ; + @org.aspectj.lang.annotation.* ; +} + +# xrouter +-keep public class com.xuexiang.xrouter.routes.**{*;} +-keep class * implements com.xuexiang.xrouter.facade.template.ISyringe{*;} +# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口 +-keep interface * implements com.xuexiang.xrouter.facade.template.IProvider +# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现 +-keep class * implements com.xuexiang.xrouter.facade.template.IProvider + +# xupdate +-keep class com.xuexiang.xupdate.entity.** { *; } + +# xvideo +-keep class com.xuexiang.xvideo.jniinterface.** { *; } + +# xipc +-keep @com.xuexiang.xipc.annotation.* class * {*;} +-keep class * { + @com.xuexiang.xipc.annotation.* ; +} +-keepclassmembers class * { + @com.xuexiang.xipc.annotation.* ; +} + +# umeng统计 +-keep class com.umeng.** {*;} +-keepclassmembers class * { + public (org.json.JSONObject); +} +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; } diff --git a/android/app/src/androidTest/java/com/kerwin/templateproject/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/kerwin/templateproject/ExampleInstrumentedTest.java new file mode 100644 index 00000000..10e93d68 --- /dev/null +++ b/android/app/src/androidTest/java/com/kerwin/templateproject/ExampleInstrumentedTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.kerwin.wumei", appContext.getPackageName()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0893f1b7 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/tips.json b/android/app/src/main/assets/tips.json new file mode 100644 index 00000000..969a4c43 --- /dev/null +++ b/android/app/src/main/assets/tips.json @@ -0,0 +1,21 @@ +{ + "Code": 0, + "Data": [ + { + "title": "微信公众号", + "content": "获取更多资讯内容,欢迎微信搜索公众号:「我的Android开源之旅」" + }, + { + "title": "关于作者", + "content": "点击关注作者,了解最新动态!
Github
\n知乎
\n掘金
简书
\n思否
\n哔哩哔哩
\n今日头条" + }, + { + "title": "赞助作者", + "content": "你的打赏是我维护的动力,点击此处支持我吧!" + }, + { + "title": "QQ交流群", + "content": "XUI开源交流1号群
XUI开源交流2号群
AndroidGitHub开源交流群
XUpdate官方交流群" + } + ] +} diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..a0b4b39c Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/com/kerwin/wumei/MyApp.java b/android/app/src/main/java/com/kerwin/wumei/MyApp.java new file mode 100644 index 00000000..6cbf244e --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/MyApp.java @@ -0,0 +1,114 @@ +package com.kerwin.wumei; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.LocationManager; +import android.net.wifi.WifiManager; +import android.os.Build; + +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.multidex.MultiDex; + +import com.kerwin.wumei.BuildConfig; +import com.kerwin.wumei.utils.sdkinit.ANRWatchDogInit; +import com.kerwin.wumei.utils.sdkinit.UMengInit; +import com.kerwin.wumei.utils.sdkinit.XBasicLibInit; +import com.kerwin.wumei.utils.sdkinit.XUpdateInit; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author xuexiang + * @since 2018/11/7 下午1:12 + */ +public class MyApp extends Application { + + private static MyApp app; + private MutableLiveData mBroadcastData; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + + switch (action) { + case WifiManager.NETWORK_STATE_CHANGED_ACTION: + case LocationManager.PROVIDERS_CHANGED_ACTION: + mBroadcastData.setValue(action); + break; + } + } + }; + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + //解决4.x运行崩溃的问题 + MultiDex.install(this); + } + + @Override + public void onCreate() { + super.onCreate(); + initLibs(); + + app = this; + mBroadcastData = new MutableLiveData<>(); + IntentFilter filter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + filter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION); + } + registerReceiver(mReceiver, filter); + } + + @Override + public void onTerminate() { + super.onTerminate(); + unregisterReceiver(mReceiver); + } + + public static MyApp getInstance() { + return app; + } + + public void observeBroadcast(LifecycleOwner owner, Observer observer) { + mBroadcastData.observe(owner, observer); + } + + + /** + * 初始化基础库 + */ + private void initLibs() { + XBasicLibInit.init(this); + + XUpdateInit.init(this); + + //运营统计数据运行时不初始化 + if (!MyApp.isDebug()) { + UMengInit.init(this); + } + + //ANR监控 + ANRWatchDogInit.init(); + } + + + /** + * @return 当前app是否是调试开发模式 + */ + public static boolean isDebug() { + return BuildConfig.DEBUG; + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/activity/AddDeviceActivity.java b/android/app/src/main/java/com/kerwin/wumei/activity/AddDeviceActivity.java new file mode 100644 index 00000000..9b89c562 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/activity/AddDeviceActivity.java @@ -0,0 +1,410 @@ + +package com.kerwin.wumei.activity; + +import android.Manifest; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.KeyEvent; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.location.LocationManagerCompat; + +import com.espressif.iot.esptouch.EsptouchTask; +import com.espressif.iot.esptouch.IEsptouchResult; +import com.espressif.iot.esptouch.IEsptouchTask; +import com.espressif.iot.esptouch.util.ByteUtil; +import com.espressif.iot.esptouch.util.TouchNetUtil; +import com.kerwin.wumei.R; +import com.kerwin.wumei.adapter.entity.EspTouchViewModel; +import com.kerwin.wumei.core.BaseActivity; +import com.kerwin.wumei.fragment.LoginFragment; +import com.kerwin.wumei.utils.NetUtils; +import com.xuexiang.xui.utils.KeyboardUtils; +import com.xuexiang.xui.utils.StatusBarUtils; +import com.xuexiang.xutil.display.Colors; + +import java.lang.ref.WeakReference; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +public class AddDeviceActivity extends BaseActivity { + + // begin esptouch ------------------------------------- + private static final String TAG = AddDeviceActivity.class.getSimpleName(); + private static final int REQUEST_PERMISSION = 0x01; + private EspTouchViewModel mViewModel; + private EsptouchAsyncTask4 mTask; + private WifiManager mWifiManager; + private List ssids; + private String selectedSSID; + + public String GetSelectedSSID(){ + return selectedSSID; + } + public List GetSsids(){ + return ssids; + } + public EspTouchViewModel GetMViewModel(){ + return mViewModel; + } + + public void executeEsptouch() { + EspTouchViewModel viewModel = mViewModel; + // byte[] ssid = viewModel.ssidBytes == null ? ByteUtil.getBytesByString(viewModel.ssid): viewModel.ssidBytes; + CharSequence ssidStr=mViewModel.ssidSpinner.getText(); + byte[] ssid= ByteUtil.getBytesByString(ssidStr.toString()); + CharSequence pwdStr = mViewModel.apPasswordEdit.getText(); + byte[] password = pwdStr == null ? null : ByteUtil.getBytesByString(pwdStr.toString()); + byte[] bssid = TouchNetUtil.parseBssid2bytes(viewModel.bssid); + byte[] broadcast = {(byte) (mViewModel.packageModeGroup.getCheckedRadioButtonId() == R.id.packageBroadcast? 1 : 0)}; + byte[] deviceCount = "1".getBytes(); + if (mTask != null) { + mTask.cancelEsptouch(); + } + mTask = new EsptouchAsyncTask4(this); + mTask.execute(ssid, bssid, password, deviceCount, broadcast); + } + + public void onWifiChanged() { + StateResult stateResult = check(); + mViewModel.message = stateResult.message; + mViewModel.ssid = stateResult.ssid; + mViewModel.ssidBytes = stateResult.ssidBytes; + mViewModel.bssid = stateResult.bssid; + mViewModel.confirmEnable = false; + if (stateResult.wifiConnected) { + mViewModel.confirmEnable = true; + if (stateResult.is5G) { + mViewModel.message = getString(R.string.esptouch1_wifi_5g_message); + } + } else { + if (mTask != null) { + mTask.cancelEsptouch(); + mTask = null; + new AlertDialog.Builder(AddDeviceActivity.this) + .setMessage(R.string.esptouch1_configure_wifi_change_message) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + } + mViewModel.invalidateAll(); + } + + protected static class StateResult { + public CharSequence message = null; + public boolean permissionGranted = false; + public boolean locationRequirement = false; + public boolean wifiConnected = false; + public boolean is5G = false; + public InetAddress address = null; + public String ssid = null; + public byte[] ssidBytes = null; + public String bssid = null; + } + + private StateResult check() { + StateResult result = checkPermission(); + if (!result.permissionGranted) { + return result; + } + result = checkLocation(); + result.permissionGranted = true; + if (result.locationRequirement) { + return result; + } + result = checkWifi(); + result.permissionGranted = true; + result.locationRequirement = false; + return result; + } + + protected StateResult checkWifi() { + StateResult result = new StateResult(); + result.wifiConnected = false; + WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + + // 获取wifi列表 + mWifiManager.startScan(); + List scanWifiList = mWifiManager.getScanResults(); + List wifiList = new ArrayList<>(); + ssids=new ArrayList<>(); + if (scanWifiList != null && scanWifiList.size() > 0) { + HashMap signalStrength = new HashMap(); + for (int i = 0; i < scanWifiList.size(); i++) { + ScanResult scanResult = scanWifiList.get(i); + Log.e(TAG, "搜索的wifi-ssid:" + scanResult.SSID); + if (!scanResult.SSID.isEmpty()) { + String key = scanResult.SSID + " " + scanResult.capabilities; + if (!signalStrength.containsKey(key)) { + signalStrength.put(key, i); + wifiList.add(scanResult); + ssids.add(scanResult.SSID); + } + } + } + } + + boolean connected = NetUtils.isWifiConnected(mWifiManager); + if (!connected) { + result.message = getString(R.string.esptouch_message_wifi_connection); + return result; + } + + String ssid = NetUtils.getSsidString(wifiInfo); + selectedSSID=ssid; + int ipValue = wifiInfo.getIpAddress(); + if (ipValue != 0) { + result.address = NetUtils.getAddress(wifiInfo.getIpAddress()); + } else { + result.address = NetUtils.getIPv4Address(); + if (result.address == null) { + result.address = NetUtils.getIPv6Address(); + } + } + + result.wifiConnected = true; + result.message = ""; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + result.is5G = NetUtils.is5G(wifiInfo.getFrequency()); + } + if (result.is5G) { + result.message = getString(R.string.esptouch_message_wifi_frequency); + } + result.ssid = ssid; + result.ssidBytes = NetUtils.getRawSsidBytesOrElse(wifiInfo, ssid.getBytes()); + result.bssid = wifiInfo.getBSSID(); + return result; + } + + protected StateResult checkLocation() { + StateResult result = new StateResult(); + result.locationRequirement = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + LocationManager manager = getSystemService(LocationManager.class); + boolean enable = manager != null && LocationManagerCompat.isLocationEnabled(manager); + if (!enable) { + result.message = getString(R.string.esptouch_message_location); + return result; + } + } + + result.locationRequirement = false; + return result; + } + + protected StateResult checkPermission() { + StateResult result = new StateResult(); + result.permissionGranted = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + boolean locationGranted = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + if (!locationGranted) { + String[] splits = getString(R.string.esptouch_message_permission).split("\n"); + if (splits.length != 2) { + throw new IllegalArgumentException("Invalid String @RES esptouch_message_permission"); + } + SpannableStringBuilder ssb = new SpannableStringBuilder(splits[0]); + ssb.append('\n'); + SpannableString clickMsg = new SpannableString(splits[1]); + ForegroundColorSpan clickSpan = new ForegroundColorSpan(0xFF0022FF); + clickMsg.setSpan(clickSpan, 0, clickMsg.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + ssb.append(clickMsg); + result.message = ssb; + return result; + } + } + + result.permissionGranted = true; + return result; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQUEST_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onWifiChanged(); + } else { + new AlertDialog.Builder(this) + .setTitle(R.string.esptouch1_location_permission_title) + .setMessage(R.string.esptouch1_location_permission_message) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, (dialog, which) -> finish()) + .show(); + } + + return; + } + + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + public static class EsptouchAsyncTask4 extends AsyncTask> { + private WeakReference mActivity; + private final Object mLock = new Object(); + private ProgressDialog mProgressDialog; + private AlertDialog mResultDialog; + private IEsptouchTask mEsptouchTask; + + EsptouchAsyncTask4(AddDeviceActivity activity) { + mActivity = new WeakReference<>(activity); + } + + public void cancelEsptouch() { + cancel(true); + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + } + if (mResultDialog != null) { + mResultDialog.dismiss(); + } + if (mEsptouchTask != null) { + mEsptouchTask.interrupt(); + } + } + + @Override + protected void onPreExecute() { + Activity activity = mActivity.get(); + mProgressDialog = new ProgressDialog(activity); + mProgressDialog.setMessage(activity.getString(R.string.esptouch1_configuring_message)); + mProgressDialog.setCanceledOnTouchOutside(false); + mProgressDialog.setOnCancelListener(dialog -> { + synchronized (mLock) { + if (mEsptouchTask != null) { + mEsptouchTask.interrupt(); + } + } + }); + mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, activity.getText(android.R.string.cancel), + (dialog, which) -> { + synchronized (mLock) { + if (mEsptouchTask != null) { + mEsptouchTask.interrupt(); + } + } + }); + mProgressDialog.show(); + } + + @Override + protected void onProgressUpdate(IEsptouchResult... values) { + Context context = mActivity.get(); + if (context != null) { + IEsptouchResult result = values[0]; + Log.i(TAG, "EspTouchResult: " + result); + String text = result.getBssid() + " is connected to the wifi"; + Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); + } + } + + @Override + protected List doInBackground(byte[]... params) { + AddDeviceActivity activity = mActivity.get(); + int taskResultCount; + synchronized (mLock) { + byte[] apSsid = params[0]; + byte[] apBssid = params[1]; + byte[] apPassword = params[2]; + byte[] deviceCountData = params[3]; + byte[] broadcastData = params[4]; + taskResultCount = deviceCountData.length == 0 ? -1 : Integer.parseInt(new String(deviceCountData)); + Context context = activity.getApplicationContext(); + mEsptouchTask = new EsptouchTask(apSsid, apBssid, apPassword, context); + mEsptouchTask.setPackageBroadcast(broadcastData[0] == 1); + mEsptouchTask.setEsptouchListener(this::publishProgress); + } + return mEsptouchTask.executeForResults(taskResultCount); + } + + @Override + protected void onPostExecute(List result) { + AddDeviceActivity activity = mActivity.get(); + activity.mTask = null; + mProgressDialog.dismiss(); + if (result == null) { + mResultDialog = new AlertDialog.Builder(activity) + .setMessage(R.string.esptouch1_configure_result_failed_port) + .setPositiveButton(android.R.string.ok, null) + .show(); + mResultDialog.setCanceledOnTouchOutside(false); + return; + } + + // check whether the task is cancelled and no results received + IEsptouchResult firstResult = result.get(0); + if (firstResult.isCancelled()) { + return; + } + // the task received some results including cancelled while + // executing before receiving enough results + + if (!firstResult.isSuc()) { + mResultDialog = new AlertDialog.Builder(activity) + .setMessage(R.string.esptouch1_configure_result_failed) + .setPositiveButton(android.R.string.ok, null) + .show(); + mResultDialog.setCanceledOnTouchOutside(false); + return; + } + + ArrayList resultMsgList = new ArrayList<>(result.size()); + for (IEsptouchResult touchResult : result) { + String message = activity.getString(R.string.esptouch1_configure_result_success_item, + touchResult.getBssid(), touchResult.getInetAddress().getHostAddress()); + resultMsgList.add(message); + } + CharSequence[] items = new CharSequence[resultMsgList.size()]; + mResultDialog = new AlertDialog.Builder(activity) + .setTitle(R.string.esptouch1_configure_result_success) + .setItems(resultMsgList.toArray(items), null) + .setPositiveButton(android.R.string.ok, null) + .show(); + mResultDialog.setCanceledOnTouchOutside(false); + } + } + + // end esptouch ---------------------------------------- + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mWifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); + mViewModel = new EspTouchViewModel(); + } + + @Override + protected boolean isSupportSlideBack() { + return true; + } + + @Override + protected void initStatusBarStyle() { + StatusBarUtils.initStatusBarStyle(this, false, Colors.WHITE); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/activity/LoginActivity.java b/android/app/src/main/java/com/kerwin/wumei/activity/LoginActivity.java new file mode 100644 index 00000000..bab79c1c --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/activity/LoginActivity.java @@ -0,0 +1,36 @@ + +package com.kerwin.wumei.activity; + +import android.os.Bundle; +import android.view.KeyEvent; + +import com.kerwin.wumei.core.BaseActivity; +import com.kerwin.wumei.fragment.LoginFragment; +import com.xuexiang.xui.utils.KeyboardUtils; +import com.xuexiang.xui.utils.StatusBarUtils; +import com.xuexiang.xutil.display.Colors; + + +public class LoginActivity extends BaseActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + openPage(LoginFragment.class, getIntent().getExtras()); + } + + @Override + protected boolean isSupportSlideBack() { + return false; + } + + @Override + protected void initStatusBarStyle() { + StatusBarUtils.initStatusBarStyle(this, false, Colors.WHITE); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/activity/MainActivity.java b/android/app/src/main/java/com/kerwin/wumei/activity/MainActivity.java new file mode 100644 index 00000000..9c05b48f --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/activity/MainActivity.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.activity; + +import android.Manifest; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.core.location.LocationManagerCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.viewpager.widget.ViewPager; + +import com.espressif.iot.esptouch.EsptouchTask; +import com.espressif.iot.esptouch.IEsptouchResult; +import com.espressif.iot.esptouch.IEsptouchTask; +import com.espressif.iot.esptouch.util.ByteUtil; +import com.espressif.iot.esptouch.util.TouchNetUtil; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.navigation.NavigationView; +import com.kerwin.wumei.R; +import com.kerwin.wumei.adapter.entity.EspTouchViewModel; +import com.kerwin.wumei.core.BaseActivity; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.fragment.AboutFragment; +import com.kerwin.wumei.fragment.FeedbackFragment; +import com.kerwin.wumei.fragment.MessageFragment; +import com.kerwin.wumei.fragment.SettingsFragment; +import com.kerwin.wumei.fragment.device.AddDeviceFragment; +import com.kerwin.wumei.fragment.device.GroupFragment; +import com.kerwin.wumei.fragment.device.SceneFragment; +import com.kerwin.wumei.fragment.device.ShareDeviceFragment; +import com.kerwin.wumei.fragment.news.NewsFragment; +import com.kerwin.wumei.fragment.profile.ProfileFragment; +import com.kerwin.wumei.fragment.device.DeviceFragment; +import com.kerwin.wumei.utils.NetUtils; +import com.kerwin.wumei.utils.Utils; +import com.kerwin.wumei.utils.XToastUtils; +import com.kerwin.wumei.widget.GuideTipsDialog; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.core.PageOption; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.adapter.FragmentAdapter; +import com.xuexiang.xui.adapter.simple.AdapterItem; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.utils.ThemeUtils; +import com.xuexiang.xui.widget.imageview.RadiusImageView; +import com.xuexiang.xui.widget.popupwindow.popup.XUISimplePopup; +import com.xuexiang.xutil.XUtil; +import com.xuexiang.xutil.common.ClickUtils; +import com.xuexiang.xutil.common.CollectionUtils; +import com.xuexiang.xutil.display.Colors; + +import java.lang.ref.WeakReference; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import butterknife.BindView; + +public class MainActivity extends BaseActivity implements View.OnClickListener, ViewPager.OnPageChangeListener, BottomNavigationView.OnNavigationItemSelectedListener, ClickUtils.OnClick2ExitListener, Toolbar.OnMenuItemClickListener { + + @BindView(R.id.toolbar) + Toolbar toolbar; + @BindView(R.id.view_pager) + ViewPager viewPager; + /** + * 底部导航栏 + */ + @BindView(R.id.bottom_navigation) + BottomNavigationView bottomNavigation; + /** + * 侧边栏 + */ + @BindView(R.id.nav_view) + NavigationView navView; + @BindView(R.id.drawer_layout) + DrawerLayout drawerLayout; + + private String[] mTitles; + + @Override + protected int getLayoutId() { + return R.layout.activity_main; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initViews(); + initListeners(); + } + + @Override + protected boolean isSupportSlideBack() { + return true; + } + + private void initViews() { + mTitles = ResUtils.getStringArray(R.array.home_titles); + toolbar.setTitle(mTitles[0]); + toolbar.inflateMenu(R.menu.menu_main); + toolbar.setOnMenuItemClickListener(this); + initHeader(); + + //主页内容填充 + BaseFragment[] fragments = new BaseFragment[]{ + new DeviceFragment(), + new SceneFragment(), + new NewsFragment(), + new ProfileFragment(), + }; + FragmentAdapter adapter = new FragmentAdapter<>(getSupportFragmentManager(), fragments); + viewPager.setOffscreenPageLimit(mTitles.length - 1); + viewPager.setAdapter(adapter); + + GuideTipsDialog.showTips(this); + } + + /** + * 侧边栏头部 + */ + private void initHeader() { + navView.setItemIconTintList(null); + View headerView = navView.getHeaderView(0); + LinearLayout navHeader = headerView.findViewById(R.id.nav_header); + RadiusImageView ivAvatar = headerView.findViewById(R.id.iv_avatar); + TextView tvAvatar = headerView.findViewById(R.id.tv_avatar); + TextView tvSign = headerView.findViewById(R.id.tv_sign); + + if (Utils.isColorDark(ThemeUtils.resolveColor(this, R.attr.colorAccent))) { + tvAvatar.setTextColor(Colors.WHITE); + tvSign.setTextColor(Colors.WHITE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ivAvatar.setImageTintList(ResUtils.getColors(R.color.xui_config_color_white)); + } + } else { + tvAvatar.setTextColor(ThemeUtils.resolveColor(this, R.attr.xui_config_color_title_text)); + tvSign.setTextColor(ThemeUtils.resolveColor(this, R.attr.xui_config_color_explain_text)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ivAvatar.setImageTintList(ResUtils.getColors(R.color.xui_config_color_gray_3)); + } + } + + // TODO: 2019-10-09 初始化数据 + ivAvatar.setImageResource(R.drawable.ic_default_head); + tvAvatar.setText("15208747707"); + tvSign.setText("物美点亮智慧生活..."); + navHeader.setOnClickListener(this); + } + + protected void initListeners() { + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawerLayout.addDrawerListener(toggle); + toggle.syncState(); + + //侧边栏点击事件 + navView.setNavigationItemSelectedListener(menuItem -> { + switch (menuItem.getItemId()) { + case R.id.nav_add_device: + PageOption.to(AddDeviceFragment.class) //跳转的fragment + .setAnim(CoreAnim.slide) //页面转场动画 + .setRequestCode(100) //请求码,用于返回结果 + .setAddToBackStack(true) //是否加入堆栈 + .setNewActivity(true,AddDeviceActivity.class) //是否使用新的Activity打开 + .open(this); //打开页面进行跳转 + break; + case R.id.nav_settings: + openNewPage(SettingsFragment.class); + break; + case R.id.nav_about: + openNewPage(AboutFragment.class); + break; + case R.id.nav_message: + openNewPage(MessageFragment.class); + break; + case R.id.nav_share_device: + openNewPage(ShareDeviceFragment.class); + break; + case R.id.nav_group: + openNewPage(GroupFragment.class); + break; + default: + XToastUtils.toast("点击了:" + menuItem.getTitle()); + break; + } + return true; + }); + + //主页事件监听 + viewPager.addOnPageChangeListener(this); + bottomNavigation.setOnNavigationItemSelectedListener(this); + } + + + + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_device: + PageOption.to(AddDeviceFragment.class) //跳转的fragment + .setAnim(CoreAnim.slide) //页面转场动画 + .setRequestCode(100) //请求码,用于返回结果 + .setAddToBackStack(true) //是否加入堆栈 + .setNewActivity(true, AddDeviceActivity.class) //是否使用新的Activity打开 + .open(this); //打开页面进行跳转 + break; + default: + break; + } + return false; + } + + @SingleClick + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.nav_header: + XToastUtils.toast("功能完善中..."); + break; + default: + break; + } + } + + //=============ViewPager===================// + + @Override + public void onPageScrolled(int i, float v, int i1) { + + } + + @Override + public void onPageSelected(int position) { + MenuItem item = bottomNavigation.getMenu().getItem(position); + toolbar.setTitle(item.getTitle()); + item.setChecked(true); + } + + @Override + public void onPageScrollStateChanged(int i) { + + } + + //================Navigation================// + + /** + * 底部导航栏点击事件 + * + * @param menuItem + * @return + */ + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { + int index = CollectionUtils.arrayIndexOf(mTitles, menuItem.getTitle()); + if (index != -1) { + toolbar.setTitle(menuItem.getTitle()); + viewPager.setCurrentItem(index, false); + return true; + } + return false; + } + + + /** + * 菜单、返回键响应 + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + ClickUtils.exitBy2Click(2000, this); + } + return true; + } + + @Override + public void onRetry() { + XToastUtils.toast("再按一次退出程序"); + } + + @Override + public void onExit() { + XUtil.exitApp(); + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/activity/SplashActivity.java b/android/app/src/main/java/com/kerwin/wumei/activity/SplashActivity.java new file mode 100644 index 00000000..9f9328e5 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/activity/SplashActivity.java @@ -0,0 +1,68 @@ +package com.kerwin.wumei.activity; + +import android.view.KeyEvent; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.SettingUtils; +import com.kerwin.wumei.utils.TokenUtils; +import com.kerwin.wumei.utils.Utils; +import com.xuexiang.xui.utils.KeyboardUtils; +import com.xuexiang.xui.widget.activity.BaseSplashActivity; +import com.xuexiang.xutil.app.ActivityUtils; + +import me.jessyan.autosize.internal.CancelAdapt; + +/** + * 启动页【无需适配屏幕大小】 + * + */ +public class SplashActivity extends BaseSplashActivity implements CancelAdapt { + + @Override + protected long getSplashDurationMillis() { + return 500; + } + + /** + * activity启动后的初始化 + */ + @Override + protected void onCreateActivity() { + initSplashView(R.drawable.xui_config_bg_splash); + startSplash(false); + } + + + /** + * 启动页结束后的动作 + */ + @Override + protected void onSplashFinished() { + if (SettingUtils.isAgreePrivacy()) { + loginOrGoMainPage(); + } else { + Utils.showPrivacyDialog(this, (dialog, which) -> { + dialog.dismiss(); + SettingUtils.setIsAgreePrivacy(true); + loginOrGoMainPage(); + }); + } + } + + private void loginOrGoMainPage() { + if (TokenUtils.hasToken()) { + ActivityUtils.startActivity(MainActivity.class); + } else { + ActivityUtils.startActivity(LoginActivity.class); + } + finish(); + } + + /** + * 菜单、返回键响应 + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliRecyclerAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliRecyclerAdapter.java new file mode 100644 index 00000000..9a8df840 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliRecyclerAdapter.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.broccoli; + +import android.view.View; + +import androidx.annotation.NonNull; + +import com.xuexiang.xui.adapter.recyclerview.BaseRecyclerAdapter; +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; +import com.xuexiang.xui.adapter.recyclerview.XRecyclerAdapter; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import me.samlss.broccoli.Broccoli; + +/** + * 使用Broccoli占位的基础适配器 + * + * @author XUE + * @since 2019/4/8 16:33 + */ +public abstract class BroccoliRecyclerAdapter extends BaseRecyclerAdapter { + /** + * 是否已经加载成功 + */ + private boolean mHasLoad = false; + private Map mBroccoliMap = new HashMap<>(); + + public BroccoliRecyclerAdapter(Collection collection) { + super(collection); + } + + @Override + protected void bindData(@NonNull RecyclerViewHolder holder, int position, T item) { + Broccoli broccoli = mBroccoliMap.get(holder.itemView); + if (broccoli == null) { + broccoli = new Broccoli(); + mBroccoliMap.put(holder.itemView, broccoli); + } + if (mHasLoad) { + broccoli.removeAllPlaceholders(); + + onBindData(holder, item, position); + } else { + onBindBroccoli(holder, broccoli); + broccoli.show(); + } + } + + /** + * 绑定控件 + * + * @param holder + * @param model + * @param position + */ + protected abstract void onBindData(RecyclerViewHolder holder, T model, int position); + + /** + * 绑定占位控件 + * + * @param broccoli + */ + protected abstract void onBindBroccoli(RecyclerViewHolder holder, Broccoli broccoli); + + @Override + public XRecyclerAdapter refresh(Collection collection) { + mHasLoad = true; + return super.refresh(collection); + } + + /** + * 资源释放,防止内存泄漏 + */ + public void recycle() { + for (Broccoli broccoli : mBroccoliMap.values()) { + broccoli.removeAllPlaceholders(); + } + mBroccoliMap.clear(); + clear(); + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliSimpleDelegateAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliSimpleDelegateAdapter.java new file mode 100644 index 00000000..ed722913 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/broccoli/BroccoliSimpleDelegateAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.broccoli; + +import android.view.View; + +import androidx.annotation.NonNull; + +import com.alibaba.android.vlayout.LayoutHelper; +import com.kerwin.wumei.adapter.base.delegate.SimpleDelegateAdapter; +import com.kerwin.wumei.adapter.base.delegate.XDelegateAdapter; +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import me.samlss.broccoli.Broccoli; + +/** + * 使用Broccoli占位的基础适配器 + * + * @author xuexiang + * @since 2021/1/9 4:52 PM + */ +public abstract class BroccoliSimpleDelegateAdapter extends SimpleDelegateAdapter { + + /** + * 是否已经加载成功 + */ + private boolean mHasLoad = false; + private Map mBroccoliMap = new HashMap<>(); + + public BroccoliSimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper) { + super(layoutId, layoutHelper); + } + + public BroccoliSimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper, Collection list) { + super(layoutId, layoutHelper, list); + } + + public BroccoliSimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper, T[] data) { + super(layoutId, layoutHelper, data); + } + + @Override + protected void bindData(@NonNull RecyclerViewHolder holder, int position, T item) { + Broccoli broccoli = mBroccoliMap.get(holder.itemView); + if (broccoli == null) { + broccoli = new Broccoli(); + mBroccoliMap.put(holder.itemView, broccoli); + } + if (mHasLoad) { + broccoli.removeAllPlaceholders(); + + onBindData(holder, item, position); + } else { + onBindBroccoli(holder, broccoli); + broccoli.show(); + } + } + + + /** + * 绑定控件 + * + * @param holder + * @param model + * @param position + */ + protected abstract void onBindData(RecyclerViewHolder holder, T model, int position); + + /** + * 绑定占位控件 + * + * @param holder + * @param broccoli + */ + protected abstract void onBindBroccoli(RecyclerViewHolder holder, Broccoli broccoli); + + @Override + public XDelegateAdapter refresh(Collection collection) { + mHasLoad = true; + return super.refresh(collection); + } + + /** + * 资源释放,防止内存泄漏 + */ + public void recycle() { + for (Broccoli broccoli : mBroccoliMap.values()) { + broccoli.removeAllPlaceholders(); + } + mBroccoliMap.clear(); + clear(); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/BaseDelegateAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/BaseDelegateAdapter.java new file mode 100644 index 00000000..52c8fcda --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/BaseDelegateAdapter.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.delegate; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; + +import java.util.Collection; + +/** + * 通用的DelegateAdapter适配器 + * + * @author xuexiang + * @since 2020/3/20 12:44 AM + */ +public abstract class BaseDelegateAdapter extends XDelegateAdapter { + + public BaseDelegateAdapter() { + super(); + } + + public BaseDelegateAdapter(Collection list) { + super(list); + } + + public BaseDelegateAdapter(T[] data) { + super(data); + } + + /** + * 适配的布局 + * + * @param viewType + * @return + */ + protected abstract int getItemLayoutId(int viewType); + + @NonNull + @Override + protected RecyclerViewHolder getViewHolder(@NonNull ViewGroup parent, int viewType) { + return new RecyclerViewHolder(inflateView(parent, getItemLayoutId(viewType))); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SimpleDelegateAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SimpleDelegateAdapter.java new file mode 100644 index 00000000..f0f280b0 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SimpleDelegateAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.delegate; + +import com.alibaba.android.vlayout.LayoutHelper; + +import java.util.Collection; + +/** + * 简易DelegateAdapter适配器 + * + * @author xuexiang + * @since 2020/3/20 12:55 AM + */ +public abstract class SimpleDelegateAdapter extends BaseDelegateAdapter { + + private int mLayoutId; + + private LayoutHelper mLayoutHelper; + + public SimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper) { + super(); + mLayoutId = layoutId; + mLayoutHelper = layoutHelper; + } + + public SimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper, Collection list) { + super(list); + mLayoutId = layoutId; + mLayoutHelper = layoutHelper; + } + + public SimpleDelegateAdapter(int layoutId, LayoutHelper layoutHelper, T[] data) { + super(data); + mLayoutId = layoutId; + mLayoutHelper = layoutHelper; + } + + @Override + protected int getItemLayoutId(int viewType) { + return mLayoutId; + } + + + @Override + public LayoutHelper onCreateLayoutHelper() { + return mLayoutHelper; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SingleDelegateAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SingleDelegateAdapter.java new file mode 100644 index 00000000..46b7329b --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/SingleDelegateAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.delegate; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; + +import com.alibaba.android.vlayout.DelegateAdapter; +import com.alibaba.android.vlayout.LayoutHelper; +import com.alibaba.android.vlayout.layout.SingleLayoutHelper; +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; + +/** + * 单独布局的DelegateAdapter + * + * @author xuexiang + * @since 2020/3/20 1:04 AM + */ +public abstract class SingleDelegateAdapter extends DelegateAdapter.Adapter { + + private int mLayoutId; + + public SingleDelegateAdapter(int layoutId) { + mLayoutId = layoutId; + } + + @Override + public LayoutHelper onCreateLayoutHelper() { + return new SingleLayoutHelper(); + } + + /** + * 加载布局获取控件 + * + * @param parent 父布局 + * @param layoutId 布局ID + * @return + */ + protected View inflateView(ViewGroup parent, @LayoutRes int layoutId) { + return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); + } + + @NonNull + @Override + public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new RecyclerViewHolder(inflateView(parent, mLayoutId)); + } + + @Override + public int getItemCount() { + return 1; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/XDelegateAdapter.java b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/XDelegateAdapter.java new file mode 100644 index 00000000..86881382 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/base/delegate/XDelegateAdapter.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.base.delegate; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alibaba.android.vlayout.DelegateAdapter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * 基础DelegateAdapter + * + * @author xuexiang + * @since 2020/3/20 12:17 AM + */ +public abstract class XDelegateAdapter extends DelegateAdapter.Adapter { + /** + * 数据源 + */ + protected final List mData = new ArrayList<>(); + /** + * 当前点击的条目 + */ + protected int mSelectPosition = -1; + + public XDelegateAdapter() { + + } + + public XDelegateAdapter(Collection list) { + if (list != null) { + mData.addAll(list); + } + } + + public XDelegateAdapter(T[] data) { + if (data != null && data.length > 0) { + mData.addAll(Arrays.asList(data)); + } + } + + /** + * 构建自定义的ViewHolder + * + * @param parent + * @param viewType + * @return + */ + @NonNull + protected abstract V getViewHolder(@NonNull ViewGroup parent, int viewType); + + /** + * 绑定数据 + * + * @param holder + * @param position 索引 + * @param item 列表项 + */ + protected abstract void bindData(@NonNull V holder, int position, T item); + + /** + * 加载布局获取控件 + * + * @param parent 父布局 + * @param layoutId 布局ID + * @return + */ + protected View inflateView(ViewGroup parent, @LayoutRes int layoutId) { + return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); + } + + @NonNull + @Override + public V onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return getViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(@NonNull V holder, int position) { + bindData(holder, position, mData.get(position)); + } + + /** + * 获取列表项 + * + * @param position + * @return + */ + public T getItem(int position) { + return checkPosition(position) ? mData.get(position) : null; + } + + private boolean checkPosition(int position) { + return position >= 0 && position <= mData.size() - 1; + } + + public boolean isEmpty() { + return getItemCount() == 0; + } + + @Override + public int getItemCount() { + return mData.size(); + } + + /** + * @return 数据源 + */ + public List getData() { + return mData; + } + + /** + * 给指定位置添加一项 + * + * @param pos + * @param item + * @return + */ + public XDelegateAdapter add(int pos, T item) { + mData.add(pos, item); + notifyItemInserted(pos); + return this; + } + + /** + * 在列表末端增加一项 + * + * @param item + * @return + */ + public XDelegateAdapter add(T item) { + mData.add(item); + notifyItemInserted(mData.size() - 1); + return this; + } + + /** + * 删除列表中指定索引的数据 + * + * @param pos + * @return + */ + public XDelegateAdapter delete(int pos) { + mData.remove(pos); + notifyItemRemoved(pos); + return this; + } + + /** + * 刷新列表中指定位置的数据 + * + * @param pos + * @param item + * @return + */ + public XDelegateAdapter refresh(int pos, T item) { + mData.set(pos, item); + notifyItemChanged(pos); + return this; + } + + /** + * 刷新列表数据 + * + * @param collection + * @return + */ + public XDelegateAdapter refresh(Collection collection) { + if (collection != null) { + mData.clear(); + mData.addAll(collection); + mSelectPosition = -1; + notifyDataSetChanged(); + } + return this; + } + + /** + * 刷新列表数据 + * + * @param array + * @return + */ + public XDelegateAdapter refresh(T[] array) { + if (array != null && array.length > 0) { + mData.clear(); + mData.addAll(Arrays.asList(array)); + mSelectPosition = -1; + notifyDataSetChanged(); + } + return this; + } + + /** + * 加载更多 + * + * @param collection + * @return + */ + public XDelegateAdapter loadMore(Collection collection) { + if (collection != null) { + mData.addAll(collection); + notifyDataSetChanged(); + } + return this; + } + + /** + * 加载更多 + * + * @param array + * @return + */ + public XDelegateAdapter loadMore(T[] array) { + if (array != null && array.length > 0) { + mData.addAll(Arrays.asList(array)); + notifyDataSetChanged(); + } + return this; + } + + /** + * 添加一个 + * + * @param item + * @return + */ + public XDelegateAdapter load(T item) { + if (item != null) { + mData.add(item); + notifyDataSetChanged(); + } + return this; + } + + /** + * @return 当前列表的选中项 + */ + public int getSelectPosition() { + return mSelectPosition; + } + + /** + * 设置当前列表的选中项 + * + * @param selectPosition + * @return + */ + public XDelegateAdapter setSelectPosition(int selectPosition) { + mSelectPosition = selectPosition; + notifyDataSetChanged(); + return this; + } + + /** + * 获取当前列表选中项 + * + * @return 当前列表选中项 + */ + public T getSelectItem() { + return getItem(mSelectPosition); + } + + /** + * 清除数据 + */ + public void clear() { + if (!isEmpty()) { + mData.clear(); + mSelectPosition = -1; + notifyDataSetChanged(); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/entity/EspTouchViewModel.java b/android/app/src/main/java/com/kerwin/wumei/adapter/entity/EspTouchViewModel.java new file mode 100644 index 00000000..fb4a2429 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/entity/EspTouchViewModel.java @@ -0,0 +1,31 @@ +package com.kerwin.wumei.adapter.entity; + +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.TextView; + +import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner; + +public class EspTouchViewModel { + public MaterialSpinner ssidSpinner; + public EditText apPasswordEdit; + public EditText deviceCountEdit; + public RadioGroup packageModeGroup; + public TextView messageView; + public Button confirmBtn; + + public String ssid; + public byte[] ssidBytes; + public String bssid; + + public CharSequence message; + + public boolean confirmEnable; + + public void invalidateAll() { + ssidSpinner.setText(ssid); + messageView.setText(message); + confirmBtn.setEnabled(confirmEnable); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/kerwin/wumei/adapter/entity/NewInfo.java b/android/app/src/main/java/com/kerwin/wumei/adapter/entity/NewInfo.java new file mode 100644 index 00000000..165ed133 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/adapter/entity/NewInfo.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.adapter.entity; + +/** + * 新闻信息 + * + * @author xuexiang + * @since 2019/4/7 下午12:07 + */ +public class NewInfo { + + /** + * 用户名 + */ + private String UserName = "kerwin"; + /** + * 标签 + */ + private String Tag; + /** + * 标题 + */ + private String Title; + /** + * 摘要 + */ + private String Summary; + + /** + * 图片 + */ + private String ImageUrl; + /** + * 点赞数 + */ + private int Praise; + /** + * 评论数 + */ + private int Comment; + /** + * 阅读量 + */ + private int Read; + /** + * 新闻的详情地址 + */ + private String DetailUrl; + + + public NewInfo() { + + } + + public NewInfo(String userName, String tag, String title, String summary, String imageUrl, int praise, int comment, int read, String detailUrl) { + UserName = userName; + Tag = tag; + Title = title; + Summary = summary; + ImageUrl = imageUrl; + Praise = praise; + Comment = comment; + Read = read; + DetailUrl = detailUrl; + } + + + public NewInfo(String tag, String title, String summary, String imageUrl, String detailUrl) { + Tag = tag; + Title = title; + Summary = summary; + ImageUrl = imageUrl; + DetailUrl = detailUrl; + } + + + public NewInfo(String tag, String title) { + Tag = tag; + Title = title; + + Praise = (int) (Math.random() * 100 + 5); + Comment = (int) (Math.random() * 50 + 5); + Read = (int) (Math.random() * 500 + 50); + } + + + + public String getUserName() { + return UserName; + } + + public NewInfo setUserName(String userName) { + UserName = userName; + return this; + } + + public String getTag() { + return Tag; + } + + public NewInfo setTag(String tag) { + Tag = tag; + return this; + } + + public String getTitle() { + return Title; + } + + public NewInfo setTitle(String title) { + Title = title; + return this; + } + + public String getSummary() { + return Summary; + } + + public NewInfo setSummary(String summary) { + Summary = summary; + return this; + } + + public String getImageUrl() { + return ImageUrl; + } + + public NewInfo setImageUrl(String imageUrl) { + ImageUrl = imageUrl; + return this; + } + + public int getPraise() { + return Praise; + } + + public NewInfo setPraise(int praise) { + Praise = praise; + return this; + } + + public int getComment() { + return Comment; + } + + public NewInfo setComment(int comment) { + Comment = comment; + return this; + } + + public int getRead() { + return Read; + } + + public NewInfo setRead(int read) { + Read = read; + return this; + } + + public String getDetailUrl() { + return DetailUrl; + } + + public NewInfo setDetailUrl(String detailUrl) { + DetailUrl = detailUrl; + return this; + } + + @Override + public String toString() { + return "NewInfo{" + + "UserName='" + UserName + '\'' + + ", Tag='" + Tag + '\'' + + ", Title='" + Title + '\'' + + ", Summary='" + Summary + '\'' + + ", ImageUrl='" + ImageUrl + '\'' + + ", Praise=" + Praise + + ", Comment=" + Comment + + ", Read=" + Read + + ", DetailUrl='" + DetailUrl + '\'' + + '}'; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/BaseActivity.java b/android/app/src/main/java/com/kerwin/wumei/core/BaseActivity.java new file mode 100644 index 00000000..b6529fdd --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/BaseActivity.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.content.Context; +import android.os.Bundle; + +import com.xuexiang.xpage.base.XPageActivity; +import com.xuexiang.xpage.base.XPageFragment; +import com.xuexiang.xpage.core.CoreSwitchBean; +import com.xuexiang.xrouter.facade.service.SerializationService; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.widget.slideback.SlideBack; + +import butterknife.ButterKnife; +import butterknife.Unbinder; +import io.github.inflationx.viewpump.ViewPumpContextWrapper; + +/** + * 基础容器Activity + * + * @author XUE + * @since 2019/3/22 11:21 + */ +public class BaseActivity extends XPageActivity { + + Unbinder mUnbinder; + + @Override + protected void attachBaseContext(Context newBase) { + //注入字体 + super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)); + } + + /** + * 是否支持侧滑返回 + */ + public static final String KEY_SUPPORT_SLIDE_BACK = "key_support_slide_back"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + initStatusBarStyle(); + super.onCreate(savedInstanceState); + mUnbinder = ButterKnife.bind(this); + + registerSlideBack(); + } + + /** + * 初始化状态栏的样式 + */ + protected void initStatusBarStyle() { + + } + + /** + * 打开fragment + * + * @param clazz 页面类 + * @param addToBackStack 是否添加到栈中 + * @return 打开的fragment对象 + */ + public T openPage(Class clazz, boolean addToBackStack) { + CoreSwitchBean page = new CoreSwitchBean(clazz) + .setAddToBackStack(addToBackStack); + return (T) openPage(page); + } + + /** + * 打开fragment + * + * @return 打开的fragment对象 + */ + public T openNewPage(Class clazz) { + CoreSwitchBean page = new CoreSwitchBean(clazz) + .setNewActivity(true); + return (T) openPage(page); + } + + /** + * 切换fragment + * + * @param clazz 页面类 + * @return 打开的fragment对象 + */ + public T switchPage(Class clazz) { + return openPage(clazz, false); + } + + /** + * 序列化对象 + * + * @param object + * @return + */ + public String serializeObject(Object object) { + return XRouter.getInstance().navigation(SerializationService.class).object2Json(object); + } + + @Override + protected void onRelease() { + mUnbinder.unbind(); + unregisterSlideBack(); + super.onRelease(); + } + + /** + * 注册侧滑回调 + */ + protected void registerSlideBack() { + if (isSupportSlideBack()) { + SlideBack.with(this) + .haveScroll(true) + .edgeMode(ResUtils.isRtl() ? SlideBack.EDGE_RIGHT : SlideBack.EDGE_LEFT) + .callBack(this::popPage) + .register(); + } + } + + /** + * 注销侧滑回调 + */ + protected void unregisterSlideBack() { + if (isSupportSlideBack()) { + SlideBack.unregister(this); + } + } + + /** + * @return 是否支持侧滑返回 + */ + protected boolean isSupportSlideBack() { + CoreSwitchBean page = getIntent().getParcelableExtra(CoreSwitchBean.KEY_SWITCH_BEAN); + return page == null || page.getBundle() == null || page.getBundle().getBoolean(KEY_SUPPORT_SLIDE_BACK, true); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/BaseContainerFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/BaseContainerFragment.java new file mode 100644 index 00000000..29987cc1 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/BaseContainerFragment.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.content.res.Configuration; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; + +import com.umeng.analytics.MobclickAgent; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.base.XPageContainerListFragment; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.actionbar.TitleUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.kerwin.wumei.core.SimpleListAdapter.KEY_SUB_TITLE; +import static com.kerwin.wumei.core.SimpleListAdapter.KEY_TITLE; + +/** + * 修改列表样式为主副标题显示 + * + * @author xuexiang + * @since 2018/11/22 上午11:26 + */ +public abstract class BaseContainerFragment extends XPageContainerListFragment { + + @Override + protected void initPage() { + initTitle(); + initViews(); + initListeners(); + } + + protected TitleBar initTitle() { + return TitleUtils.addTitleBarDynamic((ViewGroup) getRootView(), getPageTitle(), new View.OnClickListener() { + @Override + public void onClick(View v) { + popToBack(); + } + }); + } + + @Override + protected void initData() { + mSimpleData = initSimpleData(mSimpleData); + + List> data = new ArrayList<>(); + for (String content : mSimpleData) { + Map item = new HashMap<>(); + int index = content.indexOf("\n"); + if (index > 0) { + item.put(KEY_TITLE, String.valueOf(content.subSequence(0, index))); + item.put(KEY_SUB_TITLE, String.valueOf(content.subSequence(index + 1, content.length()))); + } else { + item.put(KEY_TITLE, content); + item.put(KEY_SUB_TITLE, ""); + } + data.add(item); + } + + getListView().setAdapter(new SimpleListAdapter(getContext(), data)); + initSimply(); + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + onItemClick(view, position); + } + + @SingleClick + private void onItemClick(View view, int position) { + onItemClick(position); + } + + @Override + public void onDestroyView() { + getListView().setOnItemClickListener(null); + super.onDestroyView(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + //屏幕旋转时刷新一下title + super.onConfigurationChanged(newConfig); + ViewGroup root = (ViewGroup) getRootView(); + if (root.getChildAt(0) instanceof TitleBar) { + root.removeViewAt(0); + initTitle(); + } + } + + @Override + public void onResume() { + super.onResume(); + MobclickAgent.onPageStart(getPageName()); + } + + @Override + public void onPause() { + super.onPause(); + MobclickAgent.onPageEnd(getPageName()); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/BaseFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/BaseFragment.java new file mode 100644 index 00000000..5441c502 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/BaseFragment.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.content.res.Configuration; +import android.os.Parcelable; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.umeng.analytics.MobclickAgent; +import com.kerwin.wumei.core.http.loader.ProgressLoader; +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; +import com.xuexiang.xpage.base.XPageActivity; +import com.xuexiang.xpage.base.XPageFragment; +import com.xuexiang.xpage.core.PageOption; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xpage.utils.Utils; +import com.xuexiang.xrouter.facade.service.SerializationService; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.actionbar.TitleUtils; + +import java.io.Serializable; +import java.lang.reflect.Type; + +/** + * 基础fragment + * + * @author xuexiang + * @since 2018/5/25 下午3:44 + */ +public abstract class BaseFragment extends XPageFragment { + + private IProgressLoader mIProgressLoader; + + @Override + protected void initPage() { + initTitle(); + initViews(); + initListeners(); + } + + protected TitleBar initTitle() { + return TitleUtils.addTitleBarDynamic((ViewGroup) getRootView(), getPageTitle(), v -> popToBack()); + } + + @Override + protected void initListeners() { + + } + + /** + * 获取进度条加载者 + * + * @return 进度条加载者 + */ + public IProgressLoader getProgressLoader() { + if (mIProgressLoader == null) { + mIProgressLoader = ProgressLoader.create(getContext()); + } + return mIProgressLoader; + } + + /** + * 获取进度条加载者 + * + * @param message + * @return 进度条加载者 + */ + public IProgressLoader getProgressLoader(String message) { + if (mIProgressLoader == null) { + mIProgressLoader = ProgressLoader.create(getContext(), message); + } else { + mIProgressLoader.updateMessage(message); + } + return mIProgressLoader; + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + //屏幕旋转时刷新一下title + super.onConfigurationChanged(newConfig); + ViewGroup root = (ViewGroup) getRootView(); + if (root.getChildAt(0) instanceof TitleBar) { + root.removeViewAt(0); + initTitle(); + } + } + + @Override + public void onDestroyView() { + if (mIProgressLoader != null) { + mIProgressLoader.dismissLoading(); + } + super.onDestroyView(); + } + + @Override + public void onResume() { + super.onResume(); + MobclickAgent.onPageStart(getPageName()); + } + + @Override + public void onPause() { + super.onPause(); + MobclickAgent.onPageEnd(getPageName()); + } + + //==============================页面跳转api===================================// + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param + * @return + */ + public Fragment openNewPage(Class clazz) { + return new PageOption(clazz) + .setNewActivity(true) + .open(this); + } + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param pageName 页面名 + * @param + * @return + */ + public Fragment openNewPage(String pageName) { + return new PageOption(pageName) + .setAnim(CoreAnim.slide) + .setNewActivity(true) + .open(this); + } + + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param containActivityClazz 页面容器 + * @param + * @return + */ + public Fragment openNewPage(Class clazz, @NonNull Class containActivityClazz) { + return new PageOption(clazz) + .setNewActivity(true) + .setContainActivityClazz(containActivityClazz) + .open(this); + } + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openNewPage(Class clazz, String key, Object value) { + PageOption option = new PageOption(clazz).setNewActivity(true); + return openPage(option, key, value); + } + + public Fragment openPage(PageOption option, String key, Object value) { + if (value instanceof Integer) { + option.putInt(key, (Integer) value); + } else if (value instanceof Float) { + option.putFloat(key, (Float) value); + } else if (value instanceof String) { + option.putString(key, (String) value); + } else if (value instanceof Boolean) { + option.putBoolean(key, (Boolean) value); + } else if (value instanceof Long) { + option.putLong(key, (Long) value); + } else if (value instanceof Double) { + option.putDouble(key, (Double) value); + } else if (value instanceof Parcelable) { + option.putParcelable(key, (Parcelable) value); + } else if (value instanceof Serializable) { + option.putSerializable(key, (Serializable) value); + } else { + option.putString(key, serializeObject(value)); + } + return option.open(this); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param addToBackStack 是否加入回退栈 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, boolean addToBackStack, String key, String value) { + return new PageOption(clazz) + .setAddToBackStack(addToBackStack) + .putString(key, value) + .open(this); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, String key, Object value) { + return openPage(clazz, true, key, value); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param addToBackStack 是否加入回退栈 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, boolean addToBackStack, String key, Object value) { + PageOption option = new PageOption(clazz).setAddToBackStack(addToBackStack); + return openPage(option, key, value); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, String key, String value) { + return new PageOption(clazz) + .putString(key, value) + .open(this); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, String key, Object value, int requestCode) { + PageOption option = new PageOption(clazz).setRequestCode(requestCode); + return openPage(option, key, value); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, String key, String value, int requestCode) { + return new PageOption(clazz) + .setRequestCode(requestCode) + .putString(key, value) + .open(this); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, int requestCode) { + return new PageOption(clazz) + .setRequestCode(requestCode) + .open(this); + } + + /** + * 序列化对象 + * + * @param object 需要序列化的对象 + * @return 序列化结果 + */ + public String serializeObject(Object object) { + return XRouter.getInstance().navigation(SerializationService.class).object2Json(object); + } + + /** + * 反序列化对象 + * + * @param input 反序列化的内容 + * @param clazz 类型 + * @return 反序列化结果 + */ + public T deserializeObject(String input, Type clazz) { + return XRouter.getInstance().navigation(SerializationService.class).parseObject(input, clazz); + } + + + @Override + protected void hideCurrentPageSoftInput() { + if (getActivity() == null) { + return; + } + // 记住,要在xml的父布局加上android:focusable="true" 和 android:focusableInTouchMode="true" + Utils.hideSoftInputClearFocus(getActivity().getCurrentFocus()); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/BaseSimpleListFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/BaseSimpleListFragment.java new file mode 100644 index 00000000..5474bb00 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/BaseSimpleListFragment.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.content.res.Configuration; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.umeng.analytics.MobclickAgent; +import com.xuexiang.xpage.base.XPageActivity; +import com.xuexiang.xpage.base.XPageFragment; +import com.xuexiang.xpage.base.XPageSimpleListFragment; +import com.xuexiang.xpage.core.PageOption; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xrouter.facade.service.SerializationService; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.actionbar.TitleUtils; + +import java.io.Serializable; + +/** + * @author xuexiang + * @since 2018/12/29 下午12:41 + */ +public abstract class BaseSimpleListFragment extends XPageSimpleListFragment { + + @Override + protected void initPage() { + initTitle(); + initViews(); + initListeners(); + } + + protected TitleBar initTitle() { + return TitleUtils.addTitleBarDynamic((ViewGroup) getRootView(), getPageTitle(), new View.OnClickListener() { + @Override + public void onClick(View v) { + popToBack(); + } + }); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + //屏幕旋转时刷新一下title + super.onConfigurationChanged(newConfig); + ViewGroup root = (ViewGroup) getRootView(); + if (root.getChildAt(0) instanceof TitleBar) { + root.removeViewAt(0); + initTitle(); + } + } + + @Override + public void onResume() { + super.onResume(); + MobclickAgent.onPageStart(getPageName()); + } + + @Override + public void onPause() { + super.onPause(); + MobclickAgent.onPageEnd(getPageName()); + } + + //==============================页面跳转api===================================// + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param + * @return + */ + public Fragment openNewPage(Class clazz) { + return new PageOption(clazz) + .setNewActivity(true) + .open(this); + } + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param pageName 页面名 + * @param + * @return + */ + public Fragment openNewPage(String pageName) { + return new PageOption(pageName) + .setAnim(CoreAnim.slide) + .setNewActivity(true) + .open(this); + } + + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param containActivityClazz 页面容器 + * @param + * @return + */ + public Fragment openNewPage(Class clazz, @NonNull Class containActivityClazz) { + return new PageOption(clazz) + .setNewActivity(true) + .setContainActivityClazz(containActivityClazz) + .open(this); + } + + /** + * 打开一个新的页面【建议只在主tab页使用】 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openNewPage(Class clazz, String key, Object value) { + PageOption option = new PageOption(clazz).setNewActivity(true); + return openPage(option, key, value); + } + + public Fragment openPage(PageOption option, String key, Object value) { + if (value instanceof Integer) { + option.putInt(key, (Integer) value); + } else if (value instanceof Float) { + option.putFloat(key, (Float) value); + } else if (value instanceof String) { + option.putString(key, (String) value); + } else if (value instanceof Boolean) { + option.putBoolean(key, (Boolean) value); + } else if (value instanceof Long) { + option.putLong(key, (Long) value); + } else if (value instanceof Double) { + option.putDouble(key, (Double) value); + } else if (value instanceof Parcelable) { + option.putParcelable(key, (Parcelable) value); + } else if (value instanceof Serializable) { + option.putSerializable(key, (Serializable) value); + } else { + option.putString(key, serializeObject(value)); + } + return option.open(this); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param addToBackStack 是否加入回退栈 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, boolean addToBackStack, String key, String value) { + return new PageOption(clazz) + .setAddToBackStack(addToBackStack) + .putString(key, value) + .open(this); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, String key, Object value) { + return openPage(clazz, true, key, value); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param addToBackStack 是否加入回退栈 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, boolean addToBackStack, String key, Object value) { + PageOption option = new PageOption(clazz).setAddToBackStack(addToBackStack); + return openPage(option, key, value); + } + + /** + * 打开页面 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param + * @return + */ + public Fragment openPage(Class clazz, String key, String value) { + return new PageOption(clazz) + .putString(key, value) + .open(this); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, String key, Object value, int requestCode) { + PageOption option = new PageOption(clazz).setRequestCode(requestCode); + return openPage(option, key, value); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param key 入参的键 + * @param value 入参的值 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, String key, String value, int requestCode) { + return new PageOption(clazz) + .setRequestCode(requestCode) + .putString(key, value) + .open(this); + } + + /** + * 打开页面,需要结果返回 + * + * @param clazz 页面的类 + * @param requestCode 请求码 + * @param + * @return + */ + public Fragment openPageForResult(Class clazz, int requestCode) { + return new PageOption(clazz) + .setRequestCode(requestCode) + .open(this); + } + + /** + * 序列化对象 + * + * @param object 需要序列化的对象 + * @return 序列化结果 + */ + public String serializeObject(Object object) { + return XRouter.getInstance().navigation(SerializationService.class).object2Json(object); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/SimpleListAdapter.java b/android/app/src/main/java/com/kerwin/wumei/core/SimpleListAdapter.java new file mode 100644 index 00000000..c8d43cee --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/SimpleListAdapter.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import com.kerwin.wumei.R; +import com.xuexiang.xui.adapter.listview.BaseListAdapter; +import com.xuexiang.xutil.common.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * 主副标题显示适配器 + * + * @author xuexiang + * @since 2018/12/19 上午12:19 + */ +public class SimpleListAdapter extends BaseListAdapter, SimpleListAdapter.ViewHolder> { + + public static final String KEY_TITLE = "key_title"; + public static final String KEY_SUB_TITLE = "key_sub_title"; + + public SimpleListAdapter(Context context, List> data) { + super(context, data); + } + + @Override + protected ViewHolder newViewHolder(View convertView) { + ViewHolder holder = new ViewHolder(); + holder.mTvTitle = convertView.findViewById(R.id.device_item_title); + holder.mTvSubTitle = convertView.findViewById(R.id.tv_sub_title); + return holder; + } + + @Override + protected int getLayoutId() { + return R.layout.adapter_item_simple_list_2; + } + + @Override + protected void convert(ViewHolder holder, Map item, int position) { + holder.mTvTitle.setText(item.get(KEY_TITLE)); + if (!StringUtils.isEmpty(item.get(KEY_SUB_TITLE))) { + holder.mTvSubTitle.setText(item.get(KEY_SUB_TITLE)); + holder.mTvSubTitle.setVisibility(View.VISIBLE); + } else { + holder.mTvSubTitle.setVisibility(View.GONE); + } + } + + public static class ViewHolder { + /** + * 标题 + */ + public TextView mTvTitle; + /** + * 副标题 + */ + public TextView mTvSubTitle; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/XPageTransferActivity.java b/android/app/src/main/java/com/kerwin/wumei/core/XPageTransferActivity.java new file mode 100644 index 00000000..b0343ccf --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/XPageTransferActivity.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core; + +import android.os.Bundle; + +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xrouter.annotation.AutoWired; +import com.xuexiang.xrouter.annotation.Router; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xutil.common.StringUtils; + +/** + * https://xuexiangjys.club/xpage/transfer?pageName=xxxxx&.... + * applink的中转 + * + * @author xuexiang + * @since 2019-07-06 9:37 + */ +@Router(path = "/xpage/transfer") +public class XPageTransferActivity extends BaseActivity { + + @AutoWired(name = "pageName") + String pageName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + XRouter.getInstance().inject(this); + + if (!StringUtils.isEmpty(pageName)) { + if (openPage(pageName, getIntent().getExtras()) == null) { + XToastUtils.error("页面未找到!"); + finish(); + } + } else { + XToastUtils.error("页面未找到!"); + finish(); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/api/ApiService.java b/android/app/src/main/java/com/kerwin/wumei/core/http/api/ApiService.java new file mode 100644 index 00000000..efda7d0a --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/api/ApiService.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.api; + +import com.kerwin.wumei.core.http.entity.TipInfo; +import com.xuexiang.xhttp2.model.ApiResult; + +import java.util.List; + +import io.reactivex.Observable; +import retrofit2.http.GET; + +/** + * @author xuexiang + * @since 2021/1/9 7:01 PM + */ +public class ApiService { + + /** + * 使用的是retrofit的接口定义 + */ + public interface IGetService { + + /** + * 获得小贴士 + */ + @GET("/xuexiangjys/Resource/raw/master/jsonapi/tips.json") + Observable>> getTips(); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/callback/NoTipCallBack.java b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/NoTipCallBack.java new file mode 100644 index 00000000..ceaa7fbb --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/NoTipCallBack.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.callback; + +import com.xuexiang.xhttp2.callback.SimpleCallBack; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 不带错误提示的网络请求回调 + * + * @author xuexiang + * @since 2019-11-18 23:02 + */ +public abstract class NoTipCallBack extends SimpleCallBack { + + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public NoTipCallBack() { + + } + + public NoTipCallBack(XHttpRequest req) { + this(req.getUrl()); + } + + public NoTipCallBack(String url) { + mUrl = url; + } + + @Override + public void onError(ApiException e) { + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipCallBack.java b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipCallBack.java new file mode 100644 index 00000000..10c4b1c2 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipCallBack.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.callback; + +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xhttp2.callback.SimpleCallBack; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 带错误toast提示的网络请求回调 + * + * @author xuexiang + * @since 2019-11-18 23:02 + */ +public abstract class TipCallBack extends SimpleCallBack { + + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public TipCallBack() { + + } + + public TipCallBack(XHttpRequest req) { + this(req.getUrl()); + } + + public TipCallBack(String url) { + mUrl = url; + } + + @Override + public void onError(ApiException e) { + XToastUtils.error(e); + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipProgressLoadingCallBack.java b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipProgressLoadingCallBack.java new file mode 100644 index 00000000..9fb26d4b --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/callback/TipProgressLoadingCallBack.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.callback; + +import androidx.annotation.NonNull; + +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xhttp2.callback.ProgressLoadingCallBack; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 带错误toast提示和加载进度条的网络请求回调 + * + * @author xuexiang + * @since 2019-11-18 23:16 + */ +public abstract class TipProgressLoadingCallBack extends ProgressLoadingCallBack { + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public TipProgressLoadingCallBack(BaseFragment fragment) { + super(fragment.getProgressLoader()); + } + + public TipProgressLoadingCallBack(IProgressLoader iProgressLoader) { + super(iProgressLoader); + } + + public TipProgressLoadingCallBack(@NonNull XHttpRequest req, IProgressLoader iProgressLoader) { + this(req.getUrl(), iProgressLoader); + } + + public TipProgressLoadingCallBack(String url, IProgressLoader iProgressLoader) { + super(iProgressLoader); + mUrl = url; + } + + @Override + public void onError(ApiException e) { + super.onError(e); + XToastUtils.error(e); + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/entity/TipInfo.java b/android/app/src/main/java/com/kerwin/wumei/core/http/entity/TipInfo.java new file mode 100644 index 00000000..2aad9964 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/entity/TipInfo.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.entity; + +import androidx.annotation.Keep; + +/** + * @author xuexiang + * @since 2019-08-28 15:35 + */ +@Keep +public class TipInfo { + + /** + * title : 小贴士3 + * content :

欢迎关注我的微信公众号:我的Android开源之旅。


+ */ + + private String title; + private String content; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + @Override + public String toString() { + return "TipInfo{" + + "title='" + title + '\'' + + ", content='" + content + '\'' + + '}'; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/loader/IProgressLoaderFactory.java b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/IProgressLoaderFactory.java new file mode 100644 index 00000000..0e5cd780 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/IProgressLoaderFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.loader; + +import android.content.Context; + +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; + +/** + * IProgressLoader的创建工厂实现接口 + * + * @author xuexiang + * @since 2019-11-18 23:17 + */ +public interface IProgressLoaderFactory { + + + /** + * 创建进度加载者 + * + * @param context + * @return + */ + IProgressLoader create(Context context); + + + /** + * 创建进度加载者 + * + * @param context + * @param message 默认提示 + * @return + */ + IProgressLoader create(Context context, String message); +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniLoadingDialogLoader.java b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniLoadingDialogLoader.java new file mode 100644 index 00000000..bb998aec --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniLoadingDialogLoader.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.loader; + +import android.content.Context; +import android.content.DialogInterface; + +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; +import com.xuexiang.xhttp2.subsciber.impl.OnProgressCancelListener; +import com.xuexiang.xui.widget.dialog.MiniLoadingDialog; + +/** + * 默认进度加载 + * + * @author xuexiang + * @since 2019-11-18 23:07 + */ +public class MiniLoadingDialogLoader implements IProgressLoader { + /** + * 进度loading弹窗 + */ + private MiniLoadingDialog mDialog; + /** + * 进度框取消监听 + */ + private OnProgressCancelListener mOnProgressCancelListener; + + public MiniLoadingDialogLoader(Context context) { + this(context, "请求中..."); + } + + public MiniLoadingDialogLoader(Context context, String msg) { + mDialog = new MiniLoadingDialog(context, msg); + } + + @Override + public boolean isLoading() { + return mDialog != null && mDialog.isShowing(); + } + + @Override + public void updateMessage(String msg) { + if (mDialog != null) { + mDialog.updateMessage(msg); + } + } + + @Override + public void showLoading() { + if (mDialog != null && !mDialog.isShowing()) { + mDialog.show(); + } + } + + @Override + public void dismissLoading() { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + @Override + public void setCancelable(boolean flag) { + mDialog.setCancelable(flag); + if (flag) { + mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + if (mOnProgressCancelListener != null) { + mOnProgressCancelListener.onCancelProgress(); + } + } + }); + } + } + + @Override + public void setOnProgressCancelListener(OnProgressCancelListener listener) { + mOnProgressCancelListener = listener; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniProgressLoaderFactory.java b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniProgressLoaderFactory.java new file mode 100644 index 00000000..4694a5de --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/MiniProgressLoaderFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.loader; + +import android.content.Context; + +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; + +/** + * 迷你加载框创建工厂 + * + * @author xuexiang + * @since 2019-11-18 23:23 + */ +public class MiniProgressLoaderFactory implements IProgressLoaderFactory { + + @Override + public IProgressLoader create(Context context) { + return new MiniLoadingDialogLoader(context); + } + + @Override + public IProgressLoader create(Context context, String message) { + return new MiniLoadingDialogLoader(context, message); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/loader/ProgressLoader.java b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/ProgressLoader.java new file mode 100644 index 00000000..da71eff6 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/loader/ProgressLoader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.loader; + +import android.content.Context; + +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; + +/** + * 创建进度加载者 + * + * @author xuexiang + * @since 2019-07-02 12:51 + */ +public final class ProgressLoader { + + private ProgressLoader() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static IProgressLoaderFactory sIProgressLoaderFactory = new MiniProgressLoaderFactory(); + + public static void setIProgressLoaderFactory(IProgressLoaderFactory sIProgressLoaderFactory) { + ProgressLoader.sIProgressLoaderFactory = sIProgressLoaderFactory; + } + + /** + * 创建进度加载者 + * + * @param context + * @return + */ + public static IProgressLoader create(Context context) { + return sIProgressLoaderFactory.create(context); + } + + /** + * 创建进度加载者 + * + * @param context + * @param message 默认提示信息 + * @return + */ + public static IProgressLoader create(Context context, String message) { + return sIProgressLoaderFactory.create(context, message); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/NoTipRequestSubscriber.java b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/NoTipRequestSubscriber.java new file mode 100644 index 00000000..114a4f78 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/NoTipRequestSubscriber.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.subscriber; + +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xhttp2.subsciber.BaseSubscriber; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 不带错误toast提示的网络请求订阅,只存储错误的日志 + * + * @author xuexiang + * @since 2019-11-18 23:11 + */ +public abstract class NoTipRequestSubscriber extends BaseSubscriber { + + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public NoTipRequestSubscriber() { + + } + + public NoTipRequestSubscriber(XHttpRequest req) { + this(req.getUrl()); + } + + public NoTipRequestSubscriber(String url) { + mUrl = url; + } + + @Override + public void onError(ApiException e) { + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipProgressLoadingSubscriber.java b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipProgressLoadingSubscriber.java new file mode 100644 index 00000000..ac05336b --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipProgressLoadingSubscriber.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.subscriber; + +import androidx.annotation.NonNull; + +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xhttp2.subsciber.ProgressLoadingSubscriber; +import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 带错误toast提示和加载进度条的网络请求订阅 + * + * @author xuexiang + * @since 2019-11-18 23:11 + */ +public abstract class TipProgressLoadingSubscriber extends ProgressLoadingSubscriber { + + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public TipProgressLoadingSubscriber() { + super(); + } + + public TipProgressLoadingSubscriber(BaseFragment fragment) { + super(fragment.getProgressLoader()); + } + + public TipProgressLoadingSubscriber(IProgressLoader iProgressLoader) { + super(iProgressLoader); + } + + public TipProgressLoadingSubscriber(@NonNull XHttpRequest req) { + this(req.getUrl()); + } + + public TipProgressLoadingSubscriber(String url) { + super(); + mUrl = url; + } + + @Override + public void onError(ApiException e) { + super.onError(e); + XToastUtils.error(e); + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipRequestSubscriber.java b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipRequestSubscriber.java new file mode 100644 index 00000000..050896d3 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/http/subscriber/TipRequestSubscriber.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.http.subscriber; + + +import androidx.annotation.NonNull; + +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xhttp2.model.XHttpRequest; +import com.xuexiang.xhttp2.subsciber.BaseSubscriber; +import com.xuexiang.xutil.common.StringUtils; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * 带错误toast提示的网络请求订阅 + * + * @author xuexiang + * @since 2019-11-18 23:10 + */ +public abstract class TipRequestSubscriber extends BaseSubscriber { + /** + * 记录一下请求的url,确定出错的请求是哪个请求 + */ + private String mUrl; + + public TipRequestSubscriber() { + + } + + public TipRequestSubscriber(@NonNull XHttpRequest req) { + this(req.getUrl()); + } + + public TipRequestSubscriber(String url) { + mUrl = url; + } + + + @Override + public void onError(ApiException e) { + XToastUtils.error(e); + if (!StringUtils.isEmpty(mUrl)) { + Logger.e("网络请求的url:" + mUrl, e); + } else { + Logger.e(e); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebActivity.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebActivity.java new file mode 100644 index 00000000..df21148a --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebActivity.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.KeyEvent; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xrouter.facade.Postcard; +import com.xuexiang.xrouter.facade.callback.NavCallback; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.widget.slideback.SlideBack; + +/** + * 壳浏览器 + * + * @author xuexiang + * @since 2019/1/5 上午12:15 + */ +public class AgentWebActivity extends AppCompatActivity { + + /** + * 请求浏览器 + * + * @param url + */ + public static void goWeb(Context context, final String url) { + Intent intent = new Intent(context, AgentWebActivity.class); + intent.putExtra(AgentWebFragment.KEY_URL, url); + context.startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_agent_web); + + SlideBack.with(this) + .haveScroll(true) + .callBack(this::finish) + .register(); + + Uri uri = getIntent().getData(); + if (uri != null) { + XRouter.getInstance().build(uri).navigation(this, new NavCallback() { + @Override + public void onArrival(Postcard postcard) { + finish(); + } + + @Override + public void onLost(Postcard postcard) { + loadUrl(uri.toString()); + } + }); + } else { + String url = getIntent().getStringExtra(AgentWebFragment.KEY_URL); + loadUrl(url); + } + } + + private void loadUrl(String url) { + if (url != null) { + openFragment(url); + } else { + XToastUtils.error("数据出错!"); + finish(); + } + } + + private AgentWebFragment mAgentWebFragment; + + private void openFragment(String url) { + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + ft.add(R.id.container_frame_layout, mAgentWebFragment = AgentWebFragment.getInstance(url)); + ft.commit(); + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + AgentWebFragment agentWebFragment = mAgentWebFragment; + if (agentWebFragment != null) { + if (((FragmentKeyDown) agentWebFragment).onFragmentKeyDown(keyCode, event)) { + return true; + } else { + return super.onKeyDown(keyCode, event); + } + } + return super.onKeyDown(keyCode, event); + } + + + @Override + protected void onDestroy() { + SlideBack.unregister(this); + super.onDestroy(); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebFragment.java new file mode 100644 index 00000000..4459663a --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/AgentWebFragment.java @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.Fragment; + +import com.just.agentweb.action.PermissionInterceptor; +import com.just.agentweb.core.AgentWeb; +import com.just.agentweb.core.client.MiddlewareWebChromeBase; +import com.just.agentweb.core.client.MiddlewareWebClientBase; +import com.just.agentweb.core.client.WebListenerManager; +import com.just.agentweb.core.web.AbsAgentWebSettings; +import com.just.agentweb.core.web.AgentWebConfig; +import com.just.agentweb.core.web.IAgentWebSettings; +import com.just.agentweb.download.AgentWebDownloader; +import com.just.agentweb.download.DefaultDownloadImpl; +import com.just.agentweb.download.DownloadListenerAdapter; +import com.just.agentweb.download.DownloadingService; +import com.just.agentweb.utils.LogUtils; +import com.just.agentweb.widget.IWebLayout; +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xutil.net.JsonUtil; + +import java.util.HashMap; + +/** + * 通用WebView页面 + * + * @author xuexiang + * @since 2019/1/4 下午11:13 + */ +public class AgentWebFragment extends Fragment implements FragmentKeyDown { + public static final String KEY_URL = "com.xuexiang.xuidemo.base.webview.key_url"; + + private ImageView mBackImageView; + private View mLineView; + private ImageView mFinishImageView; + private TextView mTitleTextView; + private AgentWeb mAgentWeb; + private ImageView mMoreImageView; + private PopupMenu mPopupMenu; + public static final String TAG = AgentWebFragment.class.getSimpleName(); + private DownloadingService mDownloadingService; + + public static AgentWebFragment getInstance(String url) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_URL, url); + return getInstance(bundle); + } + + public static AgentWebFragment getInstance(Bundle bundle) { + AgentWebFragment fragment = new AgentWebFragment(); + if (bundle != null) { + fragment.setArguments(bundle); + } + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_agentweb, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mAgentWeb = AgentWeb.with(this) + //传入AgentWeb的父控件。 + .setAgentWebParent((LinearLayout) view, -1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) + //设置进度条颜色与高度,-1为默认值,高度为2,单位为dp。 + .useDefaultIndicator(-1, 3) + //设置 IAgentWebSettings。 + .setAgentWebWebSettings(getSettings()) + //WebViewClient , 与 WebView 使用一致 ,但是请勿获取WebView调用setWebViewClient(xx)方法了,会覆盖AgentWeb DefaultWebClient,同时相应的中间件也会失效。 + .setWebViewClient(mWebViewClient) + //WebChromeClient + .setWebChromeClient(mWebChromeClient) + //设置WebChromeClient中间件,支持多个WebChromeClient,AgentWeb 3.0.0 加入。 + .useMiddlewareWebChrome(getMiddlewareWebChrome()) + //设置WebViewClient中间件,支持多个WebViewClient, AgentWeb 3.0.0 加入。 + .useMiddlewareWebClient(getMiddlewareWebClient()) + //权限拦截 2.0.0 加入。 + .setPermissionInterceptor(mPermissionInterceptor) + //严格模式 Android 4.2.2 以下会放弃注入对象 ,使用AgentWebView没影响。 + .setSecurityType(AgentWeb.SecurityType.STRICT_CHECK) + //自定义UI AgentWeb3.0.0 加入。 + .setAgentWebUIController(new UIController(getActivity())) + //参数1是错误显示的布局,参数2点击刷新控件ID -1表示点击整个布局都刷新, AgentWeb 3.0.0 加入。 + .setMainFrameErrorView(R.layout.agentweb_error_page, -1) + .setWebLayout(getWebLayout()) + .interceptUnkownUrl() + //创建AgentWeb。 + .createAgentWeb() + .ready()//设置 WebSettings。 + //WebView载入该url地址的页面并显示。 + .go(getUrl()); + + if (MyApp.isDebug()) { + AgentWebConfig.debug(); + } + + // 得到 AgentWeb 最底层的控件 + addBackgroundChild(mAgentWeb.getWebCreator().getWebParentLayout()); + + initView(view); + + // AgentWeb 没有把WebView的功能全面覆盖 ,所以某些设置 AgentWeb 没有提供,请从WebView方面入手设置。 + mAgentWeb.getWebCreator().getWebView().setOverScrollMode(WebView.OVER_SCROLL_NEVER); + } + + protected IWebLayout getWebLayout() { + return new WebLayout(getActivity()); + } + + protected void initView(View view) { + mBackImageView = view.findViewById(R.id.iv_back); + mLineView = view.findViewById(R.id.view_line); + mFinishImageView = view.findViewById(R.id.iv_finish); + mTitleTextView = view.findViewById(R.id.toolbar_title); + mBackImageView.setOnClickListener(mOnClickListener); + mFinishImageView.setOnClickListener(mOnClickListener); + mMoreImageView = view.findViewById(R.id.iv_more); + mMoreImageView.setOnClickListener(mOnClickListener); + pageNavigator(View.GONE); + } + + protected void addBackgroundChild(FrameLayout frameLayout) { + TextView textView = new TextView(frameLayout.getContext()); + textView.setText("技术由 AgentWeb 提供"); + textView.setTextSize(16); + textView.setTextColor(Color.parseColor("#727779")); + frameLayout.setBackgroundColor(Color.parseColor("#272b2d")); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2); + params.gravity = Gravity.CENTER_HORIZONTAL; + final float scale = frameLayout.getContext().getResources().getDisplayMetrics().density; + params.topMargin = (int) (15 * scale + 0.5f); + frameLayout.addView(textView, 0, params); + } + + + private void pageNavigator(int tag) { + mBackImageView.setVisibility(tag); + mLineView.setVisibility(tag); + } + + private View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.iv_back: + // true表示AgentWeb处理了该事件 + if (!mAgentWeb.back()) { + AgentWebFragment.this.getActivity().finish(); + } + break; + case R.id.iv_finish: + AgentWebFragment.this.getActivity().finish(); + break; + case R.id.iv_more: + showPoPup(v); + break; + default: + break; + + } + } + + }; + + //========================================// + + /** + * 权限申请拦截器 + */ + protected PermissionInterceptor mPermissionInterceptor = new PermissionInterceptor() { + /** + * PermissionInterceptor 能达到 url1 允许授权, url2 拒绝授权的效果。 + * @param url + * @param permissions + * @param action + * @return true 该Url对应页面请求权限进行拦截 ,false 表示不拦截。 + */ + @Override + public boolean intercept(String url, String[] permissions, String action) { + Log.i(TAG, "mUrl:" + url + " permission:" + JsonUtil.toJson(permissions) + " action:" + action); + return false; + } + }; + + //=====================下载============================// + + /** + * 更新于 AgentWeb 4.0.0,下载监听 + */ + protected DownloadListenerAdapter mDownloadListenerAdapter = new DownloadListenerAdapter() { + /** + * + * @param url 下载链接 + * @param userAgent UserAgent + * @param contentDisposition ContentDisposition + * @param mimetype 资源的媒体类型 + * @param contentLength 文件长度 + * @param extra 下载配置 , 用户可以通过 Extra 修改下载icon , 关闭进度条 , 是否强制下载。 + * @return true 表示用户处理了该下载事件 , false 交给 AgentWeb 下载 + */ + @Override + public boolean onStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength, AgentWebDownloader.Extra extra) { + LogUtils.i(TAG, "onStart:" + url); + // 是否开启断点续传 + extra.setOpenBreakPointDownload(true) + //下载通知的icon + .setIcon(R.drawable.ic_file_download_black_24dp) + // 连接的超时时间 + .setConnectTimeOut(6000) + // 以8KB位单位,默认60s ,如果60s内无法从网络流中读满8KB数据,则抛出异常 + .setBlockMaxTime(10 * 60 * 1000) + // 下载的超时时间 + .setDownloadTimeOut(Long.MAX_VALUE) + // 串行下载更节省资源哦 + .setParallelDownload(false) + // false 关闭进度通知 + .setEnableIndicator(true) + // 自定义请求头 + .addHeader("Cookie", "xx") + // 下载完成自动打开 + .setAutoOpen(true) + // 强制下载,不管网络网络类型 + .setForceDownload(true); + return false; + } + + /** + * + * 不需要暂停或者停止下载该方法可以不必实现 + * @param url + * @param downloadingService 用户可以通过 DownloadingService#shutdownNow 终止下载 + */ + @Override + public void onBindService(String url, DownloadingService downloadingService) { + super.onBindService(url, downloadingService); + mDownloadingService = downloadingService; + LogUtils.i(TAG, "onBindService:" + url + " DownloadingService:" + downloadingService); + } + + /** + * 回调onUnbindService方法,让用户释放掉 DownloadingService。 + * @param url + * @param downloadingService + */ + @Override + public void onUnbindService(String url, DownloadingService downloadingService) { + super.onUnbindService(url, downloadingService); + mDownloadingService = null; + LogUtils.i(TAG, "onUnbindService:" + url); + } + + /** + * + * @param url 下载链接 + * @param loaded 已经下载的长度 + * @param length 文件的总大小 + * @param usedTime 耗时 ,单位ms + * 注意该方法回调在子线程 ,线程名 AsyncTask #XX 或者 AgentWeb # XX + */ + @Override + public void onProgress(String url, long loaded, long length, long usedTime) { + int mProgress = (int) ((loaded) / Float.valueOf(length) * 100); + LogUtils.i(TAG, "onProgress:" + mProgress); + super.onProgress(url, loaded, length, usedTime); + } + + /** + * + * @param path 文件的绝对路径 + * @param url 下载地址 + * @param throwable 如果异常,返回给用户异常 + * @return true 表示用户处理了下载完成后续的事件 ,false 默认交给AgentWeb 处理 + */ + @Override + public boolean onResult(String path, String url, Throwable throwable) { + //下载成功 + if (null == throwable) { + //do you work + } else {//下载失败 + + } + // true 不会发出下载完成的通知 , 或者打开文件 + return false; + } + }; + + /** + * @return IAgentWebSettings + */ + public IAgentWebSettings getSettings() { + return new AbsAgentWebSettings() { + private AgentWeb mAgentWeb; + + @Override + protected void bindAgentWebSupport(AgentWeb agentWeb) { + this.mAgentWeb = agentWeb; + } + + /** + * AgentWeb 4.0.0 内部删除了 DownloadListener 监听 ,以及相关API ,将 Download 部分完全抽离出来独立一个库, + * 如果你需要使用 AgentWeb Download 部分 , 请依赖上 compile 'com.just.agentweb:download:4.0.0 , + * 如果你需要监听下载结果,请自定义 AgentWebSetting , New 出 DefaultDownloadImpl,传入DownloadListenerAdapter + * 实现进度或者结果监听,例如下面这个例子,如果你不需要监听进度,或者下载结果,下面 setDownloader 的例子可以忽略。 + * @param webView + * @param downloadListener + * @return WebListenerManager + */ + @Override + public WebListenerManager setDownloader(WebView webView, android.webkit.DownloadListener downloadListener) { + return super.setDownloader(webView, + DefaultDownloadImpl + .create(getActivity(), + webView, + mDownloadListenerAdapter, + mDownloadListenerAdapter, + this.mAgentWeb.getPermissionInterceptor())); + } + }; + } + + //===================WebChromeClient 和 WebViewClient===========================// + /** + * 页面空白,请检查scheme是否加上, scheme://host:port/path?query&query 。 + * + * @return mUrl + */ + public String getUrl() { + String target = ""; + Bundle bundle = getArguments(); + if (bundle != null) { + target = bundle.getString(KEY_URL); + } + + if (TextUtils.isEmpty(target)) { + target = "https://github.com/xuexiangjys"; + } + return target; + } + + protected WebChromeClient mWebChromeClient = new WebChromeClient() { + @Override + public void onProgressChanged(WebView view, int newProgress) { + Log.i(TAG, "onProgressChanged:" + newProgress + " view:" + view); + } + + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + if (mTitleTextView != null && !TextUtils.isEmpty(title)) { + if (title.length() > 10) { + title = title.substring(0, 10).concat("..."); + } + mTitleTextView.setText(title); + } + } + }; + + protected WebViewClient mWebViewClient = new WebViewClient() { + + private HashMap timer = new HashMap<>(); + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return shouldOverrideUrlLoading(view, request.getUrl() + ""); + } + + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return super.shouldInterceptRequest(view, request); + } + + // + @Override + public boolean shouldOverrideUrlLoading(final WebView view, String url) { + //intent:// scheme的处理 如果返回false , 则交给 DefaultWebClient 处理 , 默认会打开该Activity , 如果Activity不存在则跳到应用市场上去. true 表示拦截 + //例如优酷视频播放 ,intent://play?...package=com.youku.phone;end; + //优酷想唤起自己应用播放该视频 , 下面拦截地址返回 true 则会在应用内 H5 播放 ,禁止优酷唤起播放该视频, 如果返回 false , DefaultWebClient 会根据intent 协议处理 该地址 , 首先匹配该应用存不存在 ,如果存在 , 唤起该应用播放 , 如果不存在 , 则跳到应用市场下载该应用 . + if (url.startsWith("intent://") && url.contains("com.youku.phone")) { + return true; + } + + return false; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + Log.i(TAG, "mUrl:" + url + " onPageStarted target:" + getUrl()); + timer.put(url, System.currentTimeMillis()); + if (url.equals(getUrl())) { + pageNavigator(View.GONE); + } else { + pageNavigator(View.VISIBLE); + } + + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + + if (timer.get(url) != null) { + long overTime = System.currentTimeMillis(); + Long startTime = timer.get(url); + Log.i(TAG, " page mUrl:" + url + " used time:" + (overTime - startTime)); + } + + } + + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + } + }; + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + } + + + //========================菜单功能================================// + + /** + * 打开浏览器 + * + * @param targetUrl 外部浏览器打开的地址 + */ + private void openBrowser(String targetUrl) { + if (TextUtils.isEmpty(targetUrl) || targetUrl.startsWith("file://")) { + XToastUtils.toast(targetUrl + " 该链接无法使用浏览器打开。"); + return; + } + Intent intent = new Intent(); + intent.setAction("android.intent.action.VIEW"); + Uri uri = Uri.parse(targetUrl); + intent.setData(uri); + startActivity(intent); + } + + + /** + * 显示更多菜单 + * + * @param view 菜单依附在该View下面 + */ + private void showPoPup(View view) { + if (mPopupMenu == null) { + mPopupMenu = new PopupMenu(getContext(), view); + mPopupMenu.inflate(R.menu.menu_toolbar_web); + mPopupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener); + } + mPopupMenu.show(); + } + + /** + * 菜单事件 + */ + private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener = new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.refresh: + if (mAgentWeb != null) { + mAgentWeb.getUrlLoader().reload(); // 刷新 + } + return true; + + case R.id.copy: + if (mAgentWeb != null) { + toCopy(getContext(), mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + case R.id.default_browser: + if (mAgentWeb != null) { + openBrowser(mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + case R.id.share: + if (mAgentWeb != null) { + shareWebUrl(mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + default: + return false; + } + + } + }; + + /** + * 分享网页链接 + * + * @param url 网页链接 + */ + private void shareWebUrl(String url) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, url); + shareIntent.setType("text/plain"); + //设置分享列表的标题,并且每次都显示分享列表 + startActivity(Intent.createChooser(shareIntent, "分享到")); + } + + + /** + * 复制字符串 + * + * @param context + * @param text + */ + private void toCopy(Context context, String text) { + ClipboardManager manager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (manager == null) { + return; + } + manager.setPrimaryClip(ClipData.newPlainText(null, text)); + } + + //===================生命周期管理===========================// + + @Override + public void onResume() { + mAgentWeb.getWebLifeCycle().onResume();//恢复 + super.onResume(); + } + + @Override + public void onPause() { + mAgentWeb.getWebLifeCycle().onPause(); //暂停应用内所有WebView , 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。 + super.onPause(); + } + + @Override + public boolean onFragmentKeyDown(int keyCode, KeyEvent event) { + return mAgentWeb.handleKeyEvent(keyCode, event); + } + + @Override + public void onDestroyView() { + mAgentWeb.getWebLifeCycle().onDestroy(); + super.onDestroyView(); + } + + //===================中间键===========================// + + + /** + * MiddlewareWebClientBase 是 AgentWeb 3.0.0 提供一个强大的功能, + * 如果用户需要使用 AgentWeb 提供的功能, 不想重写 WebClientView方 + * 法覆盖AgentWeb提供的功能,那么 MiddlewareWebClientBase 是一个 + * 不错的选择 。 + * + * @return + */ + protected MiddlewareWebClientBase getMiddlewareWebClient() { + return new MiddlewareWebViewClient() { + /** + * + * @param view + * @param url + * @return + */ + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // 拦截 url,不执行 DefaultWebClient#shouldOverrideUrlLoading + if (url.startsWith("agentweb")) { + Log.i(TAG, "agentweb scheme ~"); + return true; + } + // 执行 DefaultWebClient#shouldOverrideUrlLoading + if (super.shouldOverrideUrlLoading(view, url)) { + return true; + } + // do you work + return false; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return super.shouldOverrideUrlLoading(view, request); + } + }; + } + + protected MiddlewareWebChromeBase getMiddlewareWebChrome() { + return new MiddlewareChromeClient() { + }; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/BaseWebViewFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/BaseWebViewFragment.java new file mode 100644 index 00000000..af3c0396 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/BaseWebViewFragment.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.view.KeyEvent; + +import com.just.agentweb.core.AgentWeb; +import com.kerwin.wumei.core.BaseFragment; + +/** + * 基础web + * + * @author xuexiang + * @since 2019/5/28 10:22 + */ +public abstract class BaseWebViewFragment extends BaseFragment { + + protected AgentWeb mAgentWeb; + + //===================生命周期管理===========================// + @Override + public void onResume() { + if (mAgentWeb != null) { + //恢复 + mAgentWeb.getWebLifeCycle().onResume(); + } + super.onResume(); + } + + @Override + public void onPause() { + if (mAgentWeb != null) { + //暂停应用内所有WebView , 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。 + mAgentWeb.getWebLifeCycle().onPause(); + } + super.onPause(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mAgentWeb != null && mAgentWeb.handleKeyEvent(keyCode, event); + } + + @Override + public void onDestroyView() { + if (mAgentWeb != null) { + mAgentWeb.destroy(); + } + super.onDestroyView(); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/FragmentKeyDown.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/FragmentKeyDown.java new file mode 100644 index 00000000..7094fa6f --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/FragmentKeyDown.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.view.KeyEvent; + +/** + * + * + * @author xuexiang + * @since 2019/1/4 下午11:32 + */ +public interface FragmentKeyDown { + + /** + * fragment按键监听 + * @param keyCode + * @param event + * @return + */ + boolean onFragmentKeyDown(int keyCode, KeyEvent event); +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/LollipopFixedWebView.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/LollipopFixedWebView.java new file mode 100644 index 00000000..6f5ad0cb --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/LollipopFixedWebView.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build; +import android.util.AttributeSet; +import android.webkit.WebView; + +/** + * 修复 Android 5.0 & 5.1 打开 WebView 闪退问题: + * 参阅 https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview + */ +@SuppressWarnings("unused") +public class LollipopFixedWebView extends WebView { + public LollipopFixedWebView(Context context) { + super(getFixedContext(context)); + } + + public LollipopFixedWebView(Context context, AttributeSet attrs) { + super(getFixedContext(context), attrs); + } + + public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(getFixedContext(context), attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(getFixedContext(context), attrs, defStyleAttr, defStyleRes); + } + + public LollipopFixedWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { + super(getFixedContext(context), attrs, defStyleAttr, privateBrowsing); + } + + public static Context getFixedContext(Context context) { + if (isLollipopWebViewBug()) { + // Avoid crashing on Android 5 and 6 (API level 21 to 23) + return context.createConfigurationContext(new Configuration()); + } + return context; + } + + public static boolean isLollipopWebViewBug() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M; + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareChromeClient.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareChromeClient.java new file mode 100644 index 00000000..9babc825 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareChromeClient.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.util.Log; +import android.webkit.JsResult; +import android.webkit.WebView; + +import com.just.agentweb.core.client.MiddlewareWebChromeBase; + +/** + * WebChrome(WebChromeClient主要辅助WebView处理JavaScript的对话框、网站图片、网站title、加载进度等)中间件 + * 【浏览器】 + * @author xuexiang + * @since 2019/1/4 下午11:31 + */ +public class MiddlewareChromeClient extends MiddlewareWebChromeBase { + + public MiddlewareChromeClient() { + + } + + @Override + public boolean onJsAlert(WebView view, String url, String message, JsResult result) { + Log.i("Info", "onJsAlert:" + url); + return super.onJsAlert(view, url, message, result); + } + + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + Log.i("Info", "onProgressChanged:"); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareWebViewClient.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareWebViewClient.java new file mode 100644 index 00000000..e4a46b61 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/MiddlewareWebViewClient.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.net.Uri; +import android.os.Build; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; + +import androidx.annotation.RequiresApi; + +import com.just.agentweb.core.client.MiddlewareWebClientBase; +import com.kerwin.wumei.R; +import com.xuexiang.xui.utils.ResUtils; + +/** + * 【网络请求、加载】 + * WebClient(WebViewClient 这个类主要帮助WebView处理各种通知、url加载,请求时间的)中间件 + *

+ *

+ * 方法的执行顺序,例如下面用了7个中间件一个 WebViewClient + *

+ * .useMiddlewareWebClient(getMiddlewareWebClient()) // 1 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 2 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 3 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 4 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 5 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 6 + * .useMiddlewareWebClient(getMiddlewareWebClient()) // 7 + * DefaultWebClient // 8 + * .setWebViewClient(mWebViewClient) // 9 + *

+ *

+ * 典型的洋葱模型 + * 对象内部的方法执行顺序: 1->2->3->4->5->6->7->8->9->8->7->6->5->4->3->2->1 + *

+ *

+ * 中断中间件的执行, 删除super.methodName(...) 这行即可 + *

+ * 这里主要是做去广告的工作 + */ +public class MiddlewareWebViewClient extends MiddlewareWebClientBase { + + public MiddlewareWebViewClient() { + } + + private static int count = 1; + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + Log.i("Info", "MiddlewareWebViewClient -- > shouldOverrideUrlLoading:" + request.getUrl().toString() + " c:" + (count++)); + if (shouldOverrideUrlLoadingByApp(view, request.getUrl().toString())) { + return true; + } + return super.shouldOverrideUrlLoading(view, request); + + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Log.i("Info", "MiddlewareWebViewClient -- > shouldOverrideUrlLoading:" + url + " c:" + (count++)); + if (shouldOverrideUrlLoadingByApp(view, url)) { + return true; + } + return super.shouldOverrideUrlLoading(view, url); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + url = url.toLowerCase(); + if (!hasAdUrl(url)) { + //正常加载 + return super.shouldInterceptRequest(view, url); + } else { + //含有广告资源屏蔽请求 + return new WebResourceResponse(null, null, null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString().toLowerCase(); + if (!hasAdUrl(url)) { + //正常加载 + return super.shouldInterceptRequest(view, request); + } else { + //含有广告资源屏蔽请求 + return new WebResourceResponse(null, null, null); + } + } + + /** + * 判断是否存在广告的链接 + * + * @param url + * @return + */ + private static boolean hasAdUrl(String url) { + String[] adUrls = ResUtils.getStringArray(R.array.adBlockUrl); + for (String adUrl : adUrls) { + if (url.contains(adUrl)) { + return true; + } + } + return false; + } + + + /** + * 根据url的scheme处理跳转第三方app的业务,true代表拦截,false代表不拦截 + */ + private boolean shouldOverrideUrlLoadingByApp(WebView webView, final String url) { + if (url.startsWith("http") || url.startsWith("https") || url.startsWith("ftp")) { + //不拦截http, https, ftp的请求 + Uri uri = Uri.parse(url); + if (uri != null && !(WebViewInterceptDialog.APP_LINK_HOST.equals(uri.getHost()) + //防止xui官网被拦截 + && url.contains("xpage"))) { + return false; + } + } + + WebViewInterceptDialog.show(url); + return true; + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/UIController.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/UIController.java new file mode 100644 index 00000000..b5650e1e --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/UIController.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.app.Activity; +import android.os.Handler; +import android.util.Log; +import android.webkit.WebView; + +import com.just.agentweb.core.web.AgentWebUIControllerImplBase; + +import java.lang.ref.WeakReference; + +/** + * 如果你需要修改某一个AgentWeb 内部的某一个弹窗 ,请看下面的例子 + * 注意写法一定要参照 DefaultUIController 的写法 ,因为UI自由定制,但是回调的方式是固定的,并且一定要回调。 + * + * @author xuexiang + * @since 2019-10-30 23:18 + */ +public class UIController extends AgentWebUIControllerImplBase { + + private WeakReference mActivity; + + public UIController(Activity activity) { + mActivity = new WeakReference<>(activity); + } + + @Override + public void onShowMessage(String message, String from) { + super.onShowMessage(message, from); + Log.i(TAG, "message:" + message); + } + + @Override + public void onSelectItemsPrompt(WebView view, String url, String[] items, Handler.Callback callback) { + // 使用默认的UI + super.onSelectItemsPrompt(view, url, items, callback); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/WebLayout.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/WebLayout.java new file mode 100644 index 00000000..341683e3 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/WebLayout.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.just.agentweb.widget.IWebLayout; +import com.scwang.smartrefresh.layout.SmartRefreshLayout; +import com.kerwin.wumei.R; + +/** + * 定义支持下来回弹的WebView + * + * @author xuexiang + * @since 2019/1/5 上午2:01 + */ +public class WebLayout implements IWebLayout { + + private final SmartRefreshLayout mSmartRefreshLayout; + private WebView mWebView; + + public WebLayout(Activity activity) { + mSmartRefreshLayout = (SmartRefreshLayout) LayoutInflater.from(activity).inflate(R.layout.fragment_pulldown_web, null); + mWebView = mSmartRefreshLayout.findViewById(R.id.webView); + } + + @NonNull + @Override + public ViewGroup getLayout() { + return mSmartRefreshLayout; + } + + @Nullable + @Override + public WebView getWebView() { + return mWebView; + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/WebViewInterceptDialog.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/WebViewInterceptDialog.java new file mode 100644 index 00000000..406e1753 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/WebViewInterceptDialog.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.widget.dialog.DialogLoader; +import com.xuexiang.xutil.XUtil; +import com.xuexiang.xutil.app.ActivityUtils; + +import java.net.URISyntaxException; + +/** + * WebView拦截提示 + * + * @author xuexiang + * @since 2019-10-21 9:51 + */ +public class WebViewInterceptDialog extends AppCompatActivity implements DialogInterface.OnDismissListener { + + private static final String KEY_INTERCEPT_URL = "key_intercept_url"; + + // TODO: 2019-10-30 这里修改你的applink + public static final String APP_LINK_HOST = "xuexiangjys.club"; + public static final String APP_LINK_ACTION = "com.xuexiang.xui.applink"; + + + /** + * 显示WebView拦截提示 + * + * @param url 需要拦截处理的url + */ + public static void show(String url) { + ActivityUtils.startActivity(WebViewInterceptDialog.class, KEY_INTERCEPT_URL, url); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String url = getIntent().getStringExtra(KEY_INTERCEPT_URL); + + DialogLoader.getInstance().showConfirmDialog( + this, + getOpenTitle(url), + ResUtils.getString(R.string.lab_yes), + (dialog, which) -> { + dialog.dismiss(); + if (isAppLink(url)) { + openAppLink(this, url); + } else { + openApp(url); + } + }, + ResUtils.getString(R.string.lab_no), + (dialog, which) -> dialog.dismiss() + ).setOnDismissListener(this); + + } + + private String getOpenTitle(String url) { + String scheme = getScheme(url); + if ("mqqopensdkapi".equals(scheme)) { + return "是否允许页面打开\"QQ\"?"; + } else { + return ResUtils.getString(R.string.lab_open_third_app); + } + } + + private String getScheme(String url) { + try { + Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + return intent.getScheme(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return ""; + } + + private boolean isAppLink(String url) { + Uri uri = Uri.parse(url); + return uri != null + && APP_LINK_HOST.equals(uri.getHost()) + && (url.startsWith("http") || url.startsWith("https")); + } + + + private void openApp(String url) { + Intent intent; + try { + intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + XUtil.getContext().startActivity(intent); + } catch (Exception e) { + XToastUtils.error("您所打开的第三方App未安装!"); + } + } + + private void openAppLink(Context context, String url) { + try { + Intent intent = new Intent(APP_LINK_ACTION); + intent.setData(Uri.parse(url)); + context.startActivity(intent); + } catch (Exception e) { + XToastUtils.error("您所打开的第三方App未安装!"); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/core/webview/XPageWebViewFragment.java b/android/app/src/main/java/com/kerwin/wumei/core/webview/XPageWebViewFragment.java new file mode 100644 index 00000000..8e77e67a --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/core/webview/XPageWebViewFragment.java @@ -0,0 +1,677 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.core.webview; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.Fragment; + +import com.just.agentweb.action.PermissionInterceptor; +import com.just.agentweb.core.AgentWeb; +import com.just.agentweb.core.client.DefaultWebClient; +import com.just.agentweb.core.client.MiddlewareWebChromeBase; +import com.just.agentweb.core.client.MiddlewareWebClientBase; +import com.just.agentweb.core.client.WebListenerManager; +import com.just.agentweb.core.web.AbsAgentWebSettings; +import com.just.agentweb.core.web.AgentWebConfig; +import com.just.agentweb.core.web.IAgentWebSettings; +import com.just.agentweb.download.AgentWebDownloader; +import com.just.agentweb.download.DefaultDownloadImpl; +import com.just.agentweb.download.DownloadListenerAdapter; +import com.just.agentweb.download.DownloadingService; +import com.just.agentweb.widget.IWebLayout; +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.base.XPageActivity; +import com.xuexiang.xpage.base.XPageFragment; +import com.xuexiang.xpage.core.PageOption; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xutil.common.logger.Logger; +import com.xuexiang.xutil.net.JsonUtil; + +import java.util.HashMap; + +import butterknife.BindView; +import butterknife.OnClick; + +/** + * 使用XPageFragment + * + * @author xuexiang + * @since 2019-05-26 18:15 + */ +@Page(params = {AgentWebFragment.KEY_URL}) +public class XPageWebViewFragment extends BaseFragment { + + @BindView(R.id.iv_back) + AppCompatImageView mIvBack; + @BindView(R.id.view_line) + View mLineView; + @BindView(R.id.toolbar_title) + TextView mTvTitle; + + protected AgentWeb mAgentWeb; + private PopupMenu mPopupMenu; + + private DownloadingService mDownloadingService; + + /** + * 打开网页 + * + * @param xPageActivity + * @param url + * @return + */ + public static Fragment openUrl(XPageActivity xPageActivity, String url) { + return PageOption.to(XPageWebViewFragment.class) + .putString(AgentWebFragment.KEY_URL, url) + .open(xPageActivity); + } + + /** + * 打开网页 + * + * @param fragment + * @param url + * @return + */ + public static Fragment openUrl(XPageFragment fragment, String url) { + return PageOption.to(XPageWebViewFragment.class) + .setNewActivity(true) + .putString(AgentWebFragment.KEY_URL, url) + .open(fragment); + } + + @Override + protected TitleBar initTitle() { + return null; + } + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_agentweb; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + mAgentWeb = AgentWeb.with(this) + //传入AgentWeb的父控件。 + .setAgentWebParent((LinearLayout) getRootView(), -1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) + //设置进度条颜色与高度,-1为默认值,高度为2,单位为dp。 + .useDefaultIndicator(-1, 3) + //设置 IAgentWebSettings。 + .setAgentWebWebSettings(getSettings()) + //WebViewClient , 与 WebView 使用一致 ,但是请勿获取WebView调用setWebViewClient(xx)方法了,会覆盖AgentWeb DefaultWebClient,同时相应的中间件也会失效。 + .setWebViewClient(mWebViewClient) + //WebChromeClient + .setWebChromeClient(mWebChromeClient) + //设置WebChromeClient中间件,支持多个WebChromeClient,AgentWeb 3.0.0 加入。 + .useMiddlewareWebChrome(getMiddlewareWebChrome()) + //设置WebViewClient中间件,支持多个WebViewClient, AgentWeb 3.0.0 加入。 + .useMiddlewareWebClient(getMiddlewareWebClient()) + //权限拦截 2.0.0 加入。 + .setPermissionInterceptor(mPermissionInterceptor) + //严格模式 Android 4.2.2 以下会放弃注入对象 ,使用AgentWebView没影响。 + .setSecurityType(AgentWeb.SecurityType.STRICT_CHECK) + //自定义UI AgentWeb3.0.0 加入。 + .setAgentWebUIController(new UIController(getActivity())) + //参数1是错误显示的布局,参数2点击刷新控件ID -1表示点击整个布局都刷新, AgentWeb 3.0.0 加入。 + .setMainFrameErrorView(R.layout.agentweb_error_page, -1) + .setWebLayout(getWebLayout()) + //打开其他页面时,弹窗质询用户前往其他应用 AgentWeb 3.0.0 加入。 + .setOpenOtherPageWays(DefaultWebClient.OpenOtherPageWays.DISALLOW) + //拦截找不到相关页面的Url AgentWeb 3.0.0 加入。 + .interceptUnkownUrl() + //创建AgentWeb。 + .createAgentWeb() + .ready()//设置 WebSettings。 + //WebView载入该url地址的页面并显示。 + .go(getUrl()); + + if (MyApp.isDebug()) { + AgentWebConfig.debug(); + } + + pageNavigator(View.GONE); + // 得到 AgentWeb 最底层的控件 + addBackgroundChild(mAgentWeb.getWebCreator().getWebParentLayout()); + + // AgentWeb 没有把WebView的功能全面覆盖 ,所以某些设置 AgentWeb 没有提供,请从WebView方面入手设置。 + mAgentWeb.getWebCreator().getWebView().setOverScrollMode(WebView.OVER_SCROLL_NEVER); + } + + protected IWebLayout getWebLayout() { + return new WebLayout(getActivity()); + } + + protected void addBackgroundChild(FrameLayout frameLayout) { + TextView textView = new TextView(frameLayout.getContext()); + textView.setText("技术由 AgentWeb 提供"); + textView.setTextSize(16); + textView.setTextColor(Color.parseColor("#727779")); + frameLayout.setBackgroundColor(Color.parseColor("#272b2d")); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2); + params.gravity = Gravity.CENTER_HORIZONTAL; + final float scale = frameLayout.getContext().getResources().getDisplayMetrics().density; + params.topMargin = (int) (15 * scale + 0.5f); + frameLayout.addView(textView, 0, params); + } + + + private void pageNavigator(int tag) { + //返回的导航按钮 + mIvBack.setVisibility(tag); + mLineView.setVisibility(tag); + } + + @SingleClick + @OnClick({R.id.iv_back, R.id.iv_finish, R.id.iv_more}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.iv_back: + // true表示AgentWeb处理了该事件 + if (!mAgentWeb.back()) { + popToBack(); + } + break; + case R.id.iv_finish: + popToBack(); + break; + case R.id.iv_more: + showPoPup(view); + break; + default: + break; + } + } + + //=====================下载============================// + + /** + * 更新于 AgentWeb 4.0.0,下载监听 + */ + protected DownloadListenerAdapter mDownloadListenerAdapter = new DownloadListenerAdapter() { + /** + * + * @param url 下载链接 + * @param userAgent UserAgent + * @param contentDisposition ContentDisposition + * @param mimeType 资源的媒体类型 + * @param contentLength 文件长度 + * @param extra 下载配置 , 用户可以通过 Extra 修改下载icon , 关闭进度条 , 是否强制下载。 + * @return true 表示用户处理了该下载事件 , false 交给 AgentWeb 下载 + */ + @Override + public boolean onStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength, AgentWebDownloader.Extra extra) { + Logger.i("onStart:" + url); + // 是否开启断点续传 + extra.setOpenBreakPointDownload(true) + //下载通知的icon + .setIcon(R.drawable.ic_file_download_black_24dp) + // 连接的超时时间 + .setConnectTimeOut(6000) + // 以8KB位单位,默认60s ,如果60s内无法从网络流中读满8KB数据,则抛出异常 + .setBlockMaxTime(10 * 60 * 1000) + // 下载的超时时间 + .setDownloadTimeOut(Long.MAX_VALUE) + // 串行下载更节省资源哦 + .setParallelDownload(false) + // false 关闭进度通知 + .setEnableIndicator(true) + // 自定义请求头 + .addHeader("Cookie", "xx") + // 下载完成自动打开 + .setAutoOpen(true) + // 强制下载,不管网络网络类型 + .setForceDownload(true); + return false; + } + + /** + * + * 不需要暂停或者停止下载该方法可以不必实现 + * @param url + * @param downloadingService 用户可以通过 DownloadingService#shutdownNow 终止下载 + */ + @Override + public void onBindService(String url, DownloadingService downloadingService) { + super.onBindService(url, downloadingService); + mDownloadingService = downloadingService; + Logger.i("onBindService:" + url + " DownloadingService:" + downloadingService); + } + + /** + * 回调onUnbindService方法,让用户释放掉 DownloadingService。 + * @param url + * @param downloadingService + */ + @Override + public void onUnbindService(String url, DownloadingService downloadingService) { + super.onUnbindService(url, downloadingService); + mDownloadingService = null; + Logger.i("onUnbindService:" + url); + } + + /** + * + * @param url 下载链接 + * @param loaded 已经下载的长度 + * @param length 文件的总大小 + * @param usedTime 耗时 ,单位ms + * 注意该方法回调在子线程 ,线程名 AsyncTask #XX 或者 AgentWeb # XX + */ + @Override + public void onProgress(String url, long loaded, long length, long usedTime) { + int mProgress = (int) ((loaded) / (float) length * 100); + Logger.i("onProgress:" + mProgress); + super.onProgress(url, loaded, length, usedTime); + } + + /** + * + * @param path 文件的绝对路径 + * @param url 下载地址 + * @param throwable 如果异常,返回给用户异常 + * @return true 表示用户处理了下载完成后续的事件 ,false 默认交给AgentWeb 处理 + */ + @Override + public boolean onResult(String path, String url, Throwable throwable) { + //下载成功 + if (null == throwable) { + //do you work + } else {//下载失败 + + } + // true 不会发出下载完成的通知 , 或者打开文件 + return false; + } + }; + + /** + * 下载服务设置 + * + * @return IAgentWebSettings + */ + public IAgentWebSettings getSettings() { + return new AbsAgentWebSettings() { + private AgentWeb mAgentWeb; + + @Override + protected void bindAgentWebSupport(AgentWeb agentWeb) { + this.mAgentWeb = agentWeb; + } + + /** + * AgentWeb 4.0.0 内部删除了 DownloadListener 监听 ,以及相关API ,将 Download 部分完全抽离出来独立一个库, + * 如果你需要使用 AgentWeb Download 部分 , 请依赖上 compile 'com.just.agentweb:download:4.0.0 , + * 如果你需要监听下载结果,请自定义 AgentWebSetting , New 出 DefaultDownloadImpl,传入DownloadListenerAdapter + * 实现进度或者结果监听,例如下面这个例子,如果你不需要监听进度,或者下载结果,下面 setDownloader 的例子可以忽略。 + * @param webView + * @param downloadListener + * @return WebListenerManager + */ + @Override + public WebListenerManager setDownloader(WebView webView, android.webkit.DownloadListener downloadListener) { + return super.setDownloader(webView, + DefaultDownloadImpl + .create(getActivity(), + webView, + mDownloadListenerAdapter, + mDownloadListenerAdapter, + mAgentWeb.getPermissionInterceptor())); + } + }; + } + + //===================WebChromeClient 和 WebViewClient===========================// + + /** + * 页面空白,请检查scheme是否加上, scheme://host:port/path?query&query 。 + * + * @return mUrl + */ + public String getUrl() { + String target = ""; + Bundle bundle = getArguments(); + if (bundle != null) { + target = bundle.getString(AgentWebFragment.KEY_URL); + } + + if (TextUtils.isEmpty(target)) { + target = "https://github.com/xuexiangjys"; + } + return target; + } + + /** + * 和浏览器相关,包括和JS的交互 + */ + protected WebChromeClient mWebChromeClient = new WebChromeClient() { + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + //网页加载进度 + } + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + if (mTvTitle != null && !TextUtils.isEmpty(title)) { + if (title.length() > 10) { + title = title.substring(0, 10).concat("..."); + } + mTvTitle.setText(title); + } + } + }; + + /** + * 和网页url加载相关,统计加载时间 + */ + protected WebViewClient mWebViewClient = new WebViewClient() { + private HashMap mTimer = new HashMap<>(); + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return shouldOverrideUrlLoading(view, request.getUrl() + ""); + } + + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + return super.shouldInterceptRequest(view, request); + } + @Override + public boolean shouldOverrideUrlLoading(final WebView view, String url) { + //intent:// scheme的处理 如果返回false , 则交给 DefaultWebClient 处理 , 默认会打开该Activity , 如果Activity不存在则跳到应用市场上去. true 表示拦截 + //例如优酷视频播放 ,intent://play?...package=com.youku.phone;end; + //优酷想唤起自己应用播放该视频 , 下面拦截地址返回 true 则会在应用内 H5 播放 ,禁止优酷唤起播放该视频, 如果返回 false , DefaultWebClient 会根据intent 协议处理 该地址 , 首先匹配该应用存不存在 ,如果存在 , 唤起该应用播放 , 如果不存在 , 则跳到应用市场下载该应用 . + if (url.startsWith("intent://") && url.contains("com.youku.phone")) { + return true; + } + return false; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + mTimer.put(url, System.currentTimeMillis()); + if (url.equals(getUrl())) { + pageNavigator(View.GONE); + } else { + pageNavigator(View.VISIBLE); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (mTimer.get(url) != null) { + long overTime = System.currentTimeMillis(); + Long startTime = mTimer.get(url); + //统计页面的使用时长 + Logger.i(" page mUrl:" + url + " used time:" + (overTime - startTime)); + } + } + + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + } + }; + + //=====================菜单========================// + + /** + * 显示更多菜单 + * + * @param view 菜单依附在该View下面 + */ + private void showPoPup(View view) { + if (mPopupMenu == null) { + mPopupMenu = new PopupMenu(getContext(), view); + mPopupMenu.inflate(R.menu.menu_toolbar_web); + mPopupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener); + } + mPopupMenu.show(); + } + + /** + * 菜单事件 + */ + private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener = new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.refresh: + if (mAgentWeb != null) { + mAgentWeb.getUrlLoader().reload(); // 刷新 + } + return true; + case R.id.copy: + if (mAgentWeb != null) { + toCopy(getContext(), mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + case R.id.default_browser: + if (mAgentWeb != null) { + openBrowser(mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + case R.id.share: + if (mAgentWeb != null) { + shareWebUrl(mAgentWeb.getWebCreator().getWebView().getUrl()); + } + return true; + default: + return false; + } + + } + }; + + /** + * 打开浏览器 + * + * @param targetUrl 外部浏览器打开的地址 + */ + private void openBrowser(String targetUrl) { + if (TextUtils.isEmpty(targetUrl) || targetUrl.startsWith("file://")) { + XToastUtils.toast(targetUrl + " 该链接无法使用浏览器打开。"); + return; + } + Intent intent = new Intent(); + intent.setAction("android.intent.action.VIEW"); + Uri uri = Uri.parse(targetUrl); + intent.setData(uri); + startActivity(intent); + } + + /** + * 分享网页链接 + * + * @param url 网页链接 + */ + private void shareWebUrl(String url) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, url); + shareIntent.setType("text/plain"); + //设置分享列表的标题,并且每次都显示分享列表 + startActivity(Intent.createChooser(shareIntent, "分享到")); + } + + /** + * 复制字符串 + * + * @param context + * @param text + */ + private void toCopy(Context context, String text) { + ClipboardManager manager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (manager == null) { + return; + } + manager.setPrimaryClip(ClipData.newPlainText(null, text)); + } + + //===================生命周期管理===========================// + + @Override + public void onResume() { + if (mAgentWeb != null) { + mAgentWeb.getWebLifeCycle().onResume();//恢复 + } + super.onResume(); + } + + @Override + public void onPause() { + if (mAgentWeb != null) { + mAgentWeb.getWebLifeCycle().onPause(); //暂停应用内所有WebView , 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。 + } + super.onPause(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mAgentWeb != null && mAgentWeb.handleKeyEvent(keyCode, event); + } + + @Override + public void onDestroyView() { + if (mAgentWeb != null) { + mAgentWeb.destroy(); + } + super.onDestroyView(); + } + + + //===================中间键===========================// + + + /** + * MiddlewareWebClientBase 是 AgentWeb 3.0.0 提供一个强大的功能, + * 如果用户需要使用 AgentWeb 提供的功能, 不想重写 WebClientView方 + * 法覆盖AgentWeb提供的功能,那么 MiddlewareWebClientBase 是一个 + * 不错的选择 。 + * + * @return + */ + protected MiddlewareWebClientBase getMiddlewareWebClient() { + return new MiddlewareWebViewClient() { + /** + * + * @param view + * @param url + * @return + */ + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + // 拦截 url,不执行 DefaultWebClient#shouldOverrideUrlLoading + if (url.startsWith("agentweb")) { + return true; + } + // 执行 DefaultWebClient#shouldOverrideUrlLoading + if (super.shouldOverrideUrlLoading(view, url)) { + return true; + } + // do you work + return false; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return super.shouldOverrideUrlLoading(view, request); + } + }; + } + + protected MiddlewareWebChromeBase getMiddlewareWebChrome() { + return new MiddlewareChromeClient() { + }; + } + + /** + * 权限申请拦截器 + */ + protected PermissionInterceptor mPermissionInterceptor = new PermissionInterceptor() { + /** + * PermissionInterceptor 能达到 url1 允许授权, url2 拒绝授权的效果。 + * @param url + * @param permissions + * @param action + * @return true 该Url对应页面请求权限进行拦截 ,false 表示不拦截。 + */ + @Override + public boolean intercept(String url, String[] permissions, String action) { + Logger.i("mUrl:" + url + " permission:" + JsonUtil.toJson(permissions) + " action:" + action); + return false; + } + }; + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/AboutFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/AboutFragment.java new file mode 100644 index 00000000..0ea94321 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/AboutFragment.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.fragment; + +import android.widget.TextView; + +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.core.webview.AgentWebActivity; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.widget.grouplist.XUIGroupListView; +import com.xuexiang.xutil.app.AppUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import butterknife.BindView; + + +@Page(name = "关于") +public class AboutFragment extends BaseFragment { + + @BindView(R.id.tv_version) + TextView mVersionTextView; + @BindView(R.id.about_list) + XUIGroupListView mAboutGroupListView; + @BindView(R.id.tv_copyright) + TextView mCopyrightTextView; + + @Override + protected int getLayoutId() { + return R.layout.fragment_about; + } + + @Override + protected void initViews() { + mVersionTextView.setText(String.format("版本号:%s", AppUtils.getAppVersionName())); + + XUIGroupListView.newSection(getContext()) + .addItemView(mAboutGroupListView.createItemView(getResources().getString(R.string.about_item_homepage)), v -> AgentWebActivity.goWeb(getContext(), getString(R.string.url_project_github))) + .addItemView(mAboutGroupListView.createItemView(getResources().getString(R.string.about_item_author_github)), v -> AgentWebActivity.goWeb(getContext(), getString(R.string.url_author_github))) + .addItemView(mAboutGroupListView.createItemView("版本"), v -> XToastUtils.toast("版本升级")) + .addTo(mAboutGroupListView); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy", Locale.CHINA); + String currentYear = dateFormat.format(new Date()); + mCopyrightTextView.setText(String.format(getResources().getString(R.string.about_copyright), currentYear)); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/FeedbackFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/FeedbackFragment.java new file mode 100644 index 00000000..fec81e94 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/FeedbackFragment.java @@ -0,0 +1,30 @@ +package com.kerwin.wumei.fragment; + +import android.widget.TextView; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.core.webview.AgentWebActivity; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.widget.grouplist.XUIGroupListView; +import com.xuexiang.xutil.app.AppUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import butterknife.BindView; + +@Page(name = "意见反馈") +public class FeedbackFragment extends BaseFragment { + + @Override + protected int getLayoutId() { + return R.layout.fragment_feedback; + } + + @Override + protected void initViews() { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/LoginFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/LoginFragment.java new file mode 100644 index 00000000..683c09f9 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/LoginFragment.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.fragment; + +import android.graphics.Color; +import android.view.View; + +import com.kerwin.wumei.activity.MainActivity; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.RandomUtils; +import com.kerwin.wumei.utils.SettingUtils; +import com.kerwin.wumei.utils.TokenUtils; +import com.kerwin.wumei.utils.Utils; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.utils.CountDownButtonHelper; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.utils.ThemeUtils; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.button.roundbutton.RoundButton; +import com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText; +import com.xuexiang.xutil.app.ActivityUtils; + +import butterknife.BindView; +import butterknife.OnClick; + + +/** + * 登录页面 + * + * @author xuexiang + * @since 2019-11-17 22:15 + */ +@Page(anim = CoreAnim.none) +public class LoginFragment extends BaseFragment { + + @BindView(R.id.et_phone_number) + MaterialEditText etPhoneNumber; + @BindView(R.id.et_verify_code) + MaterialEditText etVerifyCode; + @BindView(R.id.btn_get_verify_code) + RoundButton btnGetVerifyCode; + + private CountDownButtonHelper mCountDownHelper; + + @Override + protected int getLayoutId() { + return R.layout.fragment_login; + } + + @Override + protected TitleBar initTitle() { + TitleBar titleBar = super.initTitle() + .setImmersive(true); + titleBar.setBackgroundColor(Color.TRANSPARENT); + titleBar.setTitle(""); + titleBar.setLeftImageDrawable(ResUtils.getVectorDrawable(getContext(), R.drawable.ic_login_close)); + titleBar.setActionTextColor(ThemeUtils.resolveColor(getContext(), R.attr.colorAccent)); + titleBar.addAction(new TitleBar.TextAction(R.string.title_jump_login) { + @Override + public void performAction(View view) { + onLoginSuccess(); + } + }); + return titleBar; + } + + @Override + protected void initViews() { + mCountDownHelper = new CountDownButtonHelper(btnGetVerifyCode, 60); + + //隐私政策弹窗 + if (!SettingUtils.isAgreePrivacy()) { + Utils.showPrivacyDialog(getContext(), (dialog, which) -> { + dialog.dismiss(); + SettingUtils.setIsAgreePrivacy(true); + }); + } + } + + @SingleClick + @OnClick({R.id.btn_get_verify_code, R.id.btn_login, R.id.tv_other_login, R.id.tv_forget_password, R.id.tv_user_protocol, R.id.tv_privacy_protocol}) + public void onViewClicked(View view) { + switch (view.getId()) { + case R.id.btn_get_verify_code: + if (etPhoneNumber.validate()) { + getVerifyCode(etPhoneNumber.getEditValue()); + } + break; + case R.id.btn_login: + if (etPhoneNumber.validate()) { + if (etVerifyCode.validate()) { + loginByVerifyCode(etPhoneNumber.getEditValue(), etVerifyCode.getEditValue()); + } + } + break; + case R.id.tv_other_login: + XToastUtils.info("其他登录方式"); + break; + case R.id.tv_forget_password: + XToastUtils.info("忘记密码"); + break; + case R.id.tv_user_protocol: + XToastUtils.info("用户协议"); + break; + case R.id.tv_privacy_protocol: + XToastUtils.info("隐私政策"); + break; + default: + break; + } + } + + /** + * 获取验证码 + */ + private void getVerifyCode(String phoneNumber) { + // TODO: 2020/8/29 这里只是界面演示而已 + XToastUtils.warning("只是演示,验证码请随便输"); + mCountDownHelper.start(); + } + + /** + * 根据验证码登录 + * + * @param phoneNumber 手机号 + * @param verifyCode 验证码 + */ + private void loginByVerifyCode(String phoneNumber, String verifyCode) { + // TODO: 2020/8/29 这里只是界面演示而已 + onLoginSuccess(); + } + + /** + * 登录成功的处理 + */ + private void onLoginSuccess() { + String token = RandomUtils.getRandomNumbersAndLetters(16); + if (TokenUtils.handleLoginSuccess(token)) { + popToBack(); + ActivityUtils.startActivity(MainActivity.class); + } + } + + @Override + public void onDestroyView() { + if (mCountDownHelper != null) { + mCountDownHelper.recycle(); + } + super.onDestroyView(); + } +} + diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/MessageFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/MessageFragment.java new file mode 100644 index 00000000..d5fecd09 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/MessageFragment.java @@ -0,0 +1,54 @@ +package com.kerwin.wumei.fragment; + +import android.view.View; +import android.widget.TextView; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.core.webview.AgentWebActivity; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.grouplist.XUIGroupListView; +import com.xuexiang.xutil.app.AppUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import butterknife.BindView; + + +@Page(name = "消息") +public class MessageFragment extends BaseFragment { + + @Override + protected int getLayoutId() { + return R.layout.fragment_message; + } + + @Override + protected TitleBar initTitle() { + com.xuexiang.xui.widget.actionbar.TitleBar titleBar = super.initTitle(); + titleBar.setCenterClickListener(new View.OnClickListener() { + @SingleClick + @Override + public void onClick(View view) { + + } + }); + titleBar.addAction(new com.xuexiang.xui.widget.actionbar.TitleBar.TextAction("菜单") { + @SingleClick + @Override + public void performAction(View view) { + + } + }); + return titleBar; + } + + @Override + protected void initViews() { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/SettingsFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/SettingsFragment.java new file mode 100644 index 00000000..0e3ccd6c --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/SettingsFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.fragment; + +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.TokenUtils; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.widget.dialog.DialogLoader; +import com.xuexiang.xui.widget.textview.supertextview.SuperTextView; +import com.xuexiang.xutil.XUtil; + +import butterknife.BindView; + +/** + * @author xuexiang + * @since 2019-10-15 22:38 + */ +@Page(name = "设置") +public class SettingsFragment extends BaseFragment implements SuperTextView.OnSuperTextViewClickListener { + + @BindView(R.id.menu_common) + SuperTextView menuCommon; + @BindView(R.id.menu_privacy) + SuperTextView menuPrivacy; + @BindView(R.id.menu_push) + SuperTextView menuPush; + @BindView(R.id.menu_helper) + SuperTextView menuHelper; + @BindView(R.id.menu_change_account) + SuperTextView menuChangeAccount; + @BindView(R.id.menu_logout) + SuperTextView menuLogout; + + @Override + protected int getLayoutId() { + return R.layout.fragment_settings; + } + + @Override + protected void initViews() { + menuCommon.setOnSuperTextViewClickListener(this); + menuPrivacy.setOnSuperTextViewClickListener(this); + menuPush.setOnSuperTextViewClickListener(this); + menuHelper.setOnSuperTextViewClickListener(this); + menuChangeAccount.setOnSuperTextViewClickListener(this); + menuLogout.setOnSuperTextViewClickListener(this); + } + + @SingleClick + @Override + public void onClick(SuperTextView superTextView) { + switch (superTextView.getId()) { + case R.id.menu_common: + case R.id.menu_privacy: + case R.id.menu_push: + case R.id.menu_helper: + XToastUtils.toast(superTextView.getLeftString()); + break; + case R.id.menu_change_account: + XToastUtils.toast(superTextView.getCenterString()); + break; + case R.id.menu_logout: + DialogLoader.getInstance().showConfirmDialog( + getContext(), + getString(R.string.lab_logout_confirm), + getString(R.string.lab_yes), + (dialog, which) -> { + dialog.dismiss(); + XUtil.getActivityLifecycleHelper().exit(); + TokenUtils.handleLogoutSuccess(); + }, + getString(R.string.lab_no), + (dialog, which) -> dialog.dismiss() + ); + break; + default: + break; + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceFragment.java new file mode 100644 index 00000000..8c9af48d --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceFragment.java @@ -0,0 +1,142 @@ +package com.kerwin.wumei.fragment.device; + +import android.Manifest; +import android.os.Build; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.appcompat.widget.AppCompatImageView; + +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.R; +import com.kerwin.wumei.activity.AddDeviceActivity; +import com.kerwin.wumei.activity.MainActivity; +import com.kerwin.wumei.adapter.entity.EspTouchViewModel; +import com.kerwin.wumei.core.BaseFragment; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.core.PageOption; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner; + +import java.util.List; + +import butterknife.BindView; + + +@Page(name = "WIFI网络配置") +public class AddDeviceFragment extends BaseFragment { + @BindView(R.id.advance_frame_layout) + FrameLayout advanceFrameLayout; + @BindView(R.id.advance_linear_layout) + LinearLayout advanceLinearLayout; + @BindView(R.id.advance_icon) + AppCompatImageView advanceIcon; + @BindView(R.id.wifi_password_icon) + AppCompatImageView wifiPasswordIcon; + + private static final String TAG = AddDeviceFragment.class.getSimpleName(); + private static final int REQUEST_PERMISSION = 0x01; + private EspTouchViewModel mViewModel; + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_add_device; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + //智能配网 + mViewModel = ((AddDeviceActivity)this.getActivity()).GetMViewModel(); + mViewModel.ssidSpinner = findViewById(R.id.ssid_spinner); + mViewModel.apPasswordEdit = findViewById(R.id.wifi_password_txt); + mViewModel.packageModeGroup = findViewById(R.id.packageModeGroup); + mViewModel.messageView = findViewById(R.id.messageView); + mViewModel.confirmBtn = findViewById(R.id.add_device_next_btn); + mViewModel.confirmBtn.setOnClickListener(v -> + { + // ((AddDeviceActivity)this.getActivity()).executeEsptouch(); + PageOption.to(AddDeviceTwoFragment.class) //跳转的fragment + .setAnim(CoreAnim.slide) //页面转场动画 + .setRequestCode(100) //请求码,用于返回结果 + .setAddToBackStack(true) //是否加入堆栈 + .putString("device_mac","0908070605040306") + .open(this); //打开页面进行跳转 + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION}; + requestPermissions(permissions, REQUEST_PERMISSION); + } + + MyApp.getInstance().observeBroadcast(this, broadcast -> { + Log.d(TAG, "onCreate: Broadcast=" + broadcast); + ((AddDeviceActivity)this.getActivity()).onWifiChanged(); + + List ssids=((AddDeviceActivity)this.getActivity()).GetSsids(); + if(ssids!=null && ssids.size()>0){ + Log.e(TAG, "进入数据绑定 " ); + mViewModel.ssidSpinner.setItems(ssids); + // ssidSpinner.setOnItemSelectedListener((spinner, position, id, item) -> SnackbarUtils.Long(spinner, "Clicked " + item).show()); + // ssidSpinner.setOnNothingSelectedListener(spinner -> SnackbarUtils.Long(spinner, "Nothing selected").show()); + String ssid=((AddDeviceActivity)this.getActivity()).GetSelectedSSID(); + if(ssid!=null && ssid.length()>0 && ssids.contains(ssid)) { + mViewModel.ssidSpinner.setSelectedItem(ssid); + } + } + }); + + + } + + @Override + protected void initListeners() { + //单击高级设置项 + advanceFrameLayout.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View view) { + int visible=advanceLinearLayout.getVisibility(); + if(visible!=0) { + advanceLinearLayout.setVisibility(View.VISIBLE); + advanceIcon.setImageDrawable(getResources().getDrawable((R.drawable.up))); + }else{ + advanceLinearLayout.setVisibility(View.GONE); + advanceIcon.setImageDrawable(getResources().getDrawable((R.drawable.down))); + } + } + }); + + //显示和隐藏密码 + wifiPasswordIcon.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View view){ + if(wifiPasswordIcon.getTag()==null) return; + if(wifiPasswordIcon.getTag().toString().equals("show")){ + wifiPasswordIcon.setImageDrawable(getResources().getDrawable((R.drawable.hide))); + wifiPasswordIcon.setTag("hide"); + mViewModel.apPasswordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance()); + }else{ + wifiPasswordIcon.setImageDrawable(getResources().getDrawable((R.drawable.show))); + wifiPasswordIcon.setTag("show"); + mViewModel.apPasswordEdit.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + } + } + }); + + + + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceTwoFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceTwoFragment.java new file mode 100644 index 00000000..9003ba42 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/AddDeviceTwoFragment.java @@ -0,0 +1,64 @@ +package com.kerwin.wumei.fragment.device; + +import android.Manifest; +import android.os.Build; +import android.os.Bundle; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.appcompat.widget.AppCompatImageView; + +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.R; +import com.kerwin.wumei.activity.AddDeviceActivity; +import com.kerwin.wumei.activity.MainActivity; +import com.kerwin.wumei.adapter.entity.EspTouchViewModel; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner; + +import java.util.List; + +import butterknife.BindView; + + +@Page(name = "设备信息") +public class AddDeviceTwoFragment extends BaseFragment { + + + /** + * 布局的资源id + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_add_device_two; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + Bundle arguments = getArguments(); + String mac = arguments.getString("device_mac"); + XToastUtils.toast("设备MAC:" + mac); + + + } + + @Override + protected void initListeners() { + + + + + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/DeviceFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/DeviceFragment.java new file mode 100644 index 00000000..84b2a353 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/DeviceFragment.java @@ -0,0 +1,100 @@ +package com.kerwin.wumei.fragment.device; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.widget.AppCompatImageView; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xui.adapter.simple.AdapterItem; +import com.xuexiang.xui.utils.WidgetUtils; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.popupwindow.popup.XUISimplePopup; +import com.xuexiang.xutil.display.ViewUtils; + +import butterknife.BindView; +import butterknife.OnClick; + +import static com.google.android.material.tabs.TabLayout.MODE_SCROLLABLE; + +@Page(name = "设备") +public class DeviceFragment extends BaseFragment implements TabLayout.OnTabSelectedListener{ + @BindView(R.id.tab_layout) + TabLayout tabLayout; + @BindView(R.id.view_pager) + ViewPager2 viewPager; + + private boolean mIsShowNavigationView; + private FragmentStateViewPager2Adapter mAdapter; + + /** + * @return 返回为 null意为不需要导航栏 + */ + @Override + protected TitleBar initTitle() { +// mAdapter.addFragment(2, SimpleTabFragment.newInstance("动态加入"), "动态加入"); +// mAdapter.removeFragment(2); +// mAdapter.notifyDataSetChanged(); + + return null; + } + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_device; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + mAdapter = new FragmentStateViewPager2Adapter(this); + tabLayout.setTabMode(MODE_SCROLLABLE); + tabLayout.addOnTabSelectedListener(this); + viewPager.setAdapter(mAdapter); + // 设置缓存的数量 + viewPager.setOffscreenPageLimit(1); + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setText(mAdapter.getPageTitle(position))).attach(); + + // 动态加载选项卡内容 + for (String page : MultiPage.getPageNames()) { + mAdapter.addFragment(SimpleTabFragment.newInstance(page), page); + } + mAdapter.notifyDataSetChanged(); + viewPager.setCurrentItem(0, false); + WidgetUtils.setTabLayoutTextFont(tabLayout); + } + + + @Override + public void onTabSelected(TabLayout.Tab tab) { + XToastUtils.toast("选中了:" + tab.getText()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/EditDeviceFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/EditDeviceFragment.java new file mode 100644 index 00000000..a05602ba --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/EditDeviceFragment.java @@ -0,0 +1,27 @@ +package com.kerwin.wumei.fragment.device; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.core.BaseFragment; +import com.xuexiang.xpage.annotation.Page; + +@Page(name = "分享设备") +public class EditDeviceFragment extends BaseFragment { + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_edit_device; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/FragmentStateViewPager2Adapter.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/FragmentStateViewPager2Adapter.java new file mode 100644 index 00000000..b0a75892 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/FragmentStateViewPager2Adapter.java @@ -0,0 +1,91 @@ +package com.kerwin.wumei.fragment.device; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author xuexiang + * @since 2020/5/21 1:27 AM + */ +public class FragmentStateViewPager2Adapter extends FragmentStateAdapter { + + private List mFragmentList = new ArrayList<>(); + + private List mTitleList = new ArrayList<>(); + + private List mIds = new ArrayList<>(); + + private AtomicLong mAtomicLong = new AtomicLong(0); + + public FragmentStateViewPager2Adapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return mFragmentList.get(position); + } + + public FragmentStateViewPager2Adapter addFragment(Fragment fragment, String title) { + if (fragment != null) { + mFragmentList.add(fragment); + mTitleList.add(title); + mIds.add(getAtomicGeneratedId()); + } + return this; + } + + public FragmentStateViewPager2Adapter addFragment(int index, Fragment fragment, String title) { + if (fragment != null && index >= 0 && index <= mFragmentList.size()) { + mFragmentList.add(index, fragment); + mTitleList.add(index, title); + mIds.add(index, getAtomicGeneratedId()); + } + return this; + } + + public FragmentStateViewPager2Adapter removeFragment(int index) { + if (index >= 0 && index < mFragmentList.size()) { + mFragmentList.remove(index); + mTitleList.remove(index); + mIds.remove(index); + } + return this; + } + + private long getAtomicGeneratedId() { + return mAtomicLong.incrementAndGet(); + } + + @Override + public int getItemCount() { + return mFragmentList.size(); + } + + public void clear() { + mFragmentList.clear(); + mTitleList.clear(); + mIds.clear(); + notifyDataSetChanged(); + } + + public CharSequence getPageTitle(int position) { + return mTitleList.get(position); + } + + @Override + public long getItemId(int position) { + return mIds.get(position); + } + + @Override + public boolean containsItem(long itemId) { + return mIds.contains(itemId); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/GroupFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/GroupFragment.java new file mode 100644 index 00000000..d85115e1 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/GroupFragment.java @@ -0,0 +1,27 @@ +package com.kerwin.wumei.fragment.device; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.core.BaseFragment; +import com.xuexiang.xpage.annotation.Page; + +@Page(name = "分组管理") +public class GroupFragment extends BaseFragment { + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_group; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/MultiPage.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/MultiPage.java new file mode 100644 index 00000000..c59f1e15 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/MultiPage.java @@ -0,0 +1,44 @@ +package com.kerwin.wumei.fragment.device;/* + + +/** + * @author xuexiang + * @since 2018/12/26 下午11:49 + */ +public enum MultiPage { + + 全部(0), + 浇灌(1), + 一楼(2), + 二楼(3), + 三楼(4), + 走廊(5); + + private final int position; + + MultiPage(int pos) { + position = pos; + } + + public static MultiPage getPage(int position) { + return MultiPage.values()[position]; + } + + public static int size() { + return MultiPage.values().length; + } + + public static String[] getPageNames() { + MultiPage[] pages = MultiPage.values(); + String[] pageNames = new String[pages.length]; + for (int i = 0; i < pages.length; i++) { + pageNames[i] = pages[i].name(); + } + return pageNames; + } + + public int getPosition() { + return position; + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/SceneFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/SceneFragment.java new file mode 100644 index 00000000..b07a69b8 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/SceneFragment.java @@ -0,0 +1,59 @@ +package com.kerwin.wumei.fragment.device; + +import android.Manifest; +import android.os.Build; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.appcompat.widget.AppCompatImageView; + +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.R; +import com.kerwin.wumei.activity.MainActivity; +import com.kerwin.wumei.adapter.entity.EspTouchViewModel; +import com.kerwin.wumei.core.BaseFragment; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner; + +import java.util.List; + +import butterknife.BindView; + + +@Page(anim = CoreAnim.none) +public class SceneFragment extends BaseFragment { + + /** + * @return 返回为 null意为不需要导航栏 + */ + @Override + protected TitleBar initTitle() { + return null; + } + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_scene; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { } + + @Override + protected void initListeners() { } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/ShareDeviceFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/ShareDeviceFragment.java new file mode 100644 index 00000000..964c66e2 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/ShareDeviceFragment.java @@ -0,0 +1,29 @@ +package com.kerwin.wumei.fragment.device; + +import com.kerwin.wumei.R; +import com.kerwin.wumei.core.BaseFragment; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.widget.actionbar.TitleBar; + +@Page(name = "分享设备") +public class ShareDeviceFragment extends BaseFragment { + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_share_device; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/device/SimpleTabFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/device/SimpleTabFragment.java new file mode 100644 index 00000000..965e7d08 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/device/SimpleTabFragment.java @@ -0,0 +1,255 @@ +package com.kerwin.wumei.fragment.device; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Vibrator; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.cardview.widget.CardView; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.RecyclerView; + +import com.alibaba.android.vlayout.DelegateAdapter; +import com.alibaba.android.vlayout.LayoutHelper; +import com.alibaba.android.vlayout.VirtualLayoutManager; +import com.alibaba.android.vlayout.layout.GridLayoutHelper; +import com.alibaba.android.vlayout.layout.LinearLayoutHelper; +import com.alibaba.android.vlayout.layout.StaggeredGridLayoutHelper; +import com.alibaba.android.vlayout.layout.StickyLayoutHelper; +import com.kerwin.wumei.R; +import com.kerwin.wumei.adapter.base.broccoli.BroccoliSimpleDelegateAdapter; +import com.kerwin.wumei.adapter.base.delegate.SimpleDelegateAdapter; +import com.kerwin.wumei.adapter.base.delegate.SingleDelegateAdapter; +import com.kerwin.wumei.adapter.entity.NewInfo; +import com.kerwin.wumei.utils.DemoDataProvider; +import com.kerwin.wumei.utils.RandomUtils; +import com.kerwin.wumei.utils.Utils; +import com.kerwin.wumei.utils.XToastUtils; +import com.scwang.smartrefresh.layout.SmartRefreshLayout; +import com.xuexiang.xrouter.annotation.AutoWired; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; +import com.xuexiang.xui.adapter.simple.AdapterItem; +import com.xuexiang.xui.widget.banner.widget.banner.SimpleImageBanner; +import com.xuexiang.xui.widget.button.SwitchIconView; +import com.xuexiang.xui.widget.imageview.ImageLoader; +import com.xuexiang.xui.widget.imageview.RadiusImageView; +import com.xuexiang.xui.widget.textview.supertextview.SuperButton; + +import java.util.ArrayList; +import java.util.Collection; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; +import me.samlss.broccoli.Broccoli; + +import static com.xuexiang.xui.utils.Utils.getScreenWidth; +import static com.xuexiang.xutil.display.DensityUtils.dip2px; + +/** + * @author xuexiang + * @since 2020/4/21 12:24 AM + */ +public class SimpleTabFragment extends Fragment { + private static final String TAG = "SimpleTabFragment"; + + private static final String KEY_TITLE = "title"; + +// @BindView(R.id.tv_title) +// TextView tvTitle; +// @BindView(R.id.tv_explain) +// TextView tvExplain; + + @BindView(R.id.recyclerView) + RecyclerView recyclerView; + @BindView(R.id.refreshLayout) + SmartRefreshLayout refreshLayout; +// @BindView(R.id.item_card_view) +// CardView itemCardView; + + private Unbinder mUnbinder; + + private SimpleDelegateAdapter mNewsAdapter; + + @AutoWired(name = KEY_TITLE) + String title; + + + public static SimpleTabFragment newInstance(String title) { + Bundle args = new Bundle(); + args.putString(KEY_TITLE, title); + SimpleTabFragment fragment = new SimpleTabFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + Log.e(TAG, "onAttach:" + title); + } + + @Override + public void onDetach() { + super.onDetach(); + Log.e(TAG, "onDetach:" + title); + } + + @Override + public void onResume() { + super.onResume(); + Log.e(TAG, "onResume:" + title); + } + + @Override + public void onStop() { + super.onStop(); + Log.e(TAG, "onStop:" + title); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + XRouter.getInstance().inject(this); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_simple_tab, container, false); + mUnbinder = ButterKnife.bind(this, view); + initView(); + return view; + } + + private void initView() { +// int randomNumber = RandomUtils.getRandom(10, 100); +// Log.e(TAG, "initView, random number:" + randomNumber + ", " + title); +// tvTitle.setText(String.format("这个是%s页面的内容", title)); +// tvExplain.setText(String.format("这个是页面随机生成的数字:%d", randomNumber)); + + + VirtualLayoutManager virtualLayoutManager = new VirtualLayoutManager(getContext()); + recyclerView.setLayoutManager(virtualLayoutManager); + RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool(); + recyclerView.setRecycledViewPool(viewPool); + viewPool.setMaxRecycledViews(0, 10); + + //顶部按钮 +// SingleDelegateAdapter buttonAdapter = new SingleDelegateAdapter(R.layout.adapter_button_top_item) { +// @Override +// public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { +// SuperButton superButton = holder.findViewById(R.id.device_item_all_open); +// } +// }; + + // 设备 + FragmentActivity activity=this.getActivity(); + mNewsAdapter = new BroccoliSimpleDelegateAdapter(R.layout.adapter_device_card_view_list_item, new StaggeredGridLayoutHelper(2,0), DemoDataProvider.getEmptyNewInfo()) { + @Override + protected void onBindData(RecyclerViewHolder holder, NewInfo model, int position) { + + //设置item宽度,适配屏幕分辨率 +// CardView view=holder.findViewById(R.id.device_item_card_view); +// int widthPixels = getScreenWidth(activity); +// int space=dip2px(40); //间隙=左边距+右边距+中间间隔 +// ViewGroup.LayoutParams cardViewParams=view.getLayoutParams(); +// cardViewParams.width=(widthPixels-space)/2; + + //设置开关按钮 + SwitchIconView switchIconView=holder.findViewById(R.id.device_item_switch_button); + holder.click(R.id.device_item_switch_button, v -> { + Vibrator vibrator = (Vibrator) activity.getSystemService(activity.VIBRATOR_SERVICE); + vibrator.vibrate(100); + switchIconView.switchState(); + }); + + AppCompatImageView stateView=holder.findViewById(R.id.device_item_state_icon); + stateView.setImageDrawable(getResources().getDrawable((R.drawable.state_a))); + + + if (model != null) { +// holder.text(R.id.tv_user_name, model.getUserName()); +// holder.text(R.id.tv_tag, model.getTag()); +// holder.text(R.id.tv_title, model.getTitle()); +// holder.text(R.id.tv_summary, model.getSummary()); +// holder.text(R.id.tv_praise, model.getPraise() == 0 ? "点赞" : String.valueOf(model.getPraise())); +// holder.text(R.id.tv_comment, model.getComment() == 0 ? "评论" : String.valueOf(model.getComment())); +// holder.text(R.id.tv_read, "阅读量 " + model.getRead()); +// holder.image(R.id.iv_image, model.getImageUrl()); +// +// holder.click(R.id.card_view, v -> Utils.goWeb(getContext(), model.getDetailUrl())); + } + } + + @Override + protected void onBindBroccoli(RecyclerViewHolder holder, Broccoli broccoli) { +// broccoli.addPlaceholders( +// holder.findView(R.id.device_item_title), +// holder.findView(R.id.device_item_title_icon), +// holder.findView(R.id.device_item_time), +// holder.findView(R.id.device_item_time_icon), +// holder.findView(R.id.device_item_temperature), +// holder.findView(R.id.device_item_humidity), +// holder.findView(R.id.device_item_wifi), +// holder.findView(R.id.device_item_wifi_icon), +// holder.findView(R.id.device_item_state), +// holder.findView(R.id.device_item_state_icon), +// holder.findView(R.id.device_item_switch_button) +// ); + } + }; + + DelegateAdapter delegateAdapter = new DelegateAdapter(virtualLayoutManager); + delegateAdapter.addAdapter(mNewsAdapter); + recyclerView.setAdapter(delegateAdapter); + + + //下拉刷新 + refreshLayout.setOnRefreshListener(refreshLayout -> { + // TODO: 2020-02-25 这里只是模拟了网络请求 + refreshLayout.getLayout().postDelayed(() -> { + mNewsAdapter.refresh(DemoDataProvider.getDemoNewInfos()); + refreshLayout.finishRefresh(); + }, 1000); + }); + //上拉加载 + refreshLayout.setOnLoadMoreListener(refreshLayout -> { + // TODO: 2020-02-25 这里只是模拟了网络请求 + refreshLayout.getLayout().postDelayed(() -> { + mNewsAdapter.loadMore(DemoDataProvider.getDemoNewInfos()); + refreshLayout.finishLoadMore(); + }, 1000); + }); + refreshLayout.autoRefresh();//第一次进入触发自动刷新,演示效果 + } + + + @Override + public void onDestroyView() { + if (mUnbinder != null) { + mUnbinder.unbind(); + } + super.onDestroyView(); + Log.e(TAG, "onDestroyView:" + title); + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/news/NewsFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/news/NewsFragment.java new file mode 100644 index 00000000..21358614 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/news/NewsFragment.java @@ -0,0 +1,173 @@ +package com.kerwin.wumei.fragment.news; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alibaba.android.vlayout.DelegateAdapter; +import com.alibaba.android.vlayout.VirtualLayoutManager; +import com.alibaba.android.vlayout.layout.GridLayoutHelper; +import com.alibaba.android.vlayout.layout.LinearLayoutHelper; +import com.kerwin.wumei.adapter.base.broccoli.BroccoliSimpleDelegateAdapter; +import com.kerwin.wumei.adapter.base.delegate.SimpleDelegateAdapter; +import com.kerwin.wumei.adapter.base.delegate.SingleDelegateAdapter; +import com.kerwin.wumei.adapter.entity.NewInfo; +import com.kerwin.wumei.core.BaseFragment; +import com.scwang.smartrefresh.layout.SmartRefreshLayout; +import com.kerwin.wumei.R; +import com.kerwin.wumei.utils.DemoDataProvider; +import com.kerwin.wumei.utils.Utils; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder; +import com.xuexiang.xui.adapter.simple.AdapterItem; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.banner.widget.banner.SimpleImageBanner; +import com.xuexiang.xui.widget.imageview.ImageLoader; +import com.xuexiang.xui.widget.imageview.RadiusImageView; + +import butterknife.BindView; +import me.samlss.broccoli.Broccoli; + +@Page(anim = CoreAnim.none) +public class NewsFragment extends BaseFragment { + + @BindView(R.id.recyclerView) + RecyclerView recyclerView; + @BindView(R.id.refreshLayout) + SmartRefreshLayout refreshLayout; + + private SimpleDelegateAdapter mNewsAdapter; + + /** + * @return 返回为 null意为不需要导航栏 + */ + @Override + protected TitleBar initTitle() { + return null; + } + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_news; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + VirtualLayoutManager virtualLayoutManager = new VirtualLayoutManager(getContext()); + recyclerView.setLayoutManager(virtualLayoutManager); + RecyclerView.RecycledViewPool viewPool = new RecyclerView.RecycledViewPool(); + recyclerView.setRecycledViewPool(viewPool); + viewPool.setMaxRecycledViews(0, 10); + + //轮播条 + SingleDelegateAdapter bannerAdapter = new SingleDelegateAdapter(R.layout.include_head_view_banner) { + @Override + public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { + SimpleImageBanner banner = holder.findViewById(R.id.sib_simple_usage); + banner.setSource(DemoDataProvider.getBannerList()) + .setOnItemClickListener((view, item, position1) -> XToastUtils.toast("headBanner position--->" + position1)).startScroll(); + } + }; + + //九宫格菜单 + GridLayoutHelper gridLayoutHelper = new GridLayoutHelper(4); + gridLayoutHelper.setPadding(0, 16, 0, 0); + gridLayoutHelper.setVGap(10); + gridLayoutHelper.setHGap(0); + SimpleDelegateAdapter commonAdapter = new SimpleDelegateAdapter(R.layout.adapter_common_grid_item, gridLayoutHelper, DemoDataProvider.getGridItems(getContext())) { + @Override + protected void bindData(@NonNull RecyclerViewHolder holder, int position, AdapterItem item) { + if (item != null) { + RadiusImageView imageView = holder.findViewById(R.id.riv_item); + imageView.setCircle(true); + ImageLoader.get().loadImage(imageView, item.getIcon()); + holder.text(R.id.device_item_title, item.getTitle().toString().substring(0, 1)); + holder.text(R.id.tv_sub_title, item.getTitle()); + + holder.click(R.id.ll_container, v -> XToastUtils.toast("点击了:" + item.getTitle())); + } + } + }; + + //动态的标题 + SingleDelegateAdapter titleAdapter = new SingleDelegateAdapter(R.layout.adapter_title_item) { + @Override + public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) { + holder.text(R.id.device_item_title, "动态"); + holder.text(R.id.tv_action, "更多"); + holder.click(R.id.tv_action, v -> XToastUtils.toast("更多")); + } + }; + + // 动态 + mNewsAdapter = new BroccoliSimpleDelegateAdapter(R.layout.adapter_news_card_view_list_item, new LinearLayoutHelper(), DemoDataProvider.getEmptyNewInfo()) { + @Override + protected void onBindData(RecyclerViewHolder holder, NewInfo model, int position) { + if (model != null) { + holder.text(R.id.tv_user_name, model.getUserName()); + holder.text(R.id.tv_tag, model.getTag()); + holder.text(R.id.device_item_title, model.getTitle()); + holder.text(R.id.tv_summary, model.getSummary()); + holder.text(R.id.tv_praise, model.getPraise() == 0 ? "点赞" : String.valueOf(model.getPraise())); + holder.text(R.id.tv_comment, model.getComment() == 0 ? "评论" : String.valueOf(model.getComment())); + holder.text(R.id.tv_read, "阅读量 " + model.getRead()); + holder.image(R.id.iv_image, model.getImageUrl()); + + holder.click(R.id.card_view, v -> Utils.goWeb(getContext(), model.getDetailUrl())); + } + } + + @Override + protected void onBindBroccoli(RecyclerViewHolder holder, Broccoli broccoli) { + broccoli.addPlaceholders( + holder.findView(R.id.tv_user_name), + holder.findView(R.id.tv_tag), + holder.findView(R.id.device_item_title), + holder.findView(R.id.tv_summary), + holder.findView(R.id.tv_praise), + holder.findView(R.id.tv_comment), + holder.findView(R.id.tv_read), + holder.findView(R.id.iv_image) + ); + } + }; + + DelegateAdapter delegateAdapter = new DelegateAdapter(virtualLayoutManager); + delegateAdapter.addAdapter(bannerAdapter); + delegateAdapter.addAdapter(commonAdapter); + delegateAdapter.addAdapter(titleAdapter); + delegateAdapter.addAdapter(mNewsAdapter); + + recyclerView.setAdapter(delegateAdapter); + } + + @Override + protected void initListeners() { + //下拉刷新 + refreshLayout.setOnRefreshListener(refreshLayout -> { + // TODO: 2020-02-25 这里只是模拟了网络请求 + refreshLayout.getLayout().postDelayed(() -> { + mNewsAdapter.refresh(DemoDataProvider.getDemoNewInfos()); + refreshLayout.finishRefresh(); + }, 1000); + }); + //上拉加载 + refreshLayout.setOnLoadMoreListener(refreshLayout -> { + // TODO: 2020-02-25 这里只是模拟了网络请求 + refreshLayout.getLayout().postDelayed(() -> { + mNewsAdapter.loadMore(DemoDataProvider.getDemoNewInfos()); + refreshLayout.finishLoadMore(); + }, 1000); + }); + refreshLayout.autoRefresh();//第一次进入触发自动刷新,演示效果 + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/fragment/profile/ProfileFragment.java b/android/app/src/main/java/com/kerwin/wumei/fragment/profile/ProfileFragment.java new file mode 100644 index 00000000..50a7bc22 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/fragment/profile/ProfileFragment.java @@ -0,0 +1,88 @@ +package com.kerwin.wumei.fragment.profile; + +import android.graphics.drawable.ColorDrawable; + +import com.kerwin.wumei.core.BaseFragment; +import com.kerwin.wumei.R; +import com.kerwin.wumei.fragment.AboutFragment; +import com.kerwin.wumei.fragment.FeedbackFragment; +import com.kerwin.wumei.fragment.MessageFragment; +import com.kerwin.wumei.fragment.SettingsFragment; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xpage.annotation.Page; +import com.xuexiang.xpage.enums.CoreAnim; +import com.xuexiang.xpage.utils.Utils; +import com.xuexiang.xui.widget.actionbar.TitleBar; +import com.xuexiang.xui.widget.imageview.RadiusImageView; +import com.xuexiang.xui.widget.textview.supertextview.SuperTextView; + +import butterknife.BindView; + +@Page(anim = CoreAnim.none) +public class ProfileFragment extends BaseFragment implements SuperTextView.OnSuperTextViewClickListener { + @BindView(R.id.riv_head_pic) + RadiusImageView rivHeadPic; + @BindView(R.id.menu_settings) + SuperTextView menuSettings; + @BindView(R.id.menu_about) + SuperTextView menuAbout; + @BindView(R.id.menu_feedback) + SuperTextView menuFeedback; + @BindView(R.id.menu_message) + SuperTextView menuMessage; + + /** + * @return 返回为 null意为不需要导航栏 + */ + @Override + protected TitleBar initTitle() { + return null; + } + + /** + * 布局的资源id + * + * @return + */ + @Override + protected int getLayoutId() { + return R.layout.fragment_profile; + } + + /** + * 初始化控件 + */ + @Override + protected void initViews() { + + } + + @Override + protected void initListeners() { + menuSettings.setOnSuperTextViewClickListener(this); + menuAbout.setOnSuperTextViewClickListener(this); + menuFeedback.setOnSuperTextViewClickListener(this); + menuMessage.setOnSuperTextViewClickListener(this); + } + + @SingleClick + @Override + public void onClick(SuperTextView view) { + switch(view.getId()) { + case R.id.menu_settings: + openNewPage(SettingsFragment.class); + break; + case R.id.menu_about: + openNewPage(AboutFragment.class); + break; + case R.id.menu_message: + openNewPage(MessageFragment.class); + break; + case R.id.menu_feedback: + openNewPage(FeedbackFragment.class); + break; + default: + break; + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/DemoDataProvider.java b/android/app/src/main/java/com/kerwin/wumei/utils/DemoDataProvider.java new file mode 100644 index 00000000..9597d62f --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/DemoDataProvider.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import com.kerwin.wumei.adapter.entity.NewInfo; +import com.kerwin.wumei.R; +import com.xuexiang.xaop.annotation.MemoryCache; +import com.xuexiang.xui.adapter.simple.AdapterItem; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.widget.banner.widget.banner.BannerItem; + +import java.util.ArrayList; +import java.util.List; + +/** + * 演示数据 + * + * @author xuexiang + * @since 2018/11/23 下午5:52 + */ +public class DemoDataProvider { + + public static String[] titles = new String[]{ + "伪装者:胡歌演绎'痞子特工'", + "无心法师:生死离别!月牙遭虐杀", + "花千骨:尊上沦为花千骨", + "综艺饭:胖轩偷看夏天洗澡掀波澜", + "碟中谍4:阿汤哥高塔命悬一线,超越不可能", + }; + + public static String[] urls = new String[]{//640*360 360/640=0.5625 + "http://photocdn.sohu.com/tvmobilemvms/20150907/144160323071011277.jpg",//伪装者:胡歌演绎"痞子特工" + "http://photocdn.sohu.com/tvmobilemvms/20150907/144158380433341332.jpg",//无心法师:生死离别!月牙遭虐杀 + "http://photocdn.sohu.com/tvmobilemvms/20150907/144160286644953923.jpg",//花千骨:尊上沦为花千骨 + "http://photocdn.sohu.com/tvmobilemvms/20150902/144115156939164801.jpg",//综艺饭:胖轩偷看夏天洗澡掀波澜 + "http://photocdn.sohu.com/tvmobilemvms/20150907/144159406950245847.jpg",//碟中谍4:阿汤哥高塔命悬一线,超越不可能 + }; + + @MemoryCache + public static List getBannerList() { + List list = new ArrayList<>(); + for (int i = 0; i < urls.length; i++) { + BannerItem item = new BannerItem(); + item.imgUrl = urls[i]; + item.title = titles[i]; + + list.add(item); + } + return list; + } + + /** + * 用于占位的空信息 + * + * @return + */ + @MemoryCache + public static List getDemoNewInfos() { + List list = new ArrayList<>(); + list.add(new NewInfo("源码", "Android源码分析--Android系统启动") + .setSummary("其实Android系统的启动最主要的内容无非是init、Zygote、SystemServer这三个进程的启动,他们一起构成的铁三角是Android系统的基础。") + .setDetailUrl("https://juejin.im/post/5c6fc0cdf265da2dda694f05") + .setImageUrl("https://user-gold-cdn.xitu.io/2019/2/22/16914891cd8a950a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + + list.add(new NewInfo("Android UI", "XUI 一个简洁而优雅的Android原生UI框架,解放你的双手") + .setSummary("涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、ProgressBar、LoadingView、StateLayout、FlowLayout、Switch、Actionbar、TabBar、Banner、GuideView、BadgeView、MarqueeView、WebView、SearchView等一系列的组件和丰富多彩的样式主题。\n") + .setDetailUrl("https://juejin.im/post/5c3ed1dae51d4543805ea48d") + .setImageUrl("https://user-gold-cdn.xitu.io/2019/1/16/1685563ae5456408?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + + list.add(new NewInfo("面试", "写给即将面试的你") + .setSummary("最近由于公司业务发展,需要招聘技术方面的人才,由于我在技术方面比较熟悉,技术面的任务就交给我了。今天我要分享的就和面试有关,主要包含技术面的流程、经验和建议,避免大家在今后的面试过程中走一些弯路,帮助即将需要跳槽面试的人。") + .setDetailUrl("https://juejin.im/post/5ca4df966fb9a05e4e58320c") + .setImageUrl("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554629219186&di=6cdab5cfceaae1f7e6d78dbe79104c9f&imgtype=0&src=http%3A%2F%2Fimg.qinxue365.com%2Fuploads%2Fallimg%2F1902%2F4158-1Z22FZ64E00.jpg")); + + list.add(new NewInfo("Android", "XUpdate 一个轻量级、高可用性的Android版本更新框架") + .setSummary("XUpdate 一个轻量级、高可用性的Android版本更新框架。本框架借鉴了AppUpdate中的部分思想和UI界面,将版本更新中的各部分环节抽离出来,形成了如下几个部分:") + .setDetailUrl("https://juejin.im/post/5b480b79e51d45190905ef44") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/7/13/16492d9b7877dc21?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + list.add(new NewInfo("Android/HTTP", "XHttp2 一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp进行组装") + .setSummary("一个功能强悍的网络请求库,使用RxJava2 + Retrofit2 + OKHttp组合进行封装。还不赶紧点击使用说明文档,体验一下吧!") + .setDetailUrl("https://juejin.im/post/5b6b9b49e51d4576b828978d") + .setImageUrl("https://user-gold-cdn.xitu.io/2018/8/9/1651c568a7e30e02?imageView2/0/w/1280/h/960/format/webp/ignore-error/1")); + return list; + } + + public static List getGridItems(Context context) { + return getGridItems(context, R.array.grid_titles_entry, R.array.grid_icons_entry); + } + + + private static List getGridItems(Context context, int titleArrayId, int iconArrayId) { + List list = new ArrayList<>(); + String[] titles = ResUtils.getStringArray(titleArrayId); + Drawable[] icons = ResUtils.getDrawableArray(context, iconArrayId); + for (int i = 0; i < titles.length; i++) { + list.add(new AdapterItem(titles[i], icons[i])); + } + return list; + } + + /** + * 用于占位的空信息 + * + * @return + */ + @MemoryCache + public static List getEmptyNewInfo() { + List list = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + list.add(new NewInfo()); + } + return list; + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/MMKVUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/MMKVUtils.java new file mode 100644 index 00000000..c0a7acc3 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/MMKVUtils.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + + +import android.content.Context; +import android.os.Parcelable; + +import com.tencent.mmkv.MMKV; + +import java.util.Set; + +/** + * MMKV工具类 + * + * @author xuexiang + * @since 2019-07-04 10:20 + */ +public final class MMKVUtils { + + private MMKVUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static MMKV sMMKV; + + /** + * 初始化 + * + * @param context + */ + public static void init(Context context) { + MMKV.initialize(context.getApplicationContext()); + sMMKV = MMKV.defaultMMKV(); + } + + public static MMKV getsMMKV() { + if (sMMKV == null) { + sMMKV = MMKV.defaultMMKV(); + } + return sMMKV; + } + + //=======================================键值保存==================================================// + + /** + * 保存键值 + * + * @param key + * @param value + * @return + */ + public static boolean put(String key, Object value) { + if (value instanceof Integer) { + return getsMMKV().encode(key, (Integer) value); + } else if (value instanceof Float) { + return getsMMKV().encode(key, (Float) value); + } else if (value instanceof String) { + return getsMMKV().encode(key, (String) value); + } else if (value instanceof Boolean) { + return getsMMKV().encode(key, (Boolean) value); + } else if (value instanceof Long) { + return getsMMKV().encode(key, (Long) value); + } else if (value instanceof Double) { + return getsMMKV().encode(key, (Double) value); + } else if (value instanceof Parcelable) { + return getsMMKV().encode(key, (Parcelable) value); + } else if (value instanceof byte[]) { + return getsMMKV().encode(key, (byte[]) value); + } else if (value instanceof Set) { + return getsMMKV().encode(key, (Set) value); + } + return false; + } + + + //=======================================键值获取==================================================// + + /** + * 获取键值 + * + * @param key + * @param defaultValue + * @return + */ + public static Object get(String key, Object defaultValue) { + if (defaultValue instanceof Integer) { + return getsMMKV().decodeInt(key, (Integer) defaultValue); + } else if (defaultValue instanceof Float) { + return getsMMKV().decodeFloat(key, (Float) defaultValue); + } else if (defaultValue instanceof String) { + return getsMMKV().decodeString(key, (String) defaultValue); + } else if (defaultValue instanceof Boolean) { + return getsMMKV().decodeBool(key, (Boolean) defaultValue); + } else if (defaultValue instanceof Long) { + return getsMMKV().decodeLong(key, (Long) defaultValue); + } else if (defaultValue instanceof Double) { + return getsMMKV().decodeDouble(key, (Double) defaultValue); + } else if (defaultValue instanceof byte[]) { + return getsMMKV().decodeBytes(key); + } else if (defaultValue instanceof Set) { + return getsMMKV().decodeStringSet(key, (Set) defaultValue); + } + return null; + } + + + /** + * 根据key获取boolean值 + * + * @param key + * @param defValue + * @return + */ + public static boolean getBoolean(String key, boolean defValue) { + try { + return getsMMKV().getBoolean(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + /** + * 根据key获取long值 + * + * @param key + * @param defValue + * @return + */ + public static long getLong(String key, long defValue) { + try { + return getsMMKV().getLong(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + /** + * 根据key获取float值 + * + * @param key + * @param defValue + * @return + */ + public static float getFloat(String key, float defValue) { + try { + return getsMMKV().getFloat(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + /** + * 根据key获取String值 + * + * @param key + * @param defValue + * @return + */ + public static String getString(String key, String defValue) { + try { + return getsMMKV().getString(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + /** + * 根据key获取int值 + * + * @param key + * @param defValue + * @return + */ + public static int getInt(String key, int defValue) { + try { + return getsMMKV().getInt(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + + /** + * 根据key获取double值 + * + * @param key + * @param defValue + * @return + */ + public static double getDouble(String key, double defValue) { + try { + return getsMMKV().decodeDouble(key, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + + /** + * 获取对象 + * + * @param key + * @param tClass 类型 + * @param + * @return + */ + public static T getObject(String key, Class tClass) { + return getsMMKV().decodeParcelable(key, tClass); + } + + /** + * 获取对象 + * + * @param key + * @param tClass 类型 + * @param + * @return + */ + public static T getObject(String key, Class tClass, T defValue) { + try { + return getsMMKV().decodeParcelable(key, tClass, defValue); + } catch (Exception e) { + e.printStackTrace(); + } + return defValue; + } + + + /** + * 判断键值对是否存在 + * + * @param key 键 + * @return 键值对是否存在 + */ + public static boolean containsKey(String key) { + return getsMMKV().containsKey(key); + } + + /** + * 清除指定键值对 + * + * @param key 键 + */ + public static void remove(String key) { + getsMMKV().remove(key).apply(); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/NetUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/NetUtils.java new file mode 100644 index 00000000..21cbff26 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/NetUtils.java @@ -0,0 +1,147 @@ +package com.kerwin.wumei.utils; + +import android.net.DhcpInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; + +public class NetUtils { + public static boolean isWifiConnected(WifiManager wifiManager) { + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + return wifiInfo != null + && wifiInfo.getNetworkId() != -1 + && !"".equals(wifiInfo.getSSID()); + } + + public static byte[] getRawSsidBytes(WifiInfo info) { + try { + Method method = info.getClass().getMethod("getWifiSsid"); + method.setAccessible(true); + Object wifiSsid = method.invoke(info); + if (wifiSsid == null) { + return null; + } + method = wifiSsid.getClass().getMethod("getOctets"); + method.setAccessible(true); + return (byte[]) method.invoke(wifiSsid); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | NullPointerException e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] getRawSsidBytesOrElse(WifiInfo info, byte[] orElse) { + byte[] raw = getRawSsidBytes(info); + return raw != null ? raw : orElse; + } + + public static String getSsidString(WifiInfo info) { + String ssid = info.getSSID(); + if (ssid.startsWith("\"") && ssid.endsWith("\"")) { + ssid = ssid.substring(1, ssid.length() - 1); + } + return ssid; + } + + public static InetAddress getBroadcastAddress(WifiManager wifi) { + DhcpInfo dhcp = wifi.getDhcpInfo(); + if (dhcp != null) { + int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask; + byte[] quads = new byte[4]; + for (int k = 0; k < 4; k++) { + quads[k] = (byte) ((broadcast >> k * 8) & 0xFF); + } + try { + return InetAddress.getByAddress(quads); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + + try { + return InetAddress.getByName("255.255.255.255"); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + // Impossible arrive here + return null; + } + + public static boolean is5G(int frequency) { + return frequency > 4900 && frequency < 5900; + } + + public static InetAddress getAddress(int ipAddress) { + byte[] ip = new byte[]{ + (byte) (ipAddress & 0xff), + (byte) ((ipAddress >> 8) & 0xff), + (byte) ((ipAddress >> 16) & 0xff), + (byte) ((ipAddress >> 24) & 0xff) + }; + + try { + return InetAddress.getByAddress(ip); + } catch (UnknownHostException e) { + e.printStackTrace(); + // Impossible arrive here + return null; + } + } + + private static InetAddress getAddress(boolean isIPv4) { + try { + Enumeration enums = NetworkInterface.getNetworkInterfaces(); + while (enums.hasMoreElements()) { + NetworkInterface ni = enums.nextElement(); + Enumeration addrs = ni.getInetAddresses(); + while (addrs.hasMoreElements()) { + InetAddress address = addrs.nextElement(); + if (!address.isLoopbackAddress()) { + if (isIPv4 && address instanceof Inet4Address) { + return address; + } + if (!isIPv4 && address instanceof Inet6Address) { + return address; + } + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return null; + } + + public static InetAddress getIPv4Address() { + return getAddress(true); + } + + public static InetAddress getIPv6Address() { + return getAddress(false); + } + + /** + * @param bssid the bssid like aa:bb:cc:dd:ee:ff + * @return byte array converted from bssid + */ + public static byte[] convertBssid2Bytes(String bssid) { + String[] bssidSplits = bssid.split(":"); + if (bssidSplits.length != 6) { + throw new IllegalArgumentException("Invalid bssid format"); + } + byte[] result = new byte[bssidSplits.length]; + for (int i = 0; i < bssidSplits.length; i++) { + result[i] = (byte) Integer.parseInt(bssidSplits[i], 16); + } + return result; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/RandomUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/RandomUtils.java new file mode 100644 index 00000000..44833c4f --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/RandomUtils.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ +package com.kerwin.wumei.utils; + +import android.graphics.Color; +import android.text.TextUtils; + +import java.util.Random; + +/** + *

+ *     desc   : Random Utils
+ *     author : xuexiang
+ *     time   : 2018/4/28 上午12:41
+ * 
+ *
    + * Shuffling algorithm + *
  • {@link #shuffle(Object[])} Shuffling algorithm, Randomly permutes the specified array using a default source of + * randomness
  • + *
  • {@link #shuffle(Object[], int)} Shuffling algorithm, Randomly permutes the specified array
  • + *
  • {@link #shuffle(int[])} Shuffling algorithm, Randomly permutes the specified int array using a default source of + * randomness
  • + *
  • {@link #shuffle(int[], int)} Shuffling algorithm, Randomly permutes the specified int array
  • + *
+ *
    + * get random int + *
  • {@link #getRandom(int)} get random int between 0 and max
  • + *
  • {@link #getRandom(int, int)} get random int between min and max
  • + *
+ *
    + * get random numbers or letters + *
  • {@link #getRandomCapitalLetters(int)} get a fixed-length random string, its a mixture of uppercase letters
  • + *
  • {@link #getRandomLetters(int)} get a fixed-length random string, its a mixture of uppercase and lowercase letters + *
  • + *
  • {@link #getRandomLowerCaseLetters(int)} get a fixed-length random string, its a mixture of lowercase letters
  • + *
  • {@link #getRandomNumbers(int)} get a fixed-length random string, its a mixture of numbers
  • + *
  • {@link #getRandomNumbersAndLetters(int)} get a fixed-length random string, its a mixture of uppercase, lowercase + * letters and numbers
  • + *
  • {@link #getRandom(String, int)} get a fixed-length random string, its a mixture of chars in source
  • + *
  • {@link #getRandom(char[], int)} get a fixed-length random string, its a mixture of chars in sourceChar
  • + *
+ * + */ +public final class RandomUtils { + + public static final String NUMBERS_AND_LETTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public static final String NUMBERS = "0123456789"; + public static final String LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public static final String CAPITAL_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public static final String LOWER_CASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"; + + /** + * Don't let anyone instantiate this class. + */ + private RandomUtils() { + throw new Error("Do not need instantiate!"); + } + + /** + * 在数字和英文字母中获取一个定长的随机字符串 + * + * @param length 长度 + * @return 随机字符串 + * @see RandomUtils#getRandom(String source, int length) + */ + public static String getRandomNumbersAndLetters(int length) { + return getRandom(NUMBERS_AND_LETTERS, length); + } + + /** + * 在数字中获取一个定长的随机字符串 + * + * @param length 长度 + * @return 随机数字符串 + * @see RandomUtils#getRandom(String source, int length) + */ + public static String getRandomNumbers(int length) { + return getRandom(NUMBERS, length); + } + + /** + * 在英文字母中获取一个定长的随机字符串 + * + * @param length 长度 + * @return 随机字母字符串 + * @see RandomUtils#getRandom(String source, int length) + */ + public static String getRandomLetters(int length) { + return getRandom(LETTERS, length); + } + + /** + * 在大写英文字母中获取一个定长的随机字符串 + * + * @param length 长度 + * @return 随机字符串 只包含大写字母 + * @see RandomUtils#getRandom(String source, int length) + */ + public static String getRandomCapitalLetters(int length) { + return getRandom(CAPITAL_LETTERS, length); + } + + /** + * 在小写英文字母中获取一个定长的随机字符串 + * + * @param length 长度 + * @return 随机字符串 只包含小写字母 + * @see RandomUtils#getRandom(String source, int length) + */ + public static String getRandomLowerCaseLetters(int length) { + return getRandom(LOWER_CASE_LETTERS, length); + } + + /** + * 在一个字符数组源中获取一个定长的随机字符串 + * + * @param source 源字符串 + * @param length 长度 + * @return
    + *
  • if source is null or empty, return null
  • + *
  • else see {@link RandomUtils#getRandom(char[] sourceChar, int length)}
  • + *
+ */ + public static String getRandom(String source, int length) { + return TextUtils.isEmpty(source) ? null : getRandom(source.toCharArray(), length); + } + + /** + * 在一个字符数组源中获取一个定长的随机字符串 + * + * @param sourceChar 字符数组源 + * @param length 长度 + * @return
    + *
  • if sourceChar is null or empty, return null
  • + *
  • if length less than 0, return null
  • + *
+ */ + public static String getRandom(char[] sourceChar, int length) { + if (sourceChar == null || sourceChar.length == 0 || length < 0) { + return null; + } + + StringBuilder str = new StringBuilder(length); + Random random = new Random(); + for (int i = 0; i < length; i++) { + str.append(sourceChar[random.nextInt(sourceChar.length)]); + } + 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
  • + *
+ */ + public static int getRandom(int max) { + return 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
  • + *
+ */ + public static int getRandom(int min, int max) { + if (min > max) { + return 0; + } + if (min == max) { + return min; + } + return min + new Random().nextInt(max - min); + } + + /** + * 获取随机颜色 + * + * @return + */ + public static int getRandomColor() { + Random random = new Random(); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return Color.rgb(r, g, b); + } + + /** + * 随机打乱数组中的内容 + * + * @param objArray + * @return + */ + public static boolean shuffle(Object[] objArray) { + if (objArray == null) { + return false; + } + + return shuffle(objArray, getRandom(objArray.length)); + } + + /** + * 随机打乱数组中的内容 + * + * @param objArray + * @param shuffleCount + * @return + */ + public static boolean shuffle(Object[] objArray, int shuffleCount) { + int length; + if (objArray == null || shuffleCount < 0 || (length = objArray.length) < shuffleCount) { + return false; + } + + for (int i = 1; i <= shuffleCount; i++) { + int random = getRandom(length - i); + Object temp = objArray[length - i]; + objArray[length - i] = objArray[random]; + objArray[random] = temp; + } + return true; + } + + /** + * 随机打乱数组中的内容 + * + * @param intArray + * @return + */ + public static int[] shuffle(int[] intArray) { + if (intArray == null) { + return null; + } + + return shuffle(intArray, getRandom(intArray.length)); + } + + /** + * 随机打乱数组中的内容 + * + * @param intArray + * @param shuffleCount + * @return + */ + public static int[] shuffle(int[] intArray, int shuffleCount) { + int length; + if (intArray == null || shuffleCount < 0 || (length = intArray.length) < shuffleCount) { + return null; + } + + int[] out = new int[shuffleCount]; + for (int i = 1; i <= shuffleCount; i++) { + int random = getRandom(length - i); + out[i - 1] = intArray[random]; + int temp = intArray[length - i]; + intArray[length - i] = intArray[random]; + intArray[random] = temp; + } + return out; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/SettingUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/SettingUtils.java new file mode 100644 index 00000000..cab09bb4 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/SettingUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + + +/** + * SharedPreferences管理工具基类 + * + * @author xuexiang + * @since 2018/11/27 下午5:16 + */ +public final class SettingUtils { + + private SettingUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static final String IS_FIRST_OPEN_KEY = "is_first_open_key"; + + private static final String IS_AGREE_PRIVACY_KEY = "is_agree_privacy_key"; + + /** + * 是否是第一次启动 + */ + public static boolean isFirstOpen() { + return MMKVUtils.getBoolean(IS_FIRST_OPEN_KEY, true); + } + + /** + * 设置是否是第一次启动 + */ + public static void setIsFirstOpen(boolean isFirstOpen) { + MMKVUtils.put(IS_FIRST_OPEN_KEY, isFirstOpen); + } + + /** + * @return 是否同意隐私政策 + */ + public static boolean isAgreePrivacy() { + return MMKVUtils.getBoolean(IS_AGREE_PRIVACY_KEY, false); + } + + public static void setIsAgreePrivacy(boolean isAgreePrivacy) { + MMKVUtils.put(IS_AGREE_PRIVACY_KEY, isAgreePrivacy); + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/TokenUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/TokenUtils.java new file mode 100644 index 00000000..b4897847 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/TokenUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + +import android.content.Context; + +import com.kerwin.wumei.activity.LoginActivity; +import com.umeng.analytics.MobclickAgent; +import com.xuexiang.xutil.app.ActivityUtils; +import com.xuexiang.xutil.common.StringUtils; + +/** + * Token管理工具 + * + * @author xuexiang + * @since 2019-11-17 22:37 + */ +public final class TokenUtils { + + private static String sToken; + + private static final String KEY_TOKEN = "com.xuexiang.templateproject.utils.KEY_TOKEN"; + + private TokenUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static final String KEY_PROFILE_CHANNEL = "github"; + + /** + * 初始化Token信息 + */ + public static void init(Context context) { + MMKVUtils.init(context); + sToken = MMKVUtils.getString(KEY_TOKEN, ""); + } + + public static void setToken(String token) { + sToken = token; + MMKVUtils.put(KEY_TOKEN, token); + } + + public static void clearToken() { + sToken = null; + MMKVUtils.remove(KEY_TOKEN); + } + + public static String getToken() { + return sToken; + } + + public static boolean hasToken() { + return MMKVUtils.containsKey(KEY_TOKEN); + } + + /** + * 处理登录成功的事件 + * + * @param token 账户信息 + */ + public static boolean handleLoginSuccess(String token) { + if (!StringUtils.isEmpty(token)) { + XToastUtils.success("登录成功!"); + MobclickAgent.onProfileSignIn(KEY_PROFILE_CHANNEL, token); + setToken(token); + return true; + } else { + XToastUtils.error("登录失败!"); + return false; + } + } + + /** + * 处理登出的事件 + */ + public static void handleLogoutSuccess() { + MobclickAgent.onProfileSignOff(); + //登出时,清除账号信息 + clearToken(); + XToastUtils.success("登出成功!"); + //跳转到登录页 + ActivityUtils.startActivity(LoginActivity.class); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/Utils.java b/android/app/src/main/java/com/kerwin/wumei/utils/Utils.java new file mode 100644 index 00000000..a4298e05 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/Utils.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import com.kerwin.wumei.core.webview.AgentWebActivity; +import com.kerwin.wumei.core.webview.AgentWebFragment; +import com.kerwin.wumei.R; +import com.xuexiang.xui.utils.ResUtils; +import com.xuexiang.xui.widget.dialog.DialogLoader; +import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction; +import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog; +import com.xuexiang.xutil.XUtil; + +/** + * 工具类 + * + * @author xuexiang + * @since 2020-02-23 15:12 + */ +public final class Utils { + + private Utils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 这里填写你的应用隐私政策网页地址 + */ + private static final String PRIVACY_URL = "https://gitee.com/xuexiangjys/TemplateAppProject/raw/master/LICENSE"; + + /** + * 显示隐私政策的提示 + * + * @param context + * @param submitListener 同意的监听 + * @return + */ + public static Dialog showPrivacyDialog(Context context, MaterialDialog.SingleButtonCallback submitListener) { + MaterialDialog dialog = new MaterialDialog.Builder(context).title(R.string.title_reminder).autoDismiss(false).cancelable(false) + .positiveText(R.string.lab_agree).onPositive((dialog1, which) -> { + if (submitListener != null) { + submitListener.onClick(dialog1, which); + } else { + dialog1.dismiss(); + } + }) + .negativeText(R.string.lab_disagree).onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.dismiss(); + DialogLoader.getInstance().showConfirmDialog(context, ResUtils.getString(R.string.title_reminder), String.format(ResUtils.getString(R.string.content_privacy_explain_again), ResUtils.getString(R.string.app_name)), ResUtils.getString(R.string.lab_look_again), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + showPrivacyDialog(context, submitListener); + } + }, ResUtils.getString(R.string.lab_still_disagree), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + DialogLoader.getInstance().showConfirmDialog(context, ResUtils.getString(R.string.content_think_about_it_again), ResUtils.getString(R.string.lab_look_again), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + showPrivacyDialog(context, submitListener); + } + }, ResUtils.getString(R.string.lab_exit_app), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + XUtil.exitApp(); + } + }); + } + }); + } + }).build(); + dialog.setContent(getPrivacyContent(context)); + //开始响应点击事件 + dialog.getContentView().setMovementMethod(LinkMovementMethod.getInstance()); + dialog.show(); + return dialog; + } + + /** + * @return 隐私政策说明 + */ + private static SpannableStringBuilder getPrivacyContent(Context context) { + SpannableStringBuilder stringBuilder = new SpannableStringBuilder() + .append(" 欢迎来到").append(ResUtils.getString(R.string.app_name)).append("!\n") + .append(" 我们深知个人信息对你的重要性,也感谢你对我们的信任。\n") + .append(" 为了更好地保护你的权益,同时遵守相关监管的要求,我们将通过"); + stringBuilder.append(getPrivacyLink(context, PRIVACY_URL)) + .append("向你说明我们会如何收集、存储、保护、使用及对外提供你的信息,并说明你享有的权利。\n") + .append(" 更多详情,敬请查阅") + .append(getPrivacyLink(context, PRIVACY_URL)) + .append("全文。"); + return stringBuilder; + } + + /** + * @param context 隐私政策的链接 + * @return + */ + private static SpannableString getPrivacyLink(Context context, String privacyUrl) { + String privacyName = String.format(ResUtils.getString(R.string.lab_privacy_name), ResUtils.getString(R.string.app_name)); + SpannableString spannableString = new SpannableString(privacyName); + spannableString.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + goWeb(context, privacyUrl); + } + }, 0, privacyName.length(), Spanned.SPAN_MARK_MARK); + return spannableString; + } + + + /** + * 请求浏览器 + * + * @param url + */ + public static void goWeb(Context context, final String url) { + Intent intent = new Intent(context, AgentWebActivity.class); + intent.putExtra(AgentWebFragment.KEY_URL, url); + context.startActivity(intent); + } + + + /** + * 是否是深色的颜色 + * + * @param color + * @return + */ + public static boolean isColorDark(@ColorInt int color) { + double darkness = + 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) + / 255; + return darkness >= 0.382; + } + + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/XToastUtils.java b/android/app/src/main/java/com/kerwin/wumei/utils/XToastUtils.java new file mode 100644 index 00000000..86820b82 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/XToastUtils.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.xuexiang.xui.XUI; +import com.xuexiang.xui.widget.toast.XToast; + +/** + * xtoast 工具类 + * + * @author xuexiang + * @since 2019-06-30 19:04 + */ +public final class XToastUtils { + + + private XToastUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + static { + XToast.Config.get() + .setAlpha(200) + .allowQueue(false); + } + + //======普通土司=======// + + @MainThread + public static void toast(@NonNull CharSequence message) { + XToast.normal(XUI.getContext(), message).show(); + } + + @MainThread + public static void toast(@StringRes int message) { + XToast.normal(XUI.getContext(), message).show(); + } + + @MainThread + public static void toast(@NonNull CharSequence message, int duration) { + XToast.normal(XUI.getContext(), message, duration).show(); + } + + @MainThread + public static void toast(@StringRes int message, int duration) { + XToast.normal(XUI.getContext(), message, duration).show(); + } + + //======错误【红色】=======// + + @MainThread + public static void error(@NonNull Throwable throwable) { + XToast.error(XUI.getContext(), throwable.getMessage()).show(); + } + + @MainThread + public static void error(@NonNull CharSequence message) { + XToast.error(XUI.getContext(), message).show(); + } + + @MainThread + public static void error(@StringRes int message) { + XToast.error(XUI.getContext(), message).show(); + } + + @MainThread + public static void error(@NonNull CharSequence message, int duration) { + XToast.error(XUI.getContext(), message, duration).show(); + } + + @MainThread + public static void error(@StringRes int message, int duration) { + XToast.error(XUI.getContext(), message, duration).show(); + } + + //======成功【绿色】=======// + + @MainThread + public static void success(@NonNull CharSequence message) { + XToast.success(XUI.getContext(), message).show(); + } + + @MainThread + public static void success(@StringRes int message) { + XToast.success(XUI.getContext(), message).show(); + } + + @MainThread + public static void success(@NonNull CharSequence message, int duration) { + XToast.success(XUI.getContext(), message, duration).show(); + } + + @MainThread + public static void success(@StringRes int message, int duration) { + XToast.success(XUI.getContext(), message, duration).show(); + } + + //======信息【蓝色】=======// + + @MainThread + public static void info(@NonNull CharSequence message) { + XToast.info(XUI.getContext(), message).show(); + } + + @MainThread + public static void info(@StringRes int message) { + XToast.info(XUI.getContext(), message).show(); + } + + @MainThread + public static void info(@NonNull CharSequence message, int duration) { + XToast.info(XUI.getContext(), message, duration).show(); + } + + @MainThread + public static void info(@StringRes int message, int duration) { + XToast.info(XUI.getContext(), message, duration).show(); + } + + //=======警告【黄色】======// + + @MainThread + public static void warning(@NonNull CharSequence message) { + XToast.warning(XUI.getContext(), message).show(); + } + + @MainThread + public static void warning(@StringRes int message) { + XToast.warning(XUI.getContext(), message).show(); + } + + @MainThread + public static void warning(@NonNull CharSequence message, int duration) { + XToast.warning(XUI.getContext(), message, duration).show(); + } + + @MainThread + public static void warning(@StringRes int message, int duration) { + XToast.warning(XUI.getContext(), message, duration).show(); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/ANRWatchDogInit.java b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/ANRWatchDogInit.java new file mode 100644 index 00000000..11427a6c --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/ANRWatchDogInit.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.sdkinit; + +import com.github.anrwatchdog.ANRWatchDog; +import com.xuexiang.xutil.common.logger.Logger; + +/** + * ANR看门狗监听器初始化 + * + * @author xuexiang + * @since 2020-02-18 15:08 + */ +public final class ANRWatchDogInit { + + private static final String TAG = "ANRWatchDog"; + + private ANRWatchDogInit() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * ANR看门狗 + */ + private static ANRWatchDog sANRWatchDog; + + /** + * ANR监听触发的时间 + */ + private static final int ANR_DURATION = 4000; + + + /** + * ANR静默处理【就是不处理,直接记录一下日志】 + */ + private final static ANRWatchDog.ANRListener SILENT_LISTENER = error -> Logger.eTag(TAG, error); + + /** + * ANR自定义处理【可以是记录日志用于上传】 + */ + private final static ANRWatchDog.ANRListener CUSTOM_LISTENER = error -> { + Logger.eTag(TAG, "Detected Application Not Responding!", error); + //这里进行ANR的捕获后的操作 + + throw error; + }; + + public static void init() { + //这里设置监听的间隔为2秒 + sANRWatchDog = new ANRWatchDog(2000); + sANRWatchDog.setANRInterceptor(duration -> { + long ret = ANR_DURATION - duration; + if (ret > 0) { + Logger.wTag(TAG, "Intercepted ANR that is too short (" + duration + " ms), postponing for " + ret + " ms."); + } + //当返回是0或者负数时,就会触发ANR监听回调 + return ret; + }).setANRListener(SILENT_LISTENER).start(); + } + + public static ANRWatchDog getANRWatchDog() { + return sANRWatchDog; + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/UMengInit.java b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/UMengInit.java new file mode 100644 index 00000000..6a8b198d --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/UMengInit.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.sdkinit; + +import android.app.Application; +import android.content.Context; + +import com.kerwin.wumei.BuildConfig; +import com.kerwin.wumei.MyApp; +import com.meituan.android.walle.WalleChannelReader; +import com.umeng.analytics.MobclickAgent; +import com.umeng.commonsdk.UMConfigure; + +/** + * UMeng 统计 SDK初始化 + * + * @author xuexiang + * @since 2019-06-18 15:49 + */ +public final class UMengInit { + + private UMengInit() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + private static String DEFAULT_CHANNEL_ID = "github"; + + /** + * 初始化UmengSDK + */ + public static void init(Application application) { + //设置LOG开关,默认为false + UMConfigure.setLogEnabled(MyApp.isDebug()); + //初始化组件化基础库, 注意: 即使您已经在AndroidManifest.xml中配置过appkey和channel值,也需要在App代码中调用初始化接口(如需要使用AndroidManifest.xml中配置好的appkey和channel值,UMConfigure.init调用中appkey和channel参数请置为null)。 + //第二个参数是appkey,最后一个参数是pushSecret + //这里BuildConfig.APP_ID_UMENG是根据local.properties中定义的APP_ID_UMENG生成的,只是运行看效果的话,可以不初始化该SDK + UMConfigure.init(application, BuildConfig.APP_ID_UMENG, getChannel(application), UMConfigure.DEVICE_TYPE_PHONE, ""); + //统计SDK是否支持采集在子进程中打点的自定义事件,默认不支持 + //支持多进程打点 + UMConfigure.setProcessEvent(true); + MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO); + } + + + /** + * 获取渠道信息 + * + * @param context + * @return + */ + public static String getChannel(final Context context) { + return WalleChannelReader.getChannel(context, DEFAULT_CHANNEL_ID); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XBasicLibInit.java b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XBasicLibInit.java new file mode 100644 index 00000000..b7e32a24 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XBasicLibInit.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.sdkinit; + +import android.app.Application; + +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.core.BaseActivity; +import com.kerwin.wumei.utils.TokenUtils; +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xaop.XAOP; +import com.xuexiang.xhttp2.XHttpSDK; +import com.xuexiang.xpage.PageConfig; +import com.xuexiang.xrouter.launcher.XRouter; +import com.xuexiang.xui.XUI; +import com.xuexiang.xutil.XUtil; +import com.xuexiang.xutil.common.StringUtils; + +/** + * X系列基础库初始化 + * + * @author xuexiang + * @since 2019-06-30 23:54 + */ +public final class XBasicLibInit { + + private XBasicLibInit() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 初始化基础库SDK + */ + public static void init(Application application) { + //工具类 + initXUtil(application); + + //网络请求框架 + initXHttp2(application); + + //页面框架 + initXPage(application); + + //切片框架 + initXAOP(application); + + //UI框架 + initXUI(application); + + //路由框架 + initRouter(application); + } + + /** + * 初始化XUtil工具类 + */ + private static void initXUtil(Application application) { + XUtil.init(application); + XUtil.debug(MyApp.isDebug()); + TokenUtils.init(application); + } + + /** + * 初始化XHttp2 + */ + private static void initXHttp2(Application application) { + //初始化网络请求框架,必须首先执行 + XHttpSDK.init(application); + //需要调试的时候执行 + if (MyApp.isDebug()) { + XHttpSDK.debug(); + } +// XHttpSDK.debug(new CustomLoggingInterceptor()); //设置自定义的日志打印拦截器 + //设置网络请求的全局基础地址 + XHttpSDK.setBaseUrl("https://gitee.com/"); +// //设置动态参数添加拦截器 +// XHttpSDK.addInterceptor(new CustomDynamicInterceptor()); +// //请求失效校验拦截器 +// XHttpSDK.addInterceptor(new CustomExpiredInterceptor()); + } + + /** + * 初始化XPage页面框架 + */ + private static void initXPage(Application application) { + PageConfig.getInstance() + .debug(MyApp.isDebug() ? "PageLog" : null) + .setContainActivityClazz(BaseActivity.class) + .init(application); + } + + /** + * 初始化XAOP + */ + private static void initXAOP(Application application) { + XAOP.init(application); + XAOP.debug(MyApp.isDebug()); + //设置动态申请权限切片 申请权限被拒绝的事件响应监听 + XAOP.setOnPermissionDeniedListener(permissionsDenied -> XToastUtils.error("权限申请被拒绝:" + StringUtils.listToString(permissionsDenied, ","))); + } + + /** + * 初始化XUI框架 + */ + private static void initXUI(Application application) { + XUI.init(application); + XUI.debug(MyApp.isDebug()); + } + + /** + * 初始化路由框架 + */ + private static void initRouter(Application application) { + // 这两行必须写在init之前,否则这些配置在init过程中将无效 + if (MyApp.isDebug()) { + XRouter.openLog(); // 打印日志 + XRouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险) + } + XRouter.init(application); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XUpdateInit.java b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XUpdateInit.java new file mode 100644 index 00000000..a092c277 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/sdkinit/XUpdateInit.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.sdkinit; + +import android.app.Application; +import android.content.Context; + +import com.kerwin.wumei.MyApp; +import com.kerwin.wumei.utils.update.CustomUpdateDownloader; +import com.kerwin.wumei.utils.update.CustomUpdateFailureListener; +import com.kerwin.wumei.utils.update.XHttpUpdateHttpServiceImpl; +import com.xuexiang.xupdate.XUpdate; +import com.xuexiang.xupdate.utils.UpdateUtils; + +/** + * XUpdate 版本更新 SDK 初始化 + * + * @author xuexiang + * @since 2019-06-18 15:51 + */ +public final class XUpdateInit { + + private XUpdateInit() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 应用版本更新的检查地址 + */ + private static final String KEY_UPDATE_URL = ""; + + public static void init(Application application) { + XUpdate.get() + .debug(MyApp.isDebug()) + //默认设置只在wifi下检查版本更新 + .isWifiOnly(false) + //默认设置使用get请求检查版本 + .isGet(true) + //默认设置非自动模式,可根据具体使用配置 + .isAutoMode(false) + //设置默认公共请求参数 + .param("versionCode", UpdateUtils.getVersionCode(application)) + .param("appKey", application.getPackageName()) + //这个必须设置!实现网络请求功能。 + .setIUpdateHttpService(new XHttpUpdateHttpServiceImpl()) + .setIUpdateDownLoader(new CustomUpdateDownloader()) + //这个必须初始化 + .init(application); + } + + /** + * 进行版本更新检查 + */ + public static void checkUpdate(Context context, boolean needErrorTip) { + XUpdate.newBuild(context).updateUrl(KEY_UPDATE_URL).update(); + XUpdate.get().setOnUpdateFailureListener(new CustomUpdateFailureListener(needErrorTip)); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/service/JsonSerializationService.java b/android/app/src/main/java/com/kerwin/wumei/utils/service/JsonSerializationService.java new file mode 100644 index 00000000..dbfe7328 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/service/JsonSerializationService.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.service; + +import android.content.Context; + +import com.xuexiang.xrouter.annotation.Router; +import com.xuexiang.xrouter.facade.service.SerializationService; +import com.xuexiang.xutil.net.JsonUtil; + +import java.lang.reflect.Type; + +/** + * @author XUE + * @since 2019/3/27 16:39 + */ +@Router(path = "/service/json") +public class JsonSerializationService implements SerializationService { + /** + * 对象序列化为json + * + * @param instance obj + * @return json string + */ + @Override + public String object2Json(Object instance) { + return JsonUtil.toJson(instance); + } + + /** + * json反序列化为对象 + * + * @param input json string + * @param clazz object type + * @return instance of object + */ + @Override + public T parseObject(String input, Type clazz) { + return JsonUtil.fromJson(input, clazz); + } + + /** + * 进程初始化的方法 + * + * @param context 上下文 + */ + @Override + public void init(Context context) { + + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateDownloader.java b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateDownloader.java new file mode 100644 index 00000000..4cc9fef7 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateDownloader.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.update; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.xuexiang.xupdate.entity.UpdateEntity; +import com.xuexiang.xupdate.proxy.impl.DefaultUpdateDownloader; +import com.xuexiang.xupdate.service.OnFileDownloadListener; +import com.xuexiang.xutil.app.ActivityUtils; + +/** + * 重写DefaultUpdateDownloader,在取消下载时,弹出提示 + * + * @author xuexiang + * @since 2019-06-14 23:47 + */ +public class CustomUpdateDownloader extends DefaultUpdateDownloader { + + private boolean mIsStartDownload; + + @Override + public void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) { + super.startDownload(updateEntity, downloadListener); + mIsStartDownload = true; + + } + + @Override + public void cancelDownload() { + super.cancelDownload(); + if (mIsStartDownload) { + mIsStartDownload = false; + ActivityUtils.startActivity(UpdateTipDialog.class); + } + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateFailureListener.java b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateFailureListener.java new file mode 100644 index 00000000..001d391b --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateFailureListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.update; + +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xupdate.entity.UpdateError; +import com.xuexiang.xupdate.listener.OnUpdateFailureListener; + +/** + * 自定义版本更新提示 + * + * @author xuexiang + * @since 2019/4/15 上午12:01 + */ +public class CustomUpdateFailureListener implements OnUpdateFailureListener { + + /** + * 是否需要错误提示 + */ + private boolean mNeedErrorTip; + + public CustomUpdateFailureListener() { + this(true); + } + + public CustomUpdateFailureListener(boolean needErrorTip) { + mNeedErrorTip = needErrorTip; + } + + /** + * 更新失败 + * + * @param error 错误 + */ + @Override + public void onFailure(UpdateError error) { + if (mNeedErrorTip) { + XToastUtils.error(error); + } + if (error.getCode() == UpdateError.ERROR.DOWNLOAD_FAILED) { + UpdateTipDialog.show("Github被墙无法下载,是否考虑切换蒲公英下载?"); + } + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateParser.java b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateParser.java new file mode 100644 index 00000000..ae504a3d --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/update/CustomUpdateParser.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.update; + +import com.xuexiang.xupdate.entity.UpdateEntity; +import com.xuexiang.xupdate.proxy.impl.AbstractUpdateParser; + +/** + * 版本更新信息自定义json解析器 + * + * @author xuexiang + * @since 2020-02-18 13:01 + */ +public class CustomUpdateParser extends AbstractUpdateParser { + + @Override + public UpdateEntity parseJson(String json) throws Exception { + // TODO: 2020-02-18 这里填写你需要自定义的json格式 + return null; + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/update/UpdateTipDialog.java b/android/app/src/main/java/com/kerwin/wumei/utils/update/UpdateTipDialog.java new file mode 100644 index 00000000..0b907086 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/update/UpdateTipDialog.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.update; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.xuexiang.xui.widget.dialog.DialogLoader; +import com.xuexiang.xupdate.XUpdate; + +/** + * 版本更新提示弹窗 + * + * @author xuexiang + * @since 2019-06-15 00:06 + */ +public class UpdateTipDialog extends AppCompatActivity implements DialogInterface.OnDismissListener { + + public static final String KEY_CONTENT = "com.xuexiang.templateproject.utils.update.KEY_CONTENT"; + + /** + * 显示版本更新重试提示弹窗 + * + * @param content + */ + public static void show(String content) { + Intent intent = new Intent(XUpdate.getContext(), UpdateTipDialog.class); + intent.putExtra(KEY_CONTENT, content); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + XUpdate.getContext().startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String content = getIntent().getStringExtra(KEY_CONTENT); + if (TextUtils.isEmpty(content)) { + content = "Github下载速度太慢了,是否考虑切换蒲公英下载?"; + } + + DialogLoader.getInstance().showConfirmDialog(this, content, "是", (dialog, which) -> { + dialog.dismiss(); +// Utils.goWeb(UpdateTipDialog.this, "这里填写你应用下载页面的链接"); + }, "否") + .setOnDismissListener(this); + + } + + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/utils/update/XHttpUpdateHttpServiceImpl.java b/android/app/src/main/java/com/kerwin/wumei/utils/update/XHttpUpdateHttpServiceImpl.java new file mode 100644 index 00000000..b1bf60cb --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/utils/update/XHttpUpdateHttpServiceImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.utils.update; + +import androidx.annotation.NonNull; + +import com.kerwin.wumei.utils.XToastUtils; +import com.xuexiang.xhttp2.XHttp; +import com.xuexiang.xhttp2.XHttpSDK; +import com.xuexiang.xhttp2.callback.DownloadProgressCallBack; +import com.xuexiang.xhttp2.callback.SimpleCallBack; +import com.xuexiang.xhttp2.exception.ApiException; +import com.xuexiang.xupdate.proxy.IUpdateHttpService; +import com.xuexiang.xutil.file.FileUtils; +import com.xuexiang.xutil.net.JsonUtil; + +import java.util.Map; + +/** + * XHttp2实现的请求更新 + * + * @author xuexiang + * @since 2018/8/12 上午11:46 + */ +public class XHttpUpdateHttpServiceImpl implements IUpdateHttpService { + + @Override + public void asyncGet(@NonNull String url, @NonNull Map params, @NonNull final IUpdateHttpService.Callback callBack) { + XHttp.get(url) + .params(params) + .keepJson(true) + .execute(new SimpleCallBack() { + @Override + public void onSuccess(String response) throws Throwable { + callBack.onSuccess(response); + } + @Override + public void onError(ApiException e) { + callBack.onError(e); + } + }); + } + + @Override + public void asyncPost(@NonNull String url, @NonNull Map params, @NonNull final IUpdateHttpService.Callback callBack) { + XHttp.post(url) + .upJson(JsonUtil.toJson(params)) + .keepJson(true) + .execute(new SimpleCallBack() { + @Override + public void onSuccess(String response) throws Throwable { + callBack.onSuccess(response); + } + + @Override + public void onError(ApiException e) { + callBack.onError(e); + } + }); + } + + @Override + public void download(@NonNull String url, @NonNull String path, @NonNull String fileName, @NonNull final IUpdateHttpService.DownloadCallback callback) { + XHttpSDK.addRequest(url, XHttp.downLoad(url) + .savePath(path) + .saveName(fileName) + .isUseBaseUrl(false) + .execute(new DownloadProgressCallBack() { + @Override + public void onStart() { + callback.onStart(); + } + + @Override + public void onError(ApiException e) { + callback.onError(e); + } + + @Override + public void update(long downLoadSize, long totalSize, boolean done) { + callback.onProgress(downLoadSize / (float) totalSize, totalSize); + } + + @Override + public void onComplete(String path) { + callback.onSuccess(FileUtils.getFileByPath(path)); + } + })); + } + + @Override + public void cancelDownload(@NonNull String url) { + XToastUtils.info("已取消更新"); + XHttpSDK.cancelRequest(url); + } +} diff --git a/android/app/src/main/java/com/kerwin/wumei/widget/GuideTipsDialog.java b/android/app/src/main/java/com/kerwin/wumei/widget/GuideTipsDialog.java new file mode 100644 index 00000000..4e44f4cb --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/widget/GuideTipsDialog.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.widget; + +import android.content.Context; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatCheckBox; + +import com.kerwin.wumei.core.http.api.ApiService; +import com.kerwin.wumei.core.http.callback.NoTipCallBack; +import com.kerwin.wumei.core.http.entity.TipInfo; +import com.kerwin.wumei.utils.MMKVUtils; +import com.xuexiang.constant.TimeConstants; +import com.kerwin.wumei.R; +import com.xuexiang.xaop.annotation.SingleClick; +import com.xuexiang.xhttp2.XHttp; +import com.xuexiang.xhttp2.cache.model.CacheMode; +import com.xuexiang.xhttp2.request.CustomRequest; +import com.xuexiang.xui.widget.dialog.BaseDialog; +import com.xuexiang.xutil.app.AppUtils; +import com.zzhoujay.richtext.RichText; + +import java.util.List; + +/** + * 小贴士弹窗 + * + * @author xuexiang + * @since 2019-08-22 17:02 + */ +public class GuideTipsDialog extends BaseDialog implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { + + private static final String KEY_IS_IGNORE_TIPS = "com.xuexiang.templateproject.widget.key_is_ignore_tips_"; + + private List mTips; + private int mIndex = -1; + + private TextView mTvPrevious; + private TextView mTvNext; + + private TextView mTvTitle; + private TextView mTvContent; + + /** + * 显示提示 + * + * @param context 上下文 + */ + public static void showTips(final Context context) { + if (!isIgnoreTips()) { + CustomRequest request = XHttp.custom().cacheMode(CacheMode.FIRST_CACHE).cacheTime(TimeConstants.DAY).cacheKey("getTips"); + request.apiCall(request.create(ApiService.IGetService.class).getTips(), new NoTipCallBack>() { + @Override + public void onSuccess(List response) throws Throwable { + if (response != null && response.size() > 0) { + new GuideTipsDialog(context, response).show(); + } + } + }); + } + } + + public GuideTipsDialog(Context context, @NonNull List tips) { + super(context, R.layout.dialog_guide_tips); + initViews(); + updateTips(tips); + } + + /** + * 初始化弹窗 + */ + private void initViews() { + mTvTitle = findViewById(R.id.device_item_title); + mTvContent = findViewById(R.id.tv_content); + AppCompatCheckBox cbIgnore = findViewById(R.id.cb_ignore); + ImageView ivClose = findViewById(R.id.iv_close); + + mTvPrevious = findViewById(R.id.tv_previous); + mTvNext = findViewById(R.id.tv_next); + + if (cbIgnore != null) { + cbIgnore.setOnCheckedChangeListener(this); + } + if (ivClose != null) { + ivClose.setOnClickListener(this); + } + mTvPrevious.setOnClickListener(this); + mTvNext.setOnClickListener(this); + mTvPrevious.setEnabled(false); + mTvNext.setEnabled(true); + setCancelable(false); + setCanceledOnTouchOutside(true); + } + + /** + * 更新提示信息 + * + * @param tips 提示信息 + */ + private void updateTips(List tips) { + mTips = tips; + if (mTips != null && mTips.size() > 0 && mTvContent != null) { + mIndex = 0; + showRichText(mTips.get(mIndex)); + } + } + + /** + * 切换提示信息 + * + * @param index 索引 + */ + private void switchTipInfo(int index) { + if (mTips != null && mTips.size() > 0 && mTvContent != null) { + if (index >= 0 && index <= mTips.size() - 1) { + showRichText(mTips.get(index)); + if (index == 0) { + mTvPrevious.setEnabled(false); + mTvNext.setEnabled(true); + } else if (index == mTips.size() - 1) { + mTvPrevious.setEnabled(true); + mTvNext.setEnabled(false); + } else { + mTvPrevious.setEnabled(true); + mTvNext.setEnabled(true); + } + } + } + } + + /** + * 显示富文本 + * + * @param tipInfo 提示信息 + */ + private void showRichText(TipInfo tipInfo) { + mTvTitle.setText(tipInfo.getTitle()); + RichText.fromHtml(tipInfo.getContent()) + .bind(this) + .into(mTvContent); + } + + + @SingleClick(300) + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.iv_close: + dismiss(); + break; + case R.id.tv_previous: + if (mIndex > 0) { + mIndex--; + switchTipInfo(mIndex); + } + break; + case R.id.tv_next: + if (mIndex < mTips.size() - 1) { + mIndex++; + switchTipInfo(mIndex); + } + break; + default: + break; + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setIsIgnoreTips(isChecked); + } + + @Override + public void onDetachedFromWindow() { + RichText.clear(this); + super.onDetachedFromWindow(); + } + + + public static boolean setIsIgnoreTips(boolean isIgnore) { + return MMKVUtils.put(KEY_IS_IGNORE_TIPS + AppUtils.getAppVersionCode(), isIgnore); + } + + public static boolean isIgnoreTips() { + return MMKVUtils.getBoolean(KEY_IS_IGNORE_TIPS + AppUtils.getAppVersionCode(), false); + } + +} diff --git a/android/app/src/main/java/com/kerwin/wumei/widget/MaterialFooter.java b/android/app/src/main/java/com/kerwin/wumei/widget/MaterialFooter.java new file mode 100644 index 00000000..d134f1e4 --- /dev/null +++ b/android/app/src/main/java/com/kerwin/wumei/widget/MaterialFooter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.wumei.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; + +import com.scwang.smartrefresh.layout.api.RefreshFooter; +import com.scwang.smartrefresh.layout.api.RefreshKernel; +import com.scwang.smartrefresh.layout.api.RefreshLayout; +import com.scwang.smartrefresh.layout.constant.RefreshState; +import com.scwang.smartrefresh.layout.constant.SpinnerStyle; +import com.scwang.smartrefresh.layout.util.DensityUtil; + +/** + * Material风格的上拉加载 + * + * @author xuexiang + * @since 2019-08-03 11:14 + */ +public class MaterialFooter extends ProgressBar implements RefreshFooter { + + public MaterialFooter(Context context) { + this(context, null); + } + + public MaterialFooter(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + private void initView() { + setVisibility(GONE); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); + setPadding(0, DensityUtil.dp2px(10), 0, DensityUtil.dp2px(10)); + setLayoutParams(params); + } + + @Override + public boolean setNoMoreData(boolean noMoreData) { + return false; + } + + @NonNull + @Override + public View getView() { + return this; + } + + @NonNull + @Override + public SpinnerStyle getSpinnerStyle() { + //指定为平移,不能null + return SpinnerStyle.Translate; + } + + + @Override + public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) { + setVisibility(VISIBLE); + } + + @Override + public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) { + setVisibility(GONE); + return 100; + } + + @Override + public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) { + + } + + @Override + public void setPrimaryColors(int... colors) { + + } + + @Override + public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) { + + } + + @Override + public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) { + + } + + @Override + public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) { + + } + + @Override + public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) { + + } + + @Override + public boolean isSupportHorizontalDrag() { + return false; + } + +} diff --git a/android/app/src/main/res/color/selector_round_button_main_theme_color.xml b/android/app/src/main/res/color/selector_round_button_main_theme_color.xml new file mode 100644 index 00000000..16fd2c79 --- /dev/null +++ b/android/app/src/main/res/color/selector_round_button_main_theme_color.xml @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/color/selector_tab_text_color.xml b/android/app/src/main/res/color/selector_tab_text_color.xml new file mode 100644 index 00000000..b7628149 --- /dev/null +++ b/android/app/src/main/res/color/selector_tab_text_color.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-hdpi/about.png b/android/app/src/main/res/drawable-hdpi/about.png new file mode 100644 index 00000000..53886015 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/about.png differ diff --git a/android/app/src/main/res/drawable-hdpi/add.png b/android/app/src/main/res/drawable-hdpi/add.png new file mode 100644 index 00000000..c9c4d4e3 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/add.png differ diff --git a/android/app/src/main/res/drawable-hdpi/add_device.png b/android/app/src/main/res/drawable-hdpi/add_device.png new file mode 100644 index 00000000..be4d52be Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/add_device.png differ diff --git a/android/app/src/main/res/drawable-hdpi/add_line.png b/android/app/src/main/res/drawable-hdpi/add_line.png new file mode 100644 index 00000000..57360459 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/add_line.png differ diff --git a/android/app/src/main/res/drawable-hdpi/add_white.png b/android/app/src/main/res/drawable-hdpi/add_white.png new file mode 100644 index 00000000..0b4cf250 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/add_white.png differ diff --git a/android/app/src/main/res/drawable-hdpi/begin.png b/android/app/src/main/res/drawable-hdpi/begin.png new file mode 100644 index 00000000..3b88998e Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/begin.png differ diff --git a/android/app/src/main/res/drawable-hdpi/category.png b/android/app/src/main/res/drawable-hdpi/category.png new file mode 100644 index 00000000..496f8c04 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/category.png differ diff --git a/android/app/src/main/res/drawable-hdpi/device.png b/android/app/src/main/res/drawable-hdpi/device.png new file mode 100644 index 00000000..90278fc7 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/device.png differ diff --git a/android/app/src/main/res/drawable-hdpi/down.png b/android/app/src/main/res/drawable-hdpi/down.png new file mode 100644 index 00000000..90a06fb1 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/down.png differ diff --git a/android/app/src/main/res/drawable-hdpi/group.png b/android/app/src/main/res/drawable-hdpi/group.png new file mode 100644 index 00000000..4e664560 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/group.png differ diff --git a/android/app/src/main/res/drawable-hdpi/hide.png b/android/app/src/main/res/drawable-hdpi/hide.png new file mode 100644 index 00000000..a7fd6047 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/hide.png differ diff --git a/android/app/src/main/res/drawable-hdpi/humidity.png b/android/app/src/main/res/drawable-hdpi/humidity.png new file mode 100644 index 00000000..1fe59045 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/humidity.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_comment.png b/android/app/src/main/res/drawable-hdpi/ic_comment.png new file mode 100644 index 00000000..dec6ff48 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_comment.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_praise.png b/android/app/src/main/res/drawable-hdpi/ic_praise.png new file mode 100644 index 00000000..64021e2f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_praise.png differ diff --git a/android/app/src/main/res/drawable-hdpi/iot.png b/android/app/src/main/res/drawable-hdpi/iot.png new file mode 100644 index 00000000..365f333e Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/iot.png differ diff --git a/android/app/src/main/res/drawable-hdpi/light.png b/android/app/src/main/res/drawable-hdpi/light.png new file mode 100644 index 00000000..1d80547a Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/light.png differ diff --git a/android/app/src/main/res/drawable-hdpi/logo.png b/android/app/src/main/res/drawable-hdpi/logo.png new file mode 100644 index 00000000..ab5daf5e Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/logo.png differ diff --git a/android/app/src/main/res/drawable-hdpi/mobile_phone.png b/android/app/src/main/res/drawable-hdpi/mobile_phone.png new file mode 100644 index 00000000..042f612a Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/mobile_phone.png differ diff --git a/android/app/src/main/res/drawable-hdpi/name.png b/android/app/src/main/res/drawable-hdpi/name.png new file mode 100644 index 00000000..c84024bf Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/name.png differ diff --git a/android/app/src/main/res/drawable-hdpi/no_wifi.png b/android/app/src/main/res/drawable-hdpi/no_wifi.png new file mode 100644 index 00000000..ee5f41cd Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/no_wifi.png differ diff --git a/android/app/src/main/res/drawable-hdpi/offline.png b/android/app/src/main/res/drawable-hdpi/offline.png new file mode 100644 index 00000000..3ca5bb26 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/offline.png differ diff --git a/android/app/src/main/res/drawable-hdpi/online.png b/android/app/src/main/res/drawable-hdpi/online.png new file mode 100644 index 00000000..1381977c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/online.png differ diff --git a/android/app/src/main/res/drawable-hdpi/password.png b/android/app/src/main/res/drawable-hdpi/password.png new file mode 100644 index 00000000..6beb822b Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/password.png differ diff --git a/android/app/src/main/res/drawable-hdpi/power.png b/android/app/src/main/res/drawable-hdpi/power.png new file mode 100644 index 00000000..023d19dd Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/power.png differ diff --git a/android/app/src/main/res/drawable-hdpi/scene.png b/android/app/src/main/res/drawable-hdpi/scene.png new file mode 100644 index 00000000..c5efd44d Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/scene.png differ diff --git a/android/app/src/main/res/drawable-hdpi/sensors.png b/android/app/src/main/res/drawable-hdpi/sensors.png new file mode 100644 index 00000000..13880648 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/sensors.png differ diff --git a/android/app/src/main/res/drawable-hdpi/share.png b/android/app/src/main/res/drawable-hdpi/share.png new file mode 100644 index 00000000..f27c1547 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/share.png differ diff --git a/android/app/src/main/res/drawable-hdpi/show.png b/android/app/src/main/res/drawable-hdpi/show.png new file mode 100644 index 00000000..d4bbcbb8 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/show.png differ diff --git a/android/app/src/main/res/drawable-hdpi/state_a.png b/android/app/src/main/res/drawable-hdpi/state_a.png new file mode 100644 index 00000000..f59728ab Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/state_a.png differ diff --git a/android/app/src/main/res/drawable-hdpi/state_b.png b/android/app/src/main/res/drawable-hdpi/state_b.png new file mode 100644 index 00000000..e48af22c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/state_b.png differ diff --git a/android/app/src/main/res/drawable-hdpi/switch_a.png b/android/app/src/main/res/drawable-hdpi/switch_a.png new file mode 100644 index 00000000..3c03311f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/switch_a.png differ diff --git a/android/app/src/main/res/drawable-hdpi/switch_b.png b/android/app/src/main/res/drawable-hdpi/switch_b.png new file mode 100644 index 00000000..4a593371 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/switch_b.png differ diff --git a/android/app/src/main/res/drawable-hdpi/switch_panel.png b/android/app/src/main/res/drawable-hdpi/switch_panel.png new file mode 100644 index 00000000..20780d84 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/switch_panel.png differ diff --git a/android/app/src/main/res/drawable-hdpi/temperature.png b/android/app/src/main/res/drawable-hdpi/temperature.png new file mode 100644 index 00000000..64eb2ac6 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/temperature.png differ diff --git a/android/app/src/main/res/drawable-hdpi/time_a.png b/android/app/src/main/res/drawable-hdpi/time_a.png new file mode 100644 index 00000000..3948fa55 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/time_a.png differ diff --git a/android/app/src/main/res/drawable-hdpi/time_b.png b/android/app/src/main/res/drawable-hdpi/time_b.png new file mode 100644 index 00000000..3cec861f Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/time_b.png differ diff --git a/android/app/src/main/res/drawable-hdpi/up.png b/android/app/src/main/res/drawable-hdpi/up.png new file mode 100644 index 00000000..a7425842 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/up.png differ diff --git a/android/app/src/main/res/drawable-hdpi/water_valve.png b/android/app/src/main/res/drawable-hdpi/water_valve.png new file mode 100644 index 00000000..73bb7790 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/water_valve.png differ diff --git a/android/app/src/main/res/drawable-hdpi/wifi.png b/android/app/src/main/res/drawable-hdpi/wifi.png new file mode 100644 index 00000000..aeeb154d Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/wifi.png differ diff --git a/android/app/src/main/res/drawable-v17/xui_config_bg_splash.xml b/android/app/src/main/res/drawable-v17/xui_config_bg_splash.xml new file mode 100644 index 00000000..be312207 --- /dev/null +++ b/android/app/src/main/res/drawable-v17/xui_config_bg_splash.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v21/xui_config_bg_splash.xml b/android/app/src/main/res/drawable-v21/xui_config_bg_splash.xml new file mode 100644 index 00000000..a068a128 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/xui_config_bg_splash.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..1f6bb290 --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_web_back.png b/android/app/src/main/res/drawable-xxxhdpi/ic_web_back.png new file mode 100644 index 00000000..8bb3cf89 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_web_back.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_web_close.png b/android/app/src/main/res/drawable-xxxhdpi/ic_web_close.png new file mode 100644 index 00000000..58a70367 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_web_close.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_web_more.png b/android/app/src/main/res/drawable-xxxhdpi/ic_web_more.png new file mode 100644 index 00000000..6ecc004c Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_web_more.png differ diff --git a/android/app/src/main/res/drawable/bg_dialog_common_tip_corner_white.xml b/android/app/src/main/res/drawable/bg_dialog_common_tip_corner_white.xml new file mode 100644 index 00000000..dbcadadd --- /dev/null +++ b/android/app/src/main/res/drawable/bg_dialog_common_tip_corner_white.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_action_close_white.xml b/android/app/src/main/res/drawable/ic_action_close_white.xml new file mode 100644 index 00000000..266e01fa --- /dev/null +++ b/android/app/src/main/res/drawable/ic_action_close_white.xml @@ -0,0 +1,22 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_check_normal.xml b/android/app/src/main/res/drawable/ic_check_normal.xml new file mode 100644 index 00000000..ac01f1f4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_check_normal.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_checked.xml b/android/app/src/main/res/drawable/ic_checked.xml new file mode 100644 index 00000000..9961d244 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_checked.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_default_head.xml b/android/app/src/main/res/drawable/ic_default_head.xml new file mode 100644 index 00000000..f68423ec --- /dev/null +++ b/android/app/src/main/res/drawable/ic_default_head.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..ca3826a4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_login_close.xml b/android/app/src/main/res/drawable/ic_login_close.xml new file mode 100644 index 00000000..4d90015b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_login_close.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_logo_app.xml b/android/app/src/main/res/drawable/ic_logo_app.xml new file mode 100644 index 00000000..89557fa2 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_logo_app.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_about.xml b/android/app/src/main/res/drawable/ic_menu_about.xml new file mode 100644 index 00000000..1f7d30f0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_about.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_issues.xml b/android/app/src/main/res/drawable/ic_menu_issues.xml new file mode 100644 index 00000000..1614cdba --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_issues.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_news.xml b/android/app/src/main/res/drawable/ic_menu_news.xml new file mode 100644 index 00000000..9a7ddff3 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_news.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_notifications.xml b/android/app/src/main/res/drawable/ic_menu_notifications.xml new file mode 100644 index 00000000..9c92f095 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_notifications.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_person.xml b/android/app/src/main/res/drawable/ic_menu_person.xml new file mode 100644 index 00000000..3bd07cf2 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_person.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_privacy.xml b/android/app/src/main/res/drawable/ic_menu_privacy.xml new file mode 100644 index 00000000..9184e2e9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_privacy.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_search.xml b/android/app/src/main/res/drawable/ic_menu_search.xml new file mode 100644 index 00000000..9dba080c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_search.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_settings.xml b/android/app/src/main/res/drawable/ic_menu_settings.xml new file mode 100644 index 00000000..ee77a3e7 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_settings.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_star.xml b/android/app/src/main/res/drawable/ic_menu_star.xml new file mode 100644 index 00000000..e7b7c616 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_star.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_menu_trending.xml b/android/app/src/main/res/drawable/ic_menu_trending.xml new file mode 100644 index 00000000..df83f283 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_menu_trending.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_password.xml b/android/app/src/main/res/drawable/ic_password.xml new file mode 100644 index 00000000..716e402a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_password.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_phone.xml b/android/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 00000000..56cf551f --- /dev/null +++ b/android/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,4 @@ + + + diff --git a/android/app/src/main/res/drawable/icon_arrow_right_grey.xml b/android/app/src/main/res/drawable/icon_arrow_right_grey.xml new file mode 100644 index 00000000..964e9b20 --- /dev/null +++ b/android/app/src/main/res/drawable/icon_arrow_right_grey.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/icon_checkbox.xml b/android/app/src/main/res/drawable/icon_checkbox.xml new file mode 100644 index 00000000..bd2960a2 --- /dev/null +++ b/android/app/src/main/res/drawable/icon_checkbox.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/img_guide_tip_top.xml b/android/app/src/main/res/drawable/img_guide_tip_top.xml new file mode 100644 index 00000000..6c787f8f --- /dev/null +++ b/android/app/src/main/res/drawable/img_guide_tip_top.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/activity_agent_web.xml b/android/app/src/main/res/layout/activity_agent_web.xml new file mode 100644 index 00000000..f02c1073 --- /dev/null +++ b/android/app/src/main/res/layout/activity_agent_web.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..088aa83b --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/adapter_button_top_item.xml b/android/app/src/main/res/layout/adapter_button_top_item.xml new file mode 100644 index 00000000..d7c9fabb --- /dev/null +++ b/android/app/src/main/res/layout/adapter_button_top_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/adapter_common_grid_item.xml b/android/app/src/main/res/layout/adapter_common_grid_item.xml new file mode 100644 index 00000000..7b2513a4 --- /dev/null +++ b/android/app/src/main/res/layout/adapter_common_grid_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/adapter_device_card_view_list_item.xml b/android/app/src/main/res/layout/adapter_device_card_view_list_item.xml new file mode 100644 index 00000000..2d97ef8a --- /dev/null +++ b/android/app/src/main/res/layout/adapter_device_card_view_list_item.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/adapter_item_simple_list_2.xml b/android/app/src/main/res/layout/adapter_item_simple_list_2.xml new file mode 100644 index 00000000..94c4c2be --- /dev/null +++ b/android/app/src/main/res/layout/adapter_item_simple_list_2.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/adapter_news_card_view_list_item.xml b/android/app/src/main/res/layout/adapter_news_card_view_list_item.xml new file mode 100644 index 00000000..b6cb55cb --- /dev/null +++ b/android/app/src/main/res/layout/adapter_news_card_view_list_item.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/adapter_title_item.xml b/android/app/src/main/res/layout/adapter_title_item.xml new file mode 100644 index 00000000..cabe173d --- /dev/null +++ b/android/app/src/main/res/layout/adapter_title_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_guide_tips.xml b/android/app/src/main/res/layout/dialog_guide_tips.xml new file mode 100644 index 00000000..59d2a3d3 --- /dev/null +++ b/android/app/src/main/res/layout/dialog_guide_tips.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_about.xml b/android/app/src/main/res/layout/fragment_about.xml new file mode 100644 index 00000000..a70c8934 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_about.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_add_device.xml b/android/app/src/main/res/layout/fragment_add_device.xml new file mode 100644 index 00000000..1bc283d9 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_add_device.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_add_device_two.xml b/android/app/src/main/res/layout/fragment_add_device_two.xml new file mode 100644 index 00000000..30f3d5ab --- /dev/null +++ b/android/app/src/main/res/layout/fragment_add_device_two.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_agentweb.xml b/android/app/src/main/res/layout/fragment_agentweb.xml new file mode 100644 index 00000000..cf1b5a9a --- /dev/null +++ b/android/app/src/main/res/layout/fragment_agentweb.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_device.xml b/android/app/src/main/res/layout/fragment_device.xml new file mode 100644 index 00000000..7376c75e --- /dev/null +++ b/android/app/src/main/res/layout/fragment_device.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_edit_device.xml b/android/app/src/main/res/layout/fragment_edit_device.xml new file mode 100644 index 00000000..3aba826a --- /dev/null +++ b/android/app/src/main/res/layout/fragment_edit_device.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_feedback.xml b/android/app/src/main/res/layout/fragment_feedback.xml new file mode 100644 index 00000000..87f71d42 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_feedback.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_group.xml b/android/app/src/main/res/layout/fragment_group.xml new file mode 100644 index 00000000..ebfaaf34 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_group.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 00000000..36e05ea0 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_message.xml b/android/app/src/main/res/layout/fragment_message.xml new file mode 100644 index 00000000..a80e943e --- /dev/null +++ b/android/app/src/main/res/layout/fragment_message.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_news.xml b/android/app/src/main/res/layout/fragment_news.xml new file mode 100644 index 00000000..8e90cecf --- /dev/null +++ b/android/app/src/main/res/layout/fragment_news.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_profile.xml b/android/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 00000000..2761c921 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_pulldown_web.xml b/android/app/src/main/res/layout/fragment_pulldown_web.xml new file mode 100644 index 00000000..012b5dbd --- /dev/null +++ b/android/app/src/main/res/layout/fragment_pulldown_web.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_scene.xml b/android/app/src/main/res/layout/fragment_scene.xml new file mode 100644 index 00000000..c7094c94 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_scene.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_settings.xml b/android/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 00000000..7b9e3b71 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_share_device.xml b/android/app/src/main/res/layout/fragment_share_device.xml new file mode 100644 index 00000000..ebfaaf34 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_share_device.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_simple_tab.xml b/android/app/src/main/res/layout/fragment_simple_tab.xml new file mode 100644 index 00000000..f92614ad --- /dev/null +++ b/android/app/src/main/res/layout/fragment_simple_tab.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/include_head_view_banner.xml b/android/app/src/main/res/layout/include_head_view_banner.xml new file mode 100644 index 00000000..c0e18903 --- /dev/null +++ b/android/app/src/main/res/layout/include_head_view_banner.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/include_navigation_header.xml b/android/app/src/main/res/layout/include_navigation_header.xml new file mode 100644 index 00000000..0d9c6f43 --- /dev/null +++ b/android/app/src/main/res/layout/include_navigation_header.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/include_toolbar_web.xml b/android/app/src/main/res/layout/include_toolbar_web.xml new file mode 100644 index 00000000..1393456a --- /dev/null +++ b/android/app/src/main/res/layout/include_toolbar_web.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/layout_main_content.xml b/android/app/src/main/res/layout/layout_main_content.xml new file mode 100644 index 00000000..8c91e4c1 --- /dev/null +++ b/android/app/src/main/res/layout/layout_main_content.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/menu/menu_drawer.xml b/android/app/src/main/res/menu/menu_drawer.xml new file mode 100644 index 00000000..d64611b8 --- /dev/null +++ b/android/app/src/main/res/menu/menu_drawer.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/menu/menu_main.xml b/android/app/src/main/res/menu/menu_main.xml new file mode 100644 index 00000000..ca5df9e8 --- /dev/null +++ b/android/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/menu/menu_navigation_bottom.xml b/android/app/src/main/res/menu/menu_navigation_bottom.xml new file mode 100644 index 00000000..1b16294c --- /dev/null +++ b/android/app/src/main/res/menu/menu_navigation_bottom.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/menu/menu_toolbar_web.xml b/android/app/src/main/res/menu/menu_toolbar_web.xml new file mode 100644 index 00000000..207ca500 --- /dev/null +++ b/android/app/src/main/res/menu/menu_toolbar_web.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..c4a603d4 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..33700913 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..376ec27a Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..a8435da5 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..f36004ee Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..5c054f20 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..971ab2fe Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..7cd5cae6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2b1f0d5e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..d6ec5df4 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..587b5908 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..8b1d00f6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1ab8200b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..5d2e51be Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..79c9e3d1 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..ef1d61e3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/arrays.xml b/android/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..c407833a --- /dev/null +++ b/android/app/src/main/res/values/arrays.xml @@ -0,0 +1,80 @@ + + + + + + + @string/menu_device + @string/menu_scene + @string/menu_news + @string/menu_profile + + + + ubmcmm.baidustatic.com + gss1.bdstatic.com/ + cpro2.baidustatic.com + cpro.baidustatic.com + lianmeng.360.cn + nsclick.baidu.com + caclick.baidu.com/ + jieaogd.com + publish-pic-cpu.baidu.com/ + cpro.baidustatic.com/ + hao61.net/ + cpu.baidu.com/ + pos.baidu.com + cbjs.baidu.com + cpro.baidu.com + images.sohu.com/cs/jsfile/js/c.js + union.sogou.com/ + sogou.com/ + 5txs.cn/ + liuzhi520.com/ + yhzm.cc/ + jieaogd.com + a.baidu.com + c.baidu.com + mlnbike.com + alipays://platformapi + alipay.com/ + jieaogd.com + vipshop.com + bayimob.com + + + + 教程 + 资讯 + 社区 + 产品 + + + + + @color/app_color_theme_1 + @color/app_color_theme_2 + @color/app_color_theme_3 + @color/app_color_theme_4 + @color/app_color_theme_5 + @color/app_color_theme_6 + @color/app_color_theme_7 + @color/app_color_theme_8 + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..a9a9a4ba --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,34 @@ + + + #006dfe + #006dfe + #006dfe + + #FFF1F1F1 + + #409eff + #f56c6c + #67c23a + #909399 + #e6a23c + + + @color/xui_config_color_white + @color/xui_config_color_red + @color/colorAccent + #388E3C + @color/xui_config_color_waring + #353A3E + + #EF5362 + #FE6D4B + #FFCF47 + #9FD661 + #3FD0AD + #2BBDF3 + #5A9AEF + #AC8FEF + #EE85C1 + + + diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..509606ca --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,16 @@ + + + 24dp + + 16dp + 14dp + 10dp + 20dp + 8dp + 5dp + 18dp + 30dp + 12dp + 24dp + + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..fe495a1d --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,103 @@ + + 物美 + 通用浏览器 + + Open navigation drawer + Close navigation drawer + + 设备 + 动态 + 场景 + 添加 + 我的 + 添加设备 + 分享设备 + + 消息 + 意见反馈 + 问题 + 收藏 + 搜索 + 设置 + 关于 + + © %1$s wumei All rights reserved. + 访问官网 + 关于作者 + QQ联系 + http://wumei.live + https://gitee.com/kerwincui + http://wpa.qq.com/msgrd?v=3&uin=164770707&site=qq&menu=yes + + + + 是否允许页面打开第三方应用? + + + 退出应用 + 同意 + 不同意 + 再次查看 + 仍不同意 + 温馨提示 + 要不要再想想 + 我们非常重视对你个人信息的保护,承诺严格按照《%s隐私权政策》保护及处理你的信息。如果你不同意该政策,很遗憾我们将无法为你提供服务 + 《%s隐私权政策》 + + + 登录/注册 + 获取验证码 + 登录 + 验证码登录 + 注册 + 忘记密码? + 验证码登录 + 密码登录 + 请输入手机号码 + 手机号码 + 密码 + 旧密码 + 请输入验证码 + 验证码 + 密码必须是8~18位字母和数字的组合! + 新密码必须是8~18位字母和数字的组合! + 无效的手机号! + ^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(16[6])|(17[0,1,3,5-8])|(18[0-9])|(19[8,9]))\\d{8}$ + 请输入4位数验证码 + ^\\d{4}$ + ^(?:(?=.*[a-zA-Z])(?=.*[0-9])).{8,18}$ + 重置密码 + 点击注册即表示同意 + ]]> + 是否确认退出账号? + 跳过 + 上一条 + 下一条 + 以后不再提示此类信息 + 你知道吗? + + 需要位置权限来获取 Wi-Fi 信息。 \n点击申请权限 + 请打开 GPS 以获取 Wi-Fi 信息。 + 请先连上 Wi-Fi + 当前连接的是 5G Wi-Fi, 设备仅支持 2.4G Wi-Fi + + EspTouch + EspTouch 版本: %s + SSID: + BSSID: + 密码: + 设备数量: + 广播 + 组播 + 确认 + 设备不支持 5G Wi-Fi, 请确认当前连接的 Wi-Fi 为 2.4G, 或者您可以尝试选择组播 + ⚠️警告️ + 在 Android M 及以上版本,如果您禁止授权位置权限,APP将无法获取 Wi-Fi 信息。 + Wi-Fi 已断开或发生了变化 + Esptouch 正在执行配网, 请稍等片刻… + EspTouch 配网失败 + 建立 EspTouch 任务失败, 端口可能被其他程序占用 + EspTouch 完成 + BSSID: %1$s, 地址: %2$s + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..29400246 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/styles_widget.xml b/android/app/src/main/res/values/styles_widget.xml new file mode 100644 index 00000000..cba380d2 --- /dev/null +++ b/android/app/src/main/res/values/styles_widget.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..da03785b --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/test/java/com/kerwin/templateproject/ExampleUnitTest.java b/android/app/src/test/java/com/kerwin/templateproject/ExampleUnitTest.java new file mode 100644 index 00000000..339be1e9 --- /dev/null +++ b/android/app/src/test/java/com/kerwin/templateproject/ExampleUnitTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com) + * + * 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 + * + * http://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. + * + */ + +package com.kerwin.templateproject; + +import com.kerwin.wumei.core.http.entity.TipInfo; +import com.xuexiang.xhttp2.model.ApiResult; +import com.xuexiang.xutil.net.JsonUtil; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + + + TipInfo info = new TipInfo(); + info.setTitle("微信公众号"); + info.setContent("获取更多资讯,欢迎关注我的微信公众号:【我的Android开源之旅】"); + List list = new ArrayList<>(); + for (int i = 0; i <5 ; i++) { + list.add(info); + } + ApiResult> result = new ApiResult<>(); + result.setData(list); + System.out.println(JsonUtil.toJson(result)); + } +} \ No newline at end of file diff --git a/android/app/x-library.gradle b/android/app/x-library.gradle new file mode 100644 index 00000000..a1ffa01b --- /dev/null +++ b/android/app/x-library.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.xuexiang.xaop' //引用XAOP插件 +apply plugin: 'com.xuexiang.xrouter' //引用XRouter-plugin插件实现自动注册 + +//自动添加依赖 +project.configurations.each { configuration -> + if (configuration.name == "implementation") { + //为Project加入X-Library依赖 + //XUI框架 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys:XUI:1.1.6')) + configuration.dependencies.add(getProject().dependencies.create(deps.androidx.appcompat)) + configuration.dependencies.add(getProject().dependencies.create(deps.androidx.recyclerview)) + configuration.dependencies.add(getProject().dependencies.create(deps.androidx.design)) + configuration.dependencies.add(getProject().dependencies.create(deps.glide)) + //XUtil工具类 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XUtil:xutil-core:2.0.0')) + //XAOP切片,版本号前带x的是支持androidx的版本 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XAOP:xaop-runtime:1.1.0')) + //XUpdate版本更新 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys:XUpdate:2.0.7')) + //XHttp2 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys:XHttp2:2.0.2')) + configuration.dependencies.add(getProject().dependencies.create(deps.rxjava2)) + configuration.dependencies.add(getProject().dependencies.create(deps.rxandroid)) + configuration.dependencies.add(getProject().dependencies.create('com.squareup.okhttp3:okhttp:3.10.0')) + configuration.dependencies.add(getProject().dependencies.create(deps.gson)) + //XPage + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XPage:xpage-lib:3.1.1')) + configuration.dependencies.add(getProject().dependencies.create(deps.butterknife.runtime)) + //页面路由 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XRouter:xrouter-runtime:1.0.1')) + } + + if (configuration.name == "annotationProcessor") { + //XPage + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XPage:xpage-compiler:3.1.1')) + configuration.dependencies.add(getProject().dependencies.create(deps.butterknife.compiler)) + //页面路由 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XRouter:xrouter-compiler:1.0.1')) + } + + if (configuration.name == "debugImplementation") { + //内存泄漏监测leak + configuration.dependencies.add(getProject().dependencies.create('com.squareup.leakcanary:leakcanary-android:2.6')) + } +} + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..c4391db5 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +// 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.2.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 + } + } +} + +allprojects { + addRepos(repositories) +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + diff --git a/android/esptouch/.gitignore b/android/esptouch/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/android/esptouch/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/esptouch/README.md b/android/esptouch/README.md new file mode 100644 index 00000000..d0db4fed --- /dev/null +++ b/android/esptouch/README.md @@ -0,0 +1,42 @@ +# EspTouch +[example](../app/src/main/java/com/espressif/esptouch/android/v1) + +- Create task instance + ```java + Context context; // Set Applicatioin context + byte[] apSsid = {}; // Set AP's SSID + byte[] apBssid = {}; // Set AP's BSSID + byte[] apPassword = {}; // Set AP's password + + EsptouchTask task = new EsptouchTask(apSsid, apBssid, apPassword, context); + task.setPackageBroadcast(true); // if true send broadcast packets, else send multicast packets + ``` + +- Set result callback + ```java + task.setEsptouchListener(new IEsptouchListener() { + @Override + public void onEsptouchResultAdded(IEsptouchResult result) { + // Result callback + } + }); + ``` + +- Execute task + ```java + int expectResultCount = 1; + List results = task.executeForResults(expectResultCount); + IEsptouchResult first = results.get(0); + if (first.isCancelled()) { + // User cancel the task + return; + } + if (first.isSuc()) { + // EspTouch successfully + } + ``` + +- Cancel task + ```java + task.interrupt(); + ``` diff --git a/android/esptouch/build.gradle b/android/esptouch/build.gradle new file mode 100644 index 00000000..b26d7caf --- /dev/null +++ b/android/esptouch/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 29 + versionCode 8 + versionName "v0.3.7.2" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/android/esptouch/proguard-rules.pro b/android/esptouch/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/android/esptouch/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/esptouch/src/main/AndroidManifest.xml b/android/esptouch/src/main/AndroidManifest.xml new file mode 100644 index 00000000..90704c2c --- /dev/null +++ b/android/esptouch/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchResult.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchResult.java new file mode 100644 index 00000000..0d17c4db --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchResult.java @@ -0,0 +1,56 @@ +package com.espressif.iot.esptouch; + +import java.net.InetAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +public class EsptouchResult implements IEsptouchResult { + + private final boolean mIsSuc; + private final String mBssid; + private final InetAddress mInetAddress; + private AtomicBoolean mIsCancelled; + + /** + * Constructor of EsptouchResult + * + * @param isSuc whether the esptouch task is executed suc + * @param bssid the device's bssid + * @param inetAddress the device's ip address + */ + public EsptouchResult(boolean isSuc, String bssid, InetAddress inetAddress) { + this.mIsSuc = isSuc; + this.mBssid = bssid; + this.mInetAddress = inetAddress; + this.mIsCancelled = new AtomicBoolean(false); + } + + @Override + public boolean isSuc() { + return this.mIsSuc; + } + + @Override + public String getBssid() { + return this.mBssid; + } + + @Override + public boolean isCancelled() { + return mIsCancelled.get(); + } + + public void setIsCancelled(boolean isCancelled) { + this.mIsCancelled.set(isCancelled); + } + + @Override + public InetAddress getInetAddress() { + return this.mInetAddress; + } + + @Override + public String toString() { + return String.format("bssid=%s, address=%s, suc=%b, cancel=%b", mBssid, + mInetAddress == null ? null : mInetAddress.getHostAddress(), mIsSuc, mIsCancelled.get()); + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchTask.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchTask.java new file mode 100644 index 00000000..eef9e885 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/EsptouchTask.java @@ -0,0 +1,115 @@ +package com.espressif.iot.esptouch; + +import android.content.Context; +import android.text.TextUtils; + +import com.espressif.iot.esptouch.protocol.TouchData; +import com.espressif.iot.esptouch.security.ITouchEncryptor; +import com.espressif.iot.esptouch.task.EsptouchTaskParameter; +import com.espressif.iot.esptouch.task.__EsptouchTask; +import com.espressif.iot.esptouch.util.TouchNetUtil; + +import java.util.List; + +public class EsptouchTask implements IEsptouchTask { + private __EsptouchTask _mEsptouchTask; + private EsptouchTaskParameter _mParameter; + + /** + * Constructor of EsptouchTask + * + * @param apSsid the Ap's ssid + * @param apBssid the Ap's bssid + * @param apPassword the Ap's password + * @param context the {@link Context} of the Application + */ + public EsptouchTask(String apSsid, String apBssid, String apPassword, Context context) { + this(apSsid, apBssid, apPassword, null, context); + } + + /** + * Constructor of EsptouchTask + * + * @param apSsid the Ap's ssid + * @param apBssid the Ap's bssid + * @param apPassword the Ap's password + * @param context the {@link Context} of the Application + */ + public EsptouchTask(byte[] apSsid, byte[] apBssid, byte[] apPassword, Context context) { + this(apSsid, apBssid, apPassword, null, context); + } + + private EsptouchTask(String apSsid, String apBssid, String apPassword, ITouchEncryptor encryptor, Context context) { + if (TextUtils.isEmpty(apSsid)) { + throw new NullPointerException("SSID can't be empty"); + } + if (TextUtils.isEmpty(apBssid)) { + throw new NullPointerException("BSSID can't be empty"); + } + if (apPassword == null) { + apPassword = ""; + } + TouchData ssid = new TouchData(apSsid); + TouchData bssid = new TouchData(TouchNetUtil.parseBssid2bytes(apBssid)); + if (bssid.getData().length != 6) { + throw new IllegalArgumentException("Bssid format must be aa:bb:cc:dd:ee:ff"); + } + TouchData password = new TouchData(apPassword); + init(context, ssid, bssid, password, encryptor); + } + + private EsptouchTask(byte[] apSsid, byte[] apBssid, byte[] apPassword, ITouchEncryptor encryptor, Context context) { + if (apSsid == null || apSsid.length == 0) { + throw new NullPointerException("SSID can't be empty"); + } + if (apBssid == null || apBssid.length != 6) { + throw new NullPointerException("BSSID is empty or length is not 6"); + } + if (apPassword == null) { + apPassword = new byte[0]; + } + TouchData ssid = new TouchData(apSsid); + TouchData bssid = new TouchData(apBssid); + TouchData password = new TouchData(apPassword); + init(context, ssid, bssid, password, encryptor); + } + + private void init(Context context, TouchData ssid, TouchData bssid, TouchData password, ITouchEncryptor encryptor) { + _mParameter = new EsptouchTaskParameter(); + _mEsptouchTask = new __EsptouchTask(context, ssid, bssid, password, encryptor, _mParameter); + } + + @Override + public void interrupt() { + _mEsptouchTask.interrupt(); + } + + @Override + public IEsptouchResult executeForResult() throws RuntimeException { + return _mEsptouchTask.executeForResult(); + } + + @Override + public boolean isCancelled() { + return _mEsptouchTask.isCancelled(); + } + + @Override + public List executeForResults(int expectTaskResultCount) + throws RuntimeException { + if (expectTaskResultCount <= 0) { + expectTaskResultCount = Integer.MAX_VALUE; + } + return _mEsptouchTask.executeForResults(expectTaskResultCount); + } + + @Override + public void setEsptouchListener(IEsptouchListener esptouchListener) { + _mEsptouchTask.setEsptouchListener(esptouchListener); + } + + @Override + public void setPackageBroadcast(boolean broadcast) { + _mParameter.setBroadcast(broadcast); + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchListener.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchListener.java new file mode 100644 index 00000000..4c9d992c --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchListener.java @@ -0,0 +1,11 @@ +package com.espressif.iot.esptouch; + +public interface IEsptouchListener { + /** + * when new esptouch result is added, the listener will call + * onEsptouchResultAdded callback + * + * @param result the Esptouch result + */ + void onEsptouchResultAdded(IEsptouchResult result); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchResult.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchResult.java new file mode 100644 index 00000000..0aad8788 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchResult.java @@ -0,0 +1,34 @@ +package com.espressif.iot.esptouch; + +import java.net.InetAddress; + +public interface IEsptouchResult { + + /** + * check whether the esptouch task is executed suc + * + * @return whether the esptouch task is executed suc + */ + boolean isSuc(); + + /** + * get the device's bssid + * + * @return the device's bssid + */ + String getBssid(); + + /** + * check whether the esptouch task is cancelled by user + * + * @return whether the esptouch task is cancelled by user + */ + boolean isCancelled(); + + /** + * get the ip address of the device + * + * @return the ip device of the device + */ + InetAddress getInetAddress(); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchTask.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchTask.java new file mode 100644 index 00000000..821b9445 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/IEsptouchTask.java @@ -0,0 +1,61 @@ +package com.espressif.iot.esptouch; + +import java.util.List; + +public interface IEsptouchTask { + String ESPTOUCH_VERSION = BuildConfig.VERSION_NAME; + + /** + * set the esptouch listener, when one device is connected to the Ap, it will be called back + * + * @param esptouchListener when one device is connected to the Ap, it will be called back + */ + void setEsptouchListener(IEsptouchListener esptouchListener); + + /** + * Interrupt the Esptouch Task when User tap back or close the Application. + */ + void interrupt(); + + /** + * Note: !!!Don't call the task at UI Main Thread or RuntimeException will + * be thrown Execute the Esptouch Task and return the result + *

+ * Smart Config v2.4 support the API + * + * @return the IEsptouchResult + */ + IEsptouchResult executeForResult() throws RuntimeException; + + /** + * Note: !!!Don't call the task at UI Main Thread or RuntimeException will + * be thrown Execute the Esptouch Task and return the result + *

+ * Smart Config v2.4 support the API + *

+ * It will be blocked until the client receive result count >= expectTaskResultCount. + * If it fail, it will return one fail result will be returned in the list. + * If it is cancelled while executing, + * if it has received some results, all of them will be returned in the list. + * if it hasn't received any results, one cancel result will be returned in the list. + * + * @param expectTaskResultCount the expect result count(if expectTaskResultCount <= 0, + * expectTaskResultCount = Integer.MAX_VALUE) + * @return the list of IEsptouchResult + */ + List executeForResults(int expectTaskResultCount) throws RuntimeException; + + /** + * check whether the task is cancelled by user + * + * @return whether the task is cancelled by user + */ + boolean isCancelled(); + + /** + * Set broadcast or multicast when post configure info + * + * @param broadcast true is broadcast, false is multicast + */ + void setPackageBroadcast(boolean broadcast); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DataCode.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DataCode.java new file mode 100644 index 00000000..ada4cce1 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DataCode.java @@ -0,0 +1,87 @@ +package com.espressif.iot.esptouch.protocol; + +import com.espressif.iot.esptouch.task.ICodeData; +import com.espressif.iot.esptouch.util.ByteUtil; +import com.espressif.iot.esptouch.util.CRC8; + +/** + * one data format:(data code should have 2 to 65 data) + *

+ * control byte high 4 bits low 4 bits + * 1st 9bits: 0x0 crc(high) data(high) + * 2nd 9bits: 0x1 sequence header + * 3rd 9bits: 0x0 crc(low) data(low) + *

+ * sequence header: 0,1,2,... + * + * @author afunx + */ +public class DataCode implements ICodeData { + + public static final int DATA_CODE_LEN = 6; + + private static final int INDEX_MAX = 127; + + private final byte mSeqHeader; + private final byte mDataHigh; + private final byte mDataLow; + // the crc here means the crc of the data and sequence header be transformed + // it is calculated by index and data to be transformed + private final byte mCrcHigh; + private final byte mCrcLow; + + /** + * Constructor of DataCode + * + * @param u8 the character to be transformed + * @param index the index of the char + */ + public DataCode(char u8, int index) { + if (index > INDEX_MAX) { + throw new RuntimeException("index > INDEX_MAX"); + } + byte[] dataBytes = ByteUtil.splitUint8To2bytes(u8); + mDataHigh = dataBytes[0]; + mDataLow = dataBytes[1]; + CRC8 crc8 = new CRC8(); + crc8.update(ByteUtil.convertUint8toByte(u8)); + crc8.update(index); + byte[] crcBytes = ByteUtil.splitUint8To2bytes((char) crc8.getValue()); + mCrcHigh = crcBytes[0]; + mCrcLow = crcBytes[1]; + mSeqHeader = (byte) index; + } + + @Override + public byte[] getBytes() { + byte[] dataBytes = new byte[DATA_CODE_LEN]; + dataBytes[0] = 0x00; + dataBytes[1] = ByteUtil.combine2bytesToOne(mCrcHigh, mDataHigh); + dataBytes[2] = 0x01; + dataBytes[3] = mSeqHeader; + dataBytes[4] = 0x00; + dataBytes[5] = ByteUtil.combine2bytesToOne(mCrcLow, mDataLow); + return dataBytes; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + byte[] dataBytes = getBytes(); + for (int i = 0; i < DATA_CODE_LEN; i++) { + String hexString = ByteUtil.convertByte2HexString(dataBytes[i]); + sb.append("0x"); + if (hexString.length() == 1) { + sb.append("0"); + } + sb.append(hexString).append(" "); + } + return sb.toString(); + } + + @Override + public char[] getU8s() { + throw new RuntimeException("DataCode don't support getU8s()"); + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DatumCode.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DatumCode.java new file mode 100644 index 00000000..4cfeafbe --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/DatumCode.java @@ -0,0 +1,141 @@ +package com.espressif.iot.esptouch.protocol; + +import com.espressif.iot.esptouch.security.ITouchEncryptor; +import com.espressif.iot.esptouch.task.ICodeData; +import com.espressif.iot.esptouch.util.ByteUtil; +import com.espressif.iot.esptouch.util.CRC8; + +import java.net.InetAddress; +import java.util.LinkedList; + +public class DatumCode implements ICodeData { + + // define by the Esptouch protocol, all of the datum code should add 1 at last to prevent 0 + private static final int EXTRA_LEN = 40; + private static final int EXTRA_HEAD_LEN = 5; + + private final LinkedList mDataCodes; + + /** + * Constructor of DatumCode + * + * @param apSsid the Ap's ssid + * @param apBssid the Ap's bssid + * @param apPassword the Ap's password + * @param ipAddress the ip address of the phone or pad + * @param encryptor null use origin data, not null use encrypted data + */ + public DatumCode(byte[] apSsid, byte[] apBssid, byte[] apPassword, + InetAddress ipAddress, ITouchEncryptor encryptor) { + // Data = total len(1 byte) + apPwd len(1 byte) + SSID CRC(1 byte) + + // BSSID CRC(1 byte) + TOTAL XOR(1 byte)+ ipAddress(4 byte) + apPwd + apSsid apPwdLen <= + // 105 at the moment + + // total xor + char totalXor = 0; + + char apPwdLen = (char) apPassword.length; + CRC8 crc = new CRC8(); + crc.update(apSsid); + char apSsidCrc = (char) crc.getValue(); + + crc.reset(); + crc.update(apBssid); + char apBssidCrc = (char) crc.getValue(); + + char apSsidLen = (char) apSsid.length; + + byte[] ipBytes = ipAddress.getAddress(); + int ipLen = ipBytes.length; + + char totalLen = (char) (EXTRA_HEAD_LEN + ipLen + apPwdLen + apSsidLen); + + // build data codes + mDataCodes = new LinkedList<>(); + mDataCodes.add(new DataCode(totalLen, 0)); + totalXor ^= totalLen; + mDataCodes.add(new DataCode(apPwdLen, 1)); + totalXor ^= apPwdLen; + mDataCodes.add(new DataCode(apSsidCrc, 2)); + totalXor ^= apSsidCrc; + mDataCodes.add(new DataCode(apBssidCrc, 3)); + totalXor ^= apBssidCrc; + // ESPDataCode 4 is null + for (int i = 0; i < ipLen; ++i) { + char c = ByteUtil.convertByte2Uint8(ipBytes[i]); + totalXor ^= c; + mDataCodes.add(new DataCode(c, i + EXTRA_HEAD_LEN)); + } + + for (int i = 0; i < apPassword.length; i++) { + char c = ByteUtil.convertByte2Uint8(apPassword[i]); + totalXor ^= c; + mDataCodes.add(new DataCode(c, i + EXTRA_HEAD_LEN + ipLen)); + } + + // totalXor will xor apSsidChars no matter whether the ssid is hidden + for (int i = 0; i < apSsid.length; i++) { + char c = ByteUtil.convertByte2Uint8(apSsid[i]); + totalXor ^= c; + mDataCodes.add(new DataCode(c, i + EXTRA_HEAD_LEN + ipLen + apPwdLen)); + } + + // add total xor last + mDataCodes.add(4, new DataCode(totalXor, 4)); + + // add bssid + int bssidInsertIndex = EXTRA_HEAD_LEN; + for (int i = 0; i < apBssid.length; i++) { + int index = totalLen + i; + char c = ByteUtil.convertByte2Uint8(apBssid[i]); + DataCode dc = new DataCode(c, index); + if (bssidInsertIndex >= mDataCodes.size()) { + mDataCodes.add(dc); + } else { + mDataCodes.add(bssidInsertIndex, dc); + } + bssidInsertIndex += 4; + } + } + + @Override + public byte[] getBytes() { + byte[] datumCode = new byte[mDataCodes.size() * DataCode.DATA_CODE_LEN]; + int index = 0; + for (DataCode dc : mDataCodes) { + for (byte b : dc.getBytes()) { + datumCode[index++] = b; + } + } + return datumCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + byte[] dataBytes = getBytes(); + for (byte dataByte : dataBytes) { + String hexString = ByteUtil.convertByte2HexString(dataByte); + sb.append("0x"); + if (hexString.length() == 1) { + sb.append("0"); + } + sb.append(hexString).append(" "); + } + return sb.toString(); + } + + @Override + public char[] getU8s() { + byte[] dataBytes = getBytes(); + int len = dataBytes.length / 2; + char[] dataU8s = new char[len]; + byte high, low; + for (int i = 0; i < len; i++) { + high = dataBytes[i * 2]; + low = dataBytes[i * 2 + 1]; + dataU8s[i] = (char) (ByteUtil.combine2bytesToU16(high, low) + EXTRA_LEN); + } + return dataU8s; + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/EsptouchGenerator.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/EsptouchGenerator.java new file mode 100644 index 00000000..a928efb9 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/EsptouchGenerator.java @@ -0,0 +1,54 @@ +package com.espressif.iot.esptouch.protocol; + +import com.espressif.iot.esptouch.security.ITouchEncryptor; +import com.espressif.iot.esptouch.task.IEsptouchGenerator; +import com.espressif.iot.esptouch.util.ByteUtil; + +import java.net.InetAddress; + +public class EsptouchGenerator implements IEsptouchGenerator { + + private final byte[][] mGcBytes2; + private final byte[][] mDcBytes2; + + /** + * Constructor of EsptouchGenerator, it will cost some time(maybe a bit + * much) + * + * @param apSsid the Ap's ssid + * @param apBssid the Ap's bssid + * @param apPassword the Ap's password + * @param inetAddress the phone's or pad's local ip address allocated by Ap + */ + public EsptouchGenerator(byte[] apSsid, byte[] apBssid, byte[] apPassword, InetAddress inetAddress, + ITouchEncryptor encryptor) { + // generate guide code + GuideCode gc = new GuideCode(); + char[] gcU81 = gc.getU8s(); + mGcBytes2 = new byte[gcU81.length][]; + + for (int i = 0; i < mGcBytes2.length; i++) { + mGcBytes2[i] = ByteUtil.genSpecBytes(gcU81[i]); + } + + // generate data code + DatumCode dc = new DatumCode(apSsid, apBssid, apPassword, inetAddress, encryptor); + char[] dcU81 = dc.getU8s(); + mDcBytes2 = new byte[dcU81.length][]; + + for (int i = 0; i < mDcBytes2.length; i++) { + mDcBytes2[i] = ByteUtil.genSpecBytes(dcU81[i]); + } + } + + @Override + public byte[][] getGCBytes2() { + return mGcBytes2; + } + + @Override + public byte[][] getDCBytes2() { + return mDcBytes2; + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/GuideCode.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/GuideCode.java new file mode 100644 index 00000000..a49bea2f --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/GuideCode.java @@ -0,0 +1,39 @@ +package com.espressif.iot.esptouch.protocol; + +import com.espressif.iot.esptouch.task.ICodeData; +import com.espressif.iot.esptouch.util.ByteUtil; + +public class GuideCode implements ICodeData { + + public static final int GUIDE_CODE_LEN = 4; + + @Override + public byte[] getBytes() { + throw new RuntimeException("DataCode don't support getBytes()"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + char[] dataU8s = getU8s(); + for (int i = 0; i < GUIDE_CODE_LEN; i++) { + String hexString = ByteUtil.convertU8ToHexString(dataU8s[i]); + sb.append("0x"); + if (hexString.length() == 1) { + sb.append("0"); + } + sb.append(hexString).append(" "); + } + return sb.toString(); + } + + @Override + public char[] getU8s() { + char[] guidesU8s = new char[GUIDE_CODE_LEN]; + guidesU8s[0] = 515; + guidesU8s[1] = 514; + guidesU8s[2] = 513; + guidesU8s[3] = 512; + return guidesU8s; + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/TouchData.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/TouchData.java new file mode 100644 index 00000000..f96afaf0 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/protocol/TouchData.java @@ -0,0 +1,22 @@ +package com.espressif.iot.esptouch.protocol; + +import com.espressif.iot.esptouch.util.ByteUtil; + +public class TouchData { + private final byte[] mData; + + public TouchData(String string) { + mData = ByteUtil.getBytesByString(string); + } + + public TouchData(byte[] data) { + if (data == null) { + throw new NullPointerException("data can't be null"); + } + mData = data; + } + + public byte[] getData() { + return mData; + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/ITouchEncryptor.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/ITouchEncryptor.java new file mode 100644 index 00000000..9a8f463f --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/ITouchEncryptor.java @@ -0,0 +1,5 @@ +package com.espressif.iot.esptouch.security; + +public interface ITouchEncryptor { + byte[] encrypt(byte[] src); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/TouchAES.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/TouchAES.java new file mode 100644 index 00000000..bc7c6ae4 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/security/TouchAES.java @@ -0,0 +1,104 @@ +package com.espressif.iot.esptouch.security; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class TouchAES implements ITouchEncryptor { + private static final String TRANSFORMATION_DEFAULT = "AES/ECB/PKCS5Padding"; + + private final byte[] mKey; + private final byte[] mIV; + private final String mTransformation; + private Cipher mEncryptCipher; + private Cipher mDecryptCipher; + + public TouchAES(byte[] key) { + this(key, null, TRANSFORMATION_DEFAULT); + } + + public TouchAES(byte[] key, String transformation) { + this(key, null, transformation); + } + + public TouchAES(byte[] key, byte[] iv) { + this(key, iv, TRANSFORMATION_DEFAULT); + } + + public TouchAES(byte[] key, byte[] iv, String transformation) { + mKey = key; + mIV = iv; + mTransformation = transformation; + + mEncryptCipher = createEncryptCipher(); + mDecryptCipher = createDecryptCipher(); + } + + private Cipher createEncryptCipher() { + try { + Cipher cipher = Cipher.getInstance(mTransformation); + + SecretKeySpec secretKeySpec = new SecretKeySpec(mKey, "AES"); + if (mIV == null) { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + } else { + IvParameterSpec parameterSpec = new IvParameterSpec(mIV); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, parameterSpec); + } + + return cipher; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException + e) { + e.printStackTrace(); + } + + return null; + } + + private Cipher createDecryptCipher() { + try { + Cipher cipher = Cipher.getInstance(mTransformation); + + SecretKeySpec secretKeySpec = new SecretKeySpec(mKey, "AES"); + if (mIV == null) { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + } else { + IvParameterSpec parameterSpec = new IvParameterSpec(mIV); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, parameterSpec); + } + + return cipher; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException + e) { + e.printStackTrace(); + } + + return null; + } + + public byte[] encrypt(byte[] content) { + try { + return mEncryptCipher.doFinal(content); + } catch (BadPaddingException | IllegalBlockSizeException e) { + e.printStackTrace(); + } + return null; + } + + public byte[] decrypt(byte[] content) { + try { + return mDecryptCipher.doFinal(content); + } catch (BadPaddingException | IllegalBlockSizeException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/EsptouchTaskParameter.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/EsptouchTaskParameter.java new file mode 100644 index 00000000..7ea80864 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/EsptouchTaskParameter.java @@ -0,0 +1,165 @@ +package com.espressif.iot.esptouch.task; + +public class EsptouchTaskParameter implements IEsptouchTaskParameter { + + private static int _datagramCount = 0; + private long mIntervalGuideCodeMillisecond; + private long mIntervalDataCodeMillisecond; + private long mTimeoutGuideCodeMillisecond; + private long mTimeoutDataCodeMillisecond; + private int mTotalRepeatTime; + private int mEsptouchResultOneLen; + private int mEsptouchResultMacLen; + private int mEsptouchResultIpLen; + private int mEsptouchResultTotalLen; + private int mPortListening; + private int mTargetPort; + private int mWaitUdpReceivingMilliseond; + private int mWaitUdpSendingMillisecond; + private int mThresholdSucBroadcastCount; + private int mExpectTaskResultCount; + private boolean mBroadcast = true; + + public EsptouchTaskParameter() { + mIntervalGuideCodeMillisecond = 8; + mIntervalDataCodeMillisecond = 8; + mTimeoutGuideCodeMillisecond = 2000; + mTimeoutDataCodeMillisecond = 4000; + mTotalRepeatTime = 1; + mEsptouchResultOneLen = 1; + mEsptouchResultMacLen = 6; + mEsptouchResultIpLen = 4; + mEsptouchResultTotalLen = 1 + 6 + 4; + mPortListening = 18266; + mTargetPort = 7001; + mWaitUdpReceivingMilliseond = 15000; + mWaitUdpSendingMillisecond = 45000; + mThresholdSucBroadcastCount = 1; + mExpectTaskResultCount = 1; + } + + // the range of the result should be 1-100 + private static int __getNextDatagramCount() { + return 1 + (_datagramCount++) % 100; + } + + @Override + public long getIntervalGuideCodeMillisecond() { + return mIntervalGuideCodeMillisecond; + } + + @Override + public long getIntervalDataCodeMillisecond() { + return mIntervalDataCodeMillisecond; + } + + @Override + public long getTimeoutGuideCodeMillisecond() { + return mTimeoutGuideCodeMillisecond; + } + + @Override + public long getTimeoutDataCodeMillisecond() { + return mTimeoutDataCodeMillisecond; + } + + @Override + public long getTimeoutTotalCodeMillisecond() { + return mTimeoutGuideCodeMillisecond + mTimeoutDataCodeMillisecond; + } + + @Override + public int getTotalRepeatTime() { + return mTotalRepeatTime; + } + + @Override + public int getEsptouchResultOneLen() { + return mEsptouchResultOneLen; + } + + @Override + public int getEsptouchResultMacLen() { + return mEsptouchResultMacLen; + } + + @Override + public int getEsptouchResultIpLen() { + return mEsptouchResultIpLen; + } + + @Override + public int getEsptouchResultTotalLen() { + return mEsptouchResultTotalLen; + } + + @Override + public int getPortListening() { + return mPortListening; + } + + // target hostname is : 234.1.1.1, 234.2.2.2, 234.3.3.3 to 234.100.100.100 + @Override + public String getTargetHostname() { + if (mBroadcast) { + return "255.255.255.255"; + } else { + int count = __getNextDatagramCount(); + return "234." + count + "." + count + "." + count; + } + } + + @Override + public int getTargetPort() { + return mTargetPort; + } + + @Override + public int getWaitUdpReceivingMillisecond() { + return mWaitUdpReceivingMilliseond; + } + + @Override + public int getWaitUdpSendingMillisecond() { + return mWaitUdpSendingMillisecond; + } + + @Override + public int getWaitUdpTotalMillisecond() { + return mWaitUdpReceivingMilliseond + mWaitUdpSendingMillisecond; + } + + @Override + public void setWaitUdpTotalMillisecond(int waitUdpTotalMillisecond) { + if (waitUdpTotalMillisecond < mWaitUdpReceivingMilliseond + + getTimeoutTotalCodeMillisecond()) { + // if it happen, even one turn about sending udp broadcast can't be + // completed + throw new IllegalArgumentException( + "waitUdpTotalMillisecod is invalid, " + + "it is less than mWaitUdpReceivingMilliseond + getTimeoutTotalCodeMillisecond()"); + } + mWaitUdpSendingMillisecond = waitUdpTotalMillisecond + - mWaitUdpReceivingMilliseond; + } + + @Override + public int getThresholdSucBroadcastCount() { + return mThresholdSucBroadcastCount; + } + + @Override + public int getExpectTaskResultCount() { + return this.mExpectTaskResultCount; + } + + @Override + public void setExpectTaskResultCount(int expectTaskResultCount) { + this.mExpectTaskResultCount = expectTaskResultCount; + } + + @Override + public void setBroadcast(boolean broadcast) { + mBroadcast = broadcast; + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/ICodeData.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/ICodeData.java new file mode 100644 index 00000000..64e9c9b5 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/ICodeData.java @@ -0,0 +1,22 @@ +package com.espressif.iot.esptouch.task; + +/** + * the class used to represent some code to be transformed by UDP socket should implement the interface + * + * @author afunx + */ +public interface ICodeData { + /** + * Get the byte[] to be transformed. + * + * @return the byte[] to be transfromed + */ + byte[] getBytes(); + + /** + * Get the char[](u8[]) to be transfromed. + * + * @return the char[](u8) to be transformed + */ + char[] getU8s(); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchGenerator.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchGenerator.java new file mode 100644 index 00000000..07c8981f --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchGenerator.java @@ -0,0 +1,17 @@ +package com.espressif.iot.esptouch.task; + +public interface IEsptouchGenerator { + /** + * Get guide code by the format of byte[][] + * + * @return guide code by the format of byte[][] + */ + byte[][] getGCBytes2(); + + /** + * Get data code by the format of byte[][] + * + * @return data code by the format of byte[][] + */ + byte[][] getDCBytes2(); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchTaskParameter.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchTaskParameter.java new file mode 100644 index 00000000..f40e9535 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/IEsptouchTaskParameter.java @@ -0,0 +1,156 @@ +package com.espressif.iot.esptouch.task; + +public interface IEsptouchTaskParameter { + + /** + * get interval millisecond for guide code(the time between each guide code sending) + * + * @return interval millisecond for guide code(the time between each guide code sending) + */ + long getIntervalGuideCodeMillisecond(); + + /** + * get interval millisecond for data code(the time between each data code sending) + * + * @return interval millisecond for data code(the time between each data code sending) + */ + long getIntervalDataCodeMillisecond(); + + /** + * get timeout millisecond for guide code(the time how much the guide code sending) + * + * @return timeout millisecond for guide code(the time how much the guide code sending) + */ + long getTimeoutGuideCodeMillisecond(); + + /** + * get timeout millisecond for data code(the time how much the data code sending) + * + * @return timeout millisecond for data code(the time how much the data code sending) + */ + long getTimeoutDataCodeMillisecond(); + + /** + * get timeout millisecond for total code(guide code and data code altogether) + * + * @return timeout millisecond for total code(guide code and data code altogether) + */ + long getTimeoutTotalCodeMillisecond(); + + /** + * get total repeat time for executing esptouch task + * + * @return total repeat time for executing esptouch task + */ + int getTotalRepeatTime(); + + /** + * the length of the Esptouch result 1st byte is the total length of ssid and + * password, the other 6 bytes are the device's bssid + */ + + /** + * get esptouchResult length of one + * + * @return length of one + */ + int getEsptouchResultOneLen(); + + /** + * get esptouchResult length of mac + * + * @return length of mac + */ + int getEsptouchResultMacLen(); + + /** + * get esptouchResult length of ip + * + * @return length of ip + */ + int getEsptouchResultIpLen(); + + /** + * get esptouchResult total length + * + * @return total length + */ + int getEsptouchResultTotalLen(); + + /** + * get port for listening(used by server) + * + * @return port for listening(used by server) + */ + int getPortListening(); + + /** + * get target hostname + * + * @return target hostame(used by client) + */ + String getTargetHostname(); + + /** + * get target port + * + * @return target port(used by client) + */ + int getTargetPort(); + + /** + * get millisecond for waiting udp receiving(receiving without sending) + * + * @return millisecond for waiting udp receiving(receiving without sending) + */ + int getWaitUdpReceivingMillisecond(); + + /** + * get millisecond for waiting udp sending(sending including receiving) + * + * @return millisecond for waiting udep sending(sending including receiving) + */ + int getWaitUdpSendingMillisecond(); + + /** + * get millisecond for waiting udp sending and receiving + * + * @return millisecond for waiting udp sending and receiving + */ + int getWaitUdpTotalMillisecond(); + + /** + * set the millisecond for waiting udp sending and receiving + * + * @param waitUdpTotalMillisecond the millisecond for waiting udp sending and receiving + */ + void setWaitUdpTotalMillisecond(int waitUdpTotalMillisecond); + + /** + * get the threshold for how many correct broadcast should be received + * + * @return the threshold for how many correct broadcast should be received + */ + int getThresholdSucBroadcastCount(); + + /** + * get the count of expect task results + * + * @return the count of expect task results + */ + int getExpectTaskResultCount(); + + /** + * set the count of expect task results + * + * @param expectTaskResultCount the count of expect task results + */ + void setExpectTaskResultCount(int expectTaskResultCount); + + /** + * Set broadcast or multicast + * + * @param broadcast true is broadcast, false is multicast + */ + void setBroadcast(boolean broadcast); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__EsptouchTask.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__EsptouchTask.java new file mode 100644 index 00000000..0ffa7812 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__EsptouchTask.java @@ -0,0 +1,351 @@ +package com.espressif.iot.esptouch.task; + +import android.content.Context; +import android.os.Looper; +import android.util.Log; + +import com.espressif.iot.esptouch.EsptouchResult; +import com.espressif.iot.esptouch.IEsptouchListener; +import com.espressif.iot.esptouch.IEsptouchResult; +import com.espressif.iot.esptouch.IEsptouchTask; +import com.espressif.iot.esptouch.protocol.EsptouchGenerator; +import com.espressif.iot.esptouch.protocol.TouchData; +import com.espressif.iot.esptouch.security.ITouchEncryptor; +import com.espressif.iot.esptouch.udp.UDPSocketClient; +import com.espressif.iot.esptouch.udp.UDPSocketServer; +import com.espressif.iot.esptouch.util.ByteUtil; +import com.espressif.iot.esptouch.util.TouchNetUtil; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class __EsptouchTask implements __IEsptouchTask { + /** + * one indivisible data contain 3 9bits info + */ + private static final int ONE_DATA_LEN = 3; + + private static final String TAG = "__EsptouchTask"; + + private final UDPSocketClient mSocketClient; + private final UDPSocketServer mSocketServer; + private final byte[] mApSsid; + private final byte[] mApPassword; + private final byte[] mApBssid; + private final ITouchEncryptor mEncryptor; + private final Context mContext; + private final List mEsptouchResultList; + private volatile boolean mIsSuc = false; + private volatile boolean mIsInterrupt = false; + private volatile boolean mIsExecuted = false; + private AtomicBoolean mIsCancelled; + private IEsptouchTaskParameter mParameter; + private volatile Map mBssidTaskSucCountMap; + private IEsptouchListener mEsptouchListener; + private Thread mTask; + + public __EsptouchTask(Context context, TouchData apSsid, TouchData apBssid, TouchData apPassword, + ITouchEncryptor encryptor, IEsptouchTaskParameter parameter) { + Log.i(TAG, "Welcome Esptouch " + IEsptouchTask.ESPTOUCH_VERSION); + mContext = context; + mEncryptor = encryptor; + mApSsid = apSsid.getData(); + mApPassword = apPassword.getData(); + mApBssid = apBssid.getData(); + mIsCancelled = new AtomicBoolean(false); + mSocketClient = new UDPSocketClient(); + mParameter = parameter; + mSocketServer = new UDPSocketServer(mParameter.getPortListening(), + mParameter.getWaitUdpTotalMillisecond(), context); + mEsptouchResultList = new ArrayList<>(); + mBssidTaskSucCountMap = new HashMap<>(); + } + + private void __putEsptouchResult(boolean isSuc, String bssid, InetAddress inetAddress) { + synchronized (mEsptouchResultList) { + // check whether the result receive enough UDP response + boolean isTaskSucCountEnough = false; + Integer count = mBssidTaskSucCountMap.get(bssid); + if (count == null) { + count = 0; + } + ++count; + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "__putEsptouchResult(): count = " + count); + } + mBssidTaskSucCountMap.put(bssid, count); + isTaskSucCountEnough = count >= mParameter + .getThresholdSucBroadcastCount(); + if (!isTaskSucCountEnough) { + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "__putEsptouchResult(): count = " + count + + ", isn't enough"); + } + return; + } + // check whether the result is in the mEsptouchResultList already + boolean isExist = false; + for (IEsptouchResult esptouchResultInList : mEsptouchResultList) { + if (esptouchResultInList.getBssid().equals(bssid)) { + isExist = true; + break; + } + } + // only add the result who isn't in the mEsptouchResultList + if (!isExist) { + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "__putEsptouchResult(): put one more result " + + "bssid = " + bssid + " , address = " + inetAddress); + } + final IEsptouchResult esptouchResult = new EsptouchResult(isSuc, + bssid, inetAddress); + mEsptouchResultList.add(esptouchResult); + if (mEsptouchListener != null) { + mEsptouchListener.onEsptouchResultAdded(esptouchResult); + } + } + } + } + + private List __getEsptouchResultList() { + synchronized (mEsptouchResultList) { + if (mEsptouchResultList.isEmpty()) { + EsptouchResult esptouchResultFail = new EsptouchResult(false, + null, null); + esptouchResultFail.setIsCancelled(mIsCancelled.get()); + mEsptouchResultList.add(esptouchResultFail); + } + + return mEsptouchResultList; + } + } + + private synchronized void __interrupt() { + if (!mIsInterrupt) { + mIsInterrupt = true; + mSocketClient.interrupt(); + mSocketServer.interrupt(); + // interrupt the current Thread which is used to wait for udp response + if (mTask != null) { + mTask.interrupt(); + mTask = null; + } + } + } + + @Override + public void interrupt() { + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "interrupt()"); + } + mIsCancelled.set(true); + __interrupt(); + } + + private void __listenAsyn(final int expectDataLen) { + mTask = new Thread() { + public void run() { + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "__listenAsyn() start"); + } + long startTimestamp = System.currentTimeMillis(); +// byte[] apSsidAndPassword = ByteUtil.getBytesByString(mApSsid +// + mApPassword); + byte expectOneByte = (byte) (mApSsid.length + mApPassword.length + 9); + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "expectOneByte: " + expectOneByte); + } + byte receiveOneByte = -1; + byte[] receiveBytes = null; + while (mEsptouchResultList.size() < mParameter + .getExpectTaskResultCount() && !mIsInterrupt) { + receiveBytes = mSocketServer + .receiveSpecLenBytes(expectDataLen); + if (receiveBytes != null) { + receiveOneByte = receiveBytes[0]; + } else { + receiveOneByte = -1; + } + if (receiveOneByte == expectOneByte) { + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "receive correct broadcast"); + } + // change the socket's timeout + long consume = System.currentTimeMillis() + - startTimestamp; + int timeout = (int) (mParameter + .getWaitUdpTotalMillisecond() - consume); + if (timeout < 0) { + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "esptouch timeout"); + } + break; + } else { + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "mSocketServer's new timeout is " + + timeout + " milliseconds"); + } + mSocketServer.setSoTimeout(timeout); + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "receive correct broadcast"); + } + if (receiveBytes != null) { + String bssid = ByteUtil.parseBssid( + receiveBytes, + mParameter.getEsptouchResultOneLen(), + mParameter.getEsptouchResultMacLen()); + InetAddress inetAddress = TouchNetUtil + .parseInetAddr( + receiveBytes, + mParameter + .getEsptouchResultOneLen() + + mParameter + .getEsptouchResultMacLen(), + mParameter + .getEsptouchResultIpLen()); + __putEsptouchResult(true, bssid, inetAddress); + } + } + } else { + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "receive rubbish message, just ignore"); + } + } + } + mIsSuc = mEsptouchResultList.size() >= mParameter + .getExpectTaskResultCount(); + __EsptouchTask.this.__interrupt(); + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "__listenAsyn() finish"); + } + } + }; + mTask.start(); + } + + private boolean __execute(IEsptouchGenerator generator) { + + long startTime = System.currentTimeMillis(); + long currentTime = startTime; + long lastTime = currentTime - mParameter.getTimeoutTotalCodeMillisecond(); + + byte[][] gcBytes2 = generator.getGCBytes2(); + byte[][] dcBytes2 = generator.getDCBytes2(); + + int index = 0; + while (!mIsInterrupt) { + if (currentTime - lastTime >= mParameter.getTimeoutTotalCodeMillisecond()) { + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "send gc code "); + } + // send guide code + while (!mIsInterrupt + && System.currentTimeMillis() - currentTime < mParameter + .getTimeoutGuideCodeMillisecond()) { + mSocketClient.sendData(gcBytes2, + mParameter.getTargetHostname(), + mParameter.getTargetPort(), + mParameter.getIntervalGuideCodeMillisecond()); + // check whether the udp is send enough time + if (System.currentTimeMillis() - startTime > mParameter.getWaitUdpSendingMillisecond()) { + break; + } + } + lastTime = currentTime; + } else { + mSocketClient.sendData(dcBytes2, index, ONE_DATA_LEN, + mParameter.getTargetHostname(), + mParameter.getTargetPort(), + mParameter.getIntervalDataCodeMillisecond()); + index = (index + ONE_DATA_LEN) % dcBytes2.length; + } + currentTime = System.currentTimeMillis(); + // check whether the udp is send enough time + if (currentTime - startTime > mParameter.getWaitUdpSendingMillisecond()) { + break; + } + } + + return mIsSuc; + } + + private void __checkTaskValid() { + // !!!NOTE: the esptouch task could be executed only once + if (this.mIsExecuted) { + throw new IllegalStateException( + "the Esptouch task could be executed only once"); + } + this.mIsExecuted = true; + } + + @Override + public IEsptouchResult executeForResult() throws RuntimeException { + return executeForResults(1).get(0); + } + + @Override + public boolean isCancelled() { + return this.mIsCancelled.get(); + } + + @Override + public List executeForResults(int expectTaskResultCount) + throws RuntimeException { + __checkTaskValid(); + + mParameter.setExpectTaskResultCount(expectTaskResultCount); + + if (__IEsptouchTask.DEBUG) { + Log.d(TAG, "execute()"); + } + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new RuntimeException( + "Don't call the esptouch Task at Main(UI) thread directly."); + } + InetAddress localInetAddress = TouchNetUtil.getLocalInetAddress(mContext); + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "localInetAddress: " + localInetAddress); + } + // generator the esptouch byte[][] to be transformed, which will cost + // some time(maybe a bit much) + IEsptouchGenerator generator = new EsptouchGenerator(mApSsid, mApBssid, + mApPassword, localInetAddress, mEncryptor); + // listen the esptouch result asyn + __listenAsyn(mParameter.getEsptouchResultTotalLen()); + boolean isSuc = false; + for (int i = 0; i < mParameter.getTotalRepeatTime(); i++) { + isSuc = __execute(generator); + if (isSuc) { + return __getEsptouchResultList(); + } + } + + if (!mIsInterrupt) { + // wait the udp response without sending udp broadcast + try { + Thread.sleep(mParameter.getWaitUdpReceivingMillisecond()); + } catch (InterruptedException e) { + // receive the udp broadcast or the user interrupt the task + if (this.mIsSuc) { + return __getEsptouchResultList(); + } else { + this.__interrupt(); + return __getEsptouchResultList(); + } + } + this.__interrupt(); + } + + return __getEsptouchResultList(); + } + + @Override + public void setEsptouchListener(IEsptouchListener esptouchListener) { + mEsptouchListener = esptouchListener; + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__IEsptouchTask.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__IEsptouchTask.java new file mode 100644 index 00000000..216e09b9 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/task/__IEsptouchTask.java @@ -0,0 +1,55 @@ +package com.espressif.iot.esptouch.task; + +import com.espressif.iot.esptouch.IEsptouchListener; +import com.espressif.iot.esptouch.IEsptouchResult; + +import java.util.List; + +/** + * IEsptouchTask defined the task of esptouch should offer. INTERVAL here means + * the milliseconds of interval of the step. REPEAT here means the repeat times + * of the step. + * + * @author afunx + */ +public interface __IEsptouchTask { + + /** + * Turn on or off the log. + */ + static final boolean DEBUG = true; + + /** + * set the esptouch listener, when one device is connected to the Ap, it will be called back + * + * @param esptouchListener when one device is connected to the Ap, it will be called back + */ + void setEsptouchListener(IEsptouchListener esptouchListener); + + /** + * Interrupt the Esptouch Task when User tap back or close the Application. + */ + void interrupt(); + + /** + * Note: !!!Don't call the task at UI Main Thread or RuntimeException will + * be thrown Execute the Esptouch Task and return the result + * + * @return the IEsptouchResult + * @throws RuntimeException + */ + IEsptouchResult executeForResult() throws RuntimeException; + + /** + * Note: !!!Don't call the task at UI Main Thread or RuntimeException will + * be thrown Execute the Esptouch Task and return the result + * + * @param expectTaskResultCount the expect result count(if expectTaskResultCount <= 0, + * expectTaskResultCount = Integer.MAX_VALUE) + * @return the list of IEsptouchResult + * @throws RuntimeException + */ + List executeForResults(int expectTaskResultCount) throws RuntimeException; + + boolean isCancelled(); +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketClient.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketClient.java new file mode 100644 index 00000000..7ca7880e --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketClient.java @@ -0,0 +1,130 @@ +package com.espressif.iot.esptouch.udp; + +import android.util.Log; + +import com.espressif.iot.esptouch.task.__IEsptouchTask; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +/** + * this class is used to help send UDP data according to length + * + * @author afunx + */ +public class UDPSocketClient { + + private static final String TAG = "UDPSocketClient"; + private DatagramSocket mSocket; + private volatile boolean mIsStop; + private volatile boolean mIsClosed; + + public UDPSocketClient() { + try { + this.mSocket = new DatagramSocket(); + this.mIsStop = false; + this.mIsClosed = false; + } catch (SocketException e) { + if (__IEsptouchTask.DEBUG) { + Log.w(TAG, "SocketException"); + } + e.printStackTrace(); + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + public void interrupt() { + if (__IEsptouchTask.DEBUG) { + Log.i(TAG, "USPSocketClient is interrupt"); + } + this.mIsStop = true; + } + + /** + * close the UDP socket + */ + public synchronized void close() { + if (!this.mIsClosed) { + this.mSocket.close(); + this.mIsClosed = true; + } + } + + /** + * send the data by UDP + * + * @param data the data to be sent + * @param targetPort the port of target + * @param interval the milliseconds to between each UDP sent + */ + public void sendData(byte[][] data, String targetHostName, int targetPort, + long interval) { + sendData(data, 0, data.length, targetHostName, targetPort, interval); + } + + + /** + * send the data by UDP + * + * @param data the data to be sent + * @param offset the offset which data to be sent + * @param count the count of the data + * @param targetPort the port of target + * @param interval the milliseconds to between each UDP sent + */ + public void sendData(byte[][] data, int offset, int count, + String targetHostName, int targetPort, long interval) { + if ((data == null) || (data.length <= 0)) { + if (__IEsptouchTask.DEBUG) { + Log.w(TAG, "sendData(): data == null or length <= 0"); + } + return; + } + for (int i = offset; !mIsStop && i < offset + count; i++) { + if (data[i].length == 0) { + continue; + } + try { + InetAddress targetInetAddress = InetAddress.getByName(targetHostName); + DatagramPacket localDatagramPacket = new DatagramPacket( + data[i], data[i].length, targetInetAddress, targetPort); + this.mSocket.send(localDatagramPacket); + } catch (UnknownHostException e) { + if (__IEsptouchTask.DEBUG) { + Log.w(TAG, "sendData(): UnknownHostException"); + } + e.printStackTrace(); + mIsStop = true; + break; + } catch (IOException e) { + if (__IEsptouchTask.DEBUG) { + Log.w(TAG, "sendData(): IOException, but just ignore it"); + } + // for the Ap will make some troubles when the phone send too many UDP packets, + // but we don't expect the UDP packet received by others, so just ignore it + } + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + e.printStackTrace(); + if (__IEsptouchTask.DEBUG) { + Log.w(TAG, "sendData is Interrupted"); + } + mIsStop = true; + break; + } + } + if (mIsStop) { + close(); + } + } +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketServer.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketServer.java new file mode 100644 index 00000000..049f3ffc --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/udp/UDPSocketServer.java @@ -0,0 +1,149 @@ +package com.espressif.iot.esptouch.udp; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.util.Log; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.Arrays; + +public class UDPSocketServer { + private static final String TAG = "UDPSocketServer"; + private DatagramSocket mServerSocket; + private Context mContext; + private WifiManager.MulticastLock mLock; + private volatile boolean mIsClosed; + + /** + * Constructor of UDP Socket Server + * + * @param port the Socket Server port + * @param socketTimeout the socket read timeout + * @param context the context of the Application + */ + public UDPSocketServer(int port, int socketTimeout, Context context) { + this.mContext = context; + try { + this.mServerSocket = new DatagramSocket(null); + this.mServerSocket.setReuseAddress(true); + this.mServerSocket.bind(new InetSocketAddress(port)); + this.mServerSocket.setSoTimeout(socketTimeout); + } catch (IOException e) { + Log.w(TAG, "IOException"); + e.printStackTrace(); + } + this.mIsClosed = false; + WifiManager manager = (WifiManager) mContext.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + mLock = manager.createMulticastLock("test wifi"); + Log.d(TAG, "mServerSocket is created, socket read timeout: " + + socketTimeout + ", port: " + port); + } + + private synchronized void acquireLock() { + if (mLock != null && !mLock.isHeld()) { + mLock.acquire(); + } + } + + private synchronized void releaseLock() { + if (mLock != null && mLock.isHeld()) { + try { + mLock.release(); + } catch (Throwable th) { + // ignoring this exception, probably wakeLock was already released + } + } + } + + /** + * Set the socket timeout in milliseconds + * + * @param timeout the timeout in milliseconds or 0 for no timeout. + * @return true whether the timeout is set suc + */ + public boolean setSoTimeout(int timeout) { + try { + this.mServerSocket.setSoTimeout(timeout); + return true; + } catch (SocketException e) { + e.printStackTrace(); + } + return false; + } + + /** + * Receive one byte from the port and convert it into String + * + * @return + */ + public byte receiveOneByte() { + Log.d(TAG, "receiveOneByte() entrance"); + try { + acquireLock(); + DatagramPacket packet = new DatagramPacket(new byte[1], 1); + mServerSocket.receive(packet); + Log.d(TAG, "receive: " + (packet.getData()[0])); + return packet.getData()[0]; + } catch (Exception e) { + e.printStackTrace(); + } + return -1; + } + + /** + * Receive specific length bytes from the port and convert it into String + * 21,24,-2,52,-102,-93,-60 + * 15,18,fe,34,9a,a3,c4 + * + * @return + */ + public byte[] receiveSpecLenBytes(int len) { + Log.d(TAG, "receiveSpecLenBytes() entrance: len = " + len); + try { + acquireLock(); + DatagramPacket packet = new DatagramPacket(new byte[64], 64); + mServerSocket.receive(packet); + byte[] recDatas = Arrays.copyOf(packet.getData(), packet.getLength()); + Log.d(TAG, "received len : " + recDatas.length); + for (int i = 0; i < recDatas.length; i++) { + Log.w(TAG, "recDatas[" + i + "]:" + recDatas[i]); + } + Log.w(TAG, "receiveSpecLenBytes: " + new String(recDatas)); + if (recDatas.length != len) { + Log.w(TAG, + "received len is different from specific len, return null"); + return null; + } + return recDatas; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public void interrupt() { + Log.i(TAG, "USPSocketServer is interrupt"); + close(); + } + + public synchronized void close() { + if (!this.mIsClosed) { + Log.w(TAG, "mServerSocket is closed"); + mServerSocket.close(); + releaseLock(); + this.mIsClosed = true; + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/ByteUtil.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/ByteUtil.java new file mode 100644 index 00000000..15c85db4 --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/ByteUtil.java @@ -0,0 +1,323 @@ +package com.espressif.iot.esptouch.util; + +import java.io.UnsupportedEncodingException; +import java.util.Random; + +/** + * In Java, it don't support unsigned int, so we use char to replace uint8. + * The range of byte is [-128,127], and the range of char is [0,65535]. + * So the byte could used to store the uint8. + * (We assume that the String could be mapped to assic) + * + * @author afunx + */ +public class ByteUtil { + + public static final String ESPTOUCH_ENCODING_CHARSET = "UTF-8"; + + /** + * Put String to byte[] + * + * @param destbytes the byte[] of dest + * @param srcString the String of src + * @param destOffset the offset of byte[] + * @param srcOffset the offset of String + * @param count the count of dest, and the count of src as well + */ + public static void putString2bytes(byte[] destbytes, String srcString, + int destOffset, int srcOffset, int count) { + for (int i = 0; i < count; i++) { + destbytes[count + i] = srcString.getBytes()[i]; + } + } + + /** + * Convert uint8 into char( we treat char as uint8) + * + * @param uint8 the unit8 to be converted + * @return the byte of the unint8 + */ + public static byte convertUint8toByte(char uint8) { + if (uint8 > Byte.MAX_VALUE - Byte.MIN_VALUE) { + throw new RuntimeException("Out of Boundary"); + } + return (byte) uint8; + } + + /** + * Convert char into uint8( we treat char as uint8 ) + * + * @param b the byte to be converted + * @return the char(uint8) + */ + public static char convertByte2Uint8(byte b) { + // char will be promoted to int for char don't support & operator + // & 0xff could make negatvie value to positive + return (char) (b & 0xff); + } + + /** + * Convert byte[] into char[]( we treat char[] as uint8[]) + * + * @param bytes the byte[] to be converted + * @return the char[](uint8[]) + */ + public static char[] convertBytes2Uint8s(byte[] bytes) { + int len = bytes.length; + char[] uint8s = new char[len]; + for (int i = 0; i < len; i++) { + uint8s[i] = convertByte2Uint8(bytes[i]); + } + return uint8s; + } + + /** + * Put byte[] into char[]( we treat char[] as uint8[]) + * + * @param destUint8s the char[](uint8[]) array + * @param srcBytes the byte[] + * @param destOffset the offset of char[](uint8[]) + * @param srcOffset the offset of byte[] + * @param count the count of dest, and the count of src as well + */ + public static void putbytes2Uint8s(char[] destUint8s, byte[] srcBytes, + int destOffset, int srcOffset, int count) { + for (int i = 0; i < count; i++) { + destUint8s[destOffset + i] = convertByte2Uint8(srcBytes[srcOffset + + i]); + } + } + + /** + * Convert byte to Hex String + * + * @param b the byte to be converted + * @return the Hex String + */ + public static String convertByte2HexString(byte b) { + char u8 = convertByte2Uint8(b); + return Integer.toHexString(u8); + } + + /** + * Convert char(uint8) to Hex String + * + * @param u8 the char(uint8) to be converted + * @return the Hex String + */ + public static String convertU8ToHexString(char u8) { + return Integer.toHexString(u8); + } + + /** + * Split uint8 to 2 bytes of high byte and low byte. e.g. 20 = 0x14 should + * be split to [0x01,0x04] 0x01 is high byte and 0x04 is low byte + * + * @param uint8 the char(uint8) + * @return the high and low bytes be split, byte[0] is high and byte[1] is + * low + */ + public static byte[] splitUint8To2bytes(char uint8) { + if (uint8 < 0 || uint8 > 0xff) { + throw new RuntimeException("Out of Boundary"); + } + String hexString = Integer.toHexString(uint8); + byte low; + byte high; + if (hexString.length() > 1) { + high = (byte) Integer.parseInt(hexString.substring(0, 1), 16); + low = (byte) Integer.parseInt(hexString.substring(1, 2), 16); + } else { + high = 0; + low = (byte) Integer.parseInt(hexString.substring(0, 1), 16); + } + byte[] result = new byte[]{high, low}; + return result; + } + + /** + * Combine 2 bytes (high byte and low byte) to one whole byte + * + * @param high the high byte + * @param low the low byte + * @return the whole byte + */ + public static byte combine2bytesToOne(byte high, byte low) { + if (high < 0 || high > 0xf || low < 0 || low > 0xf) { + throw new RuntimeException("Out of Boundary"); + } + return (byte) (high << 4 | low); + } + + /** + * Combine 2 bytes (high byte and low byte) to + * + * @param high the high byte + * @param low the low byte + * @return the char(u8) + */ + public static char combine2bytesToU16(byte high, byte low) { + char highU8 = convertByte2Uint8(high); + char lowU8 = convertByte2Uint8(low); + return (char) (highU8 << 8 | lowU8); + } + + /** + * Generate the random byte to be sent + * + * @return the random byte + */ + private static byte randomByte() { + return (byte) (127 - new Random().nextInt(256)); + } + + /** + * Generate the random byte to be sent + * + * @param len the len presented by u8 + * @return the byte[] to be sent + */ + public static byte[] randomBytes(char len) { + byte[] data = new byte[len]; + for (int i = 0; i < len; i++) { + data[i] = randomByte(); + } + return data; + } + + public static byte[] genSpecBytes(char len) { + byte[] data = new byte[len]; + for (int i = 0; i < len; i++) { + data[i] = '1'; + } + return data; + } + + /** + * Generate the random byte to be sent + * + * @param len the len presented by byte + * @return the byte[] to be sent + */ + public static byte[] randomBytes(byte len) { + char u8 = convertByte2Uint8(len); + return randomBytes(u8); + } + + /** + * Generate the specific byte to be sent + * + * @param len the len presented by byte + * @return the byte[] + */ + public static byte[] genSpecBytes(byte len) { + char u8 = convertByte2Uint8(len); + return genSpecBytes(u8); + } + + public static String parseBssid(byte[] bssidBytes, int offset, int count) { + byte[] bytes = new byte[count]; + System.arraycopy(bssidBytes, offset, bytes, 0, count); + return parseBssid(bytes); + } + + /** + * parse "24,-2,52,-102,-93,-60" to "18,fe,34,9a,a3,c4" + * parse the bssid from hex to String + * + * @param bssidBytes the hex bytes bssid, e.g. {24,-2,52,-102,-93,-60} + * @return the String of bssid, e.g. 18fe349aa3c4 + */ + public static String parseBssid(byte[] bssidBytes) { + StringBuilder sb = new StringBuilder(); + int k; + String hexK; + String str; + for (byte bssidByte : bssidBytes) { + k = 0xff & bssidByte; + hexK = Integer.toHexString(k); + str = ((k < 16) ? ("0" + hexK) : (hexK)); + System.out.println(str); + sb.append(str); + } + return sb.toString(); + } + + /** + * @param string the string to be used + * @return the byte[] of String according to {@link #ESPTOUCH_ENCODING_CHARSET} + */ + public static byte[] getBytesByString(String string) { + try { + return string.getBytes(ESPTOUCH_ENCODING_CHARSET); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("the charset is invalid"); + } + } + + private static void test_splitUint8To2bytes() { + // 20 = 0x14 + byte[] result = splitUint8To2bytes((char) 20); + if (result[0] == 1 && result[1] == 4) { + System.out.println("test_splitUint8To2bytes(): pass"); + } else { + System.out.println("test_splitUint8To2bytes(): fail"); + } + } + + private static void test_combine2bytesToOne() { + byte high = 0x01; + byte low = 0x04; + if (combine2bytesToOne(high, low) == 20) { + System.out.println("test_combine2bytesToOne(): pass"); + } else { + System.out.println("test_combine2bytesToOne(): fail"); + } + } + + private static void test_convertChar2Uint8() { + byte b1 = 'a'; + // -128: 1000 0000 should be 128 in unsigned char + // -1: 1111 1111 should be 255 in unsigned char + byte b2 = (byte) -128; + byte b3 = (byte) -1; + if (convertByte2Uint8(b1) == 97 && convertByte2Uint8(b2) == 128 + && convertByte2Uint8(b3) == 255) { + System.out.println("test_convertChar2Uint8(): pass"); + } else { + System.out.println("test_convertChar2Uint8(): fail"); + } + } + + private static void test_convertUint8toByte() { + char c1 = 'a'; + // 128: 1000 0000 should be -128 in byte + // 255: 1111 1111 should be -1 in byte + char c2 = 128; + char c3 = 255; + if (convertUint8toByte(c1) == 97 && convertUint8toByte(c2) == -128 + && convertUint8toByte(c3) == -1) { + System.out.println("test_convertUint8toByte(): pass"); + } else { + System.out.println("test_convertUint8toByte(): fail"); + } + } + + private static void test_parseBssid() { + byte b[] = {15, -2, 52, -102, -93, -60}; + if (parseBssid(b).equals("0ffe349aa3c4")) { + System.out.println("test_parseBssid(): pass"); + } else { + System.out.println("test_parseBssid(): fail"); + } + } + + public static void main(String args[]) { + test_convertUint8toByte(); + test_convertChar2Uint8(); + test_splitUint8To2bytes(); + test_combine2bytesToOne(); + test_parseBssid(); + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/CRC8.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/CRC8.java new file mode 100644 index 00000000..20b35dba --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/CRC8.java @@ -0,0 +1,63 @@ +package com.espressif.iot.esptouch.util; + +import java.util.zip.Checksum; + +public class CRC8 implements Checksum { + + private static final short[] crcTable = new short[256]; + private static final short CRC_POLYNOM = 0x8c; + private static final short CRC_INITIAL = 0x00; + + static { + for (int dividend = 0; dividend < 256; dividend++) { + int remainder = dividend;// << 8; + for (int bit = 0; bit < 8; ++bit) + if ((remainder & 0x01) != 0) + remainder = (remainder >>> 1) ^ CRC_POLYNOM; + else + remainder >>>= 1; + crcTable[dividend] = (short) remainder; + } + } + + private final short init; + private short value; + + public CRC8() { + this.value = this.init = CRC_INITIAL; + } + + @Override + public void update(byte[] buffer, int offset, int len) { + for (int i = 0; i < len; i++) { + int data = buffer[offset + i] ^ value; + value = (short) (crcTable[data & 0xff] ^ (value << 8)); + } + } + + /** + * Updates the current checksum with the specified array of bytes. + * Equivalent to calling update(buffer, 0, buffer.length). + * + * @param buffer the byte array to update the checksum with + */ + public void update(byte[] buffer) { + update(buffer, 0, buffer.length); + } + + @Override + public void update(int b) { + update(new byte[]{(byte) b}, 0, 1); + } + + @Override + public long getValue() { + return value & 0xff; + } + + @Override + public void reset() { + value = init; + } + +} diff --git a/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/TouchNetUtil.java b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/TouchNetUtil.java new file mode 100644 index 00000000..d714600c --- /dev/null +++ b/android/esptouch/src/main/java/com/espressif/iot/esptouch/util/TouchNetUtil.java @@ -0,0 +1,118 @@ +package com.espressif.iot.esptouch.util; + +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class TouchNetUtil { + + /** + * get the local ip address by Android System + * + * @param context the context + * @return the local ip addr allocated by Ap + */ + public static InetAddress getLocalInetAddress(Context context) { + WifiManager wm = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + assert wm != null; + WifiInfo wifiInfo = wm.getConnectionInfo(); + int localAddrInt = wifiInfo.getIpAddress(); + String localAddrStr = __formatString(localAddrInt); + InetAddress localInetAddr = null; + try { + localInetAddr = InetAddress.getByName(localAddrStr); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return localInetAddr; + } + + private static String __formatString(int value) { + StringBuilder strValue = new StringBuilder(); + byte[] ary = __intToByteArray(value); + for (int i = ary.length - 1; i >= 0; i--) { + strValue.append(ary[i] & 0xFF); + if (i > 0) { + strValue.append("."); + } + } + return strValue.toString(); + } + + private static byte[] __intToByteArray(int value) { + byte[] b = new byte[4]; + for (int i = 0; i < 4; i++) { + int offset = (b.length - 1 - i) * 8; + b[i] = (byte) ((value >>> offset) & 0xFF); + } + return b; + } + + /** + * parse InetAddress + * + * @param inetAddrBytes + * @return + */ + public static InetAddress parseInetAddr(byte[] inetAddrBytes, int offset, + int count) { + InetAddress inetAddress = null; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append((inetAddrBytes[offset + i] & 0xff)); + if (i != count - 1) { + sb.append('.'); + } + } + try { + inetAddress = InetAddress.getByName(sb.toString()); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return inetAddress; + } + + /** + * parse bssid + * + * @param bssid the bssid like aa:bb:cc:dd:ee:ff + * @return byte converted from bssid + */ + public static byte[] parseBssid2bytes(String bssid) { + String[] bssidSplits = bssid.split(":"); + byte[] result = new byte[bssidSplits.length]; + for (int i = 0; i < bssidSplits.length; i++) { + result[i] = (byte) Integer.parseInt(bssidSplits[i], 16); + } + return result; + } + + public static byte[] getOriginalSsidBytes(WifiInfo info) { + try { + Method method = info.getClass().getMethod("getWifiSsid"); + method.setAccessible(true); + Object wifiSsid = method.invoke(info); + if (wifiSsid == null) { + return null; + } + method = wifiSsid.getClass().getMethod("getOctets"); + method.setAccessible(true); + return (byte[]) method.invoke(wifiSsid); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (NullPointerException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..5d43612f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,24 @@ +# 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=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# 是否打包APK,打正式包时请设置为true,使用正式的签名 +isNeedPackage=false +# 是否使用booster优化APK,这里需要注意gradle的版本,对于最新的gradle版本可能存在兼容问题 +isUseBooster=false +android.precompileDependenciesResources=false + +android.useAndroidX=true +android.enableJetifier=true + +android.enableD8=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac44e589 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 28 16:23:16 CST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## 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="" + +# 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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@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 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= + +@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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..002fcc6c --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +include ':app',':esptouch' diff --git a/android/versions.gradle b/android/versions.gradle new file mode 100644 index 00000000..534aad86 --- /dev/null +++ b/android/versions.gradle @@ -0,0 +1,173 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +/** + * Shared file between builds so that they can all use the same dependencies and + * maven repositories. + **/ +ext.deps = [:] +def versions = [:] +versions.android_gradle_plugin = "3.6.1" +versions.android_maven_gradle_plugin = "2.0" +versions.gradle_bintray_plugin = "1.8.0" +versions.booster = "3.1.0" +versions.booster_all = "1.1.1" +versions.support = "28.0.0" +versions.androidx = "1.1.0" +versions.junit = "4.12" +versions.espresso = "3.2.0" +versions.constraint_layout = "1.1.3" +versions.glide = "4.11.0" +versions.rxjava2 = "2.2.20" +versions.rxandroid = "2.1.1" +versions.rxbinding = "2.2.0" +versions.butterknife = "10.1.0" +versions.runner = "1.2.0" +versions.gson = "2.8.5" + +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.androidx" +androidx.appcompat = "androidx.appcompat:appcompat:$versions.androidx" +androidx.recyclerview = "androidx.recyclerview:recyclerview:$versions.androidx" +androidx.design = "com.google.android.material:material:$versions.androidx" +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" + +ext.deps = deps + +def build_versions = [:] +build_versions.min_sdk = 19 +build_versions.target_sdk = 28 +build_versions.build_tools = "28.0.3" +ext.build_versions = build_versions + +def app_release = [:] +app_release.storeFile = "../keystores/android.keystore" +app_release.storePassword = "xuexiang" +app_release.keyAlias = "android.keystore" +app_release.keyPassword = "xuexiang" + +ext.app_release = app_release + +/** + * @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.jcenter { url 'https://maven.aliyun.com/repository/jcenter' } + handler.mavenCentral { url 'https://maven.aliyun.com/repository/central' } + handler.maven { url "https://jitpack.io" } + handler.maven { url 'https://maven.aliyun.com/repository/public' } + handler.maven { url "https://dl.bintray.com/umsdk/release" } + handler.maven { url 'https://oss.sonatype.org/content/repositories/public' } + //Add the Local repository + handler.maven { url 'LocalRepository' } +} + +ext.addRepos = this.&addRepos + + +//自动添加XAOP和XRouter插件 +project.buildscript.configurations.each { configuration -> + if (configuration.name == "classpath") { + //XAOP插件 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XAOP:xaop-plugin:1.1.0')) + //XRouter插件 + configuration.dependencies.add(getProject().dependencies.create('com.github.xuexiangjys.XRouter:xrouter-plugin:1.0.1')) + } +} \ No newline at end of file