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