diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bc103f1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+## NSGK App
+
+> 首页地址列表
+
+| 名称 | 地址(实际需自行添加协议,地址,端口) | 图标 | 圆角图标 |
+|:-----|:-----|:-----|:-----|
+| 事项审批 | /yinnongLogin | ic_launcher_sxsp 或者 ic_launcher_yhzl | ic_launcher_sxsp_round 或者 ic_launcher_yhzl_round |
+| 阳光村务(村级事项) | /sunVillage_info/login | ic_launcher_ygcw | ic_launcher_ygcw_round |
+| 大托管 | /agriculturalTrusteeship/index | ic_launcher_dtg | ic_launcher_dtg_round |
+| 产权交易 | /index | ic_launcher_cqjy | ic_launcher_cqjy_round |
+| 农业执法 | /lawEnforcement/login | ic_launcher_nyzf | ic_launcher_nyzf_round |
+| 宅基地审批(乌市版) | /zjdLogin | ic_launcher_zjd | ic_launcher_zjd_round |
+| 宅基地调查 | /homesteadLogin | ic_launcher_zjddc | ic_launcher_zjddc_round |
+| 两清三化 | /homestead/login | ic_launcher_2q3h | ic_launcher_2q3h_round |
+| 网上家园(废) | /onlineHomeLogin | ic_launcher_white | ic_launcher_white_round |
+| 确权调查 | /contracted/login | ic_launcher_qqdc | ic_launcher_qqdc_round |
+| 一体机 | / | ic_launcher_njytj | ic_launcher_njytj_round |
+
+> 使用`Android Studio`修改步骤
+
+* 编辑`gradle.properties`文件
+* 配置```appHomeUrl=```首页地址, 不要加双引号
+* 配置```appName=```App桌面快捷方式名称, 可以直接使用字符串(需要加双引号, 如 "NSGK APP"), 也可以使用国际化字符串配置(不要加双引号, 如 @string/app_name)
+* 配置```appIconKey=```图标名称, 不要加双引号, 去掉前后缀(如 yhzl, 图标为ic_launcher_yhzl; 圆角图标为ic_launcher_yhzl_round)
+* 使用Android Studio生成签名包
+* 配置```appCopyright=```启动页版权文本, 不要加双引号
+* 配置```appVendor=```启动页厂商名称, 不要加双引号
+* 配置```appUpdateUrl=```apk下载更新地址, 不要加双引号
+* 生成的apk路径为 `/app/release/app-release.apk`
+
+> 使用`gradle`脚本打签名的正式包
+
+* 执行 ```gradlew assembleRelease``` 将使用`gradle.properties`文件里的配置进行打包
+* 如果需要自定义配置(无需修改`gradle.properties`文件), 执行 ```gradlew assembleRelease -PappHomeUrl="首页地址" -PappName="App桌面快捷方式名称(只能使用字符串)" -PappIconKey="图标名称" -PappIconKey="启动页版权文本" -PappCopyright="启动页厂商名称" -PappUpdateUrl="apk下载更新地址"``` 将使用命令行里的配置进行打包
+* 生成的apk路径为 `/app/build/outputs/apk/release/app-release.apk`
+
+> 帮助脚本
+
+* `打包-正式.bat`: 构建正式包(直接执行将使用`gradle.properties`文件里的配置进行打包, 完整命令行用法为 ```.\打包-正式.bat App主页链接地址 App名称 App图标简称 App启动页版权文本 App启动页厂商名称```, 此时会使用命令行里的配置进行打包. 生成的路径为`/app/build/outputs/apk/release/app-release.apk`)
+* `打包-debug.bat`: 构建Debug包(生成的路径为`/app/build/outputs/apk/debug/app-debug.apk`)
+* `安装-发布包.bat`: 将已打好的Release包安装至手机(手机需连接到电脑, 并且启用开发者模式)
+* `安装-调试包.bat`: 将已打好的Debug包安装至手机(手机需连接到电脑, 并且启用开发者模式)
+
+> 签名证书(安装签名的发布包需要先卸载已安装的debug包)
+
+* 路径: `/app-keystore.jks`
+* 密码: `ns61GK32x%`
+* Key Alias: `nsgk_rural_web`
+* Key Password: `ns61GK32x%`
+
+> 示例
+```shell
+.\打包-正式.bat http://mxixiaxian.nongshen.net/sunVillage_info/login_code_new 阳光三资 ygcw
+```
\ No newline at end of file
diff --git a/app-keystore.jks b/app-keystore.jks
new file mode 100644
index 0000000..132bf4f
Binary files /dev/null and b/app-keystore.jks differ
diff --git a/app/build.gradle b/app/build.gradle
index c5972a8..7c33dd3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,15 +10,66 @@ android {
minSdkVersion 24
targetSdkVersion 30
versionCode 1
- versionName "1.0"
+ versionName "1.0.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ // App首页链接地址
+ buildConfigField "String", "APP_HOME_URL", "\"${project.properties.appHomeUrl}\""
+
+ // App启动页版权文本
+ buildConfigField "String", "APP_COMPYRIGHT", "\"${project.properties.appCopyright}\""
+ // App启动页厂商名称
+ buildConfigField "String", "APP_VENDOR", "\"${project.properties.appVendor}\""
+
+ // App图标
+ buildConfigField "String", "APP_ICON", "\"${project.properties.appIconKey}\""
+
+ // App更新下载地址
+ buildConfigField "String", "APP_UPDATE_URL", "\"${project.properties.appUpdateUrl}\""
+
+ // AndroidManifest.xml占位符
+ manifestPlaceholders = [
+ // App名称
+ APP_NAME: "${project.properties.appName}",
+ // App图标
+ APP_ICON: "@mipmap/ic_launcher_${project.properties.appIconKey}",
+ // App圆角图标
+ APP_ROUND_ICON: "@mipmap/ic_launcher_${project.properties.appIconKey}_round",
+ // 高德key
+ AMAP_KEY: "${project.properties.amapKey}",
+ ]
+ }
+
+ signingConfigs {
+ release {
+ v1SigningEnabled true
+ v2SigningEnabled true
+ storeFile file(project.properties.storeFile)
+ storePassword project.properties.storePassword
+ keyAlias project.properties.keyAlias
+ keyPassword project.properties.keyPassword
+ }
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.release
+ resValue("string", "app_home_url", "\"${project.properties.appHomeUrl}\"")
+ resValue("string", "app_copyright", "\"${project.properties.appCopyright}\"")
+ resValue("string", "app_vendor", "\"${project.properties.appVendor}\"")
+ resValue("string", "app_name2", "\"${project.properties.appName}\"")
+ resValue("string", "app_update_url", "\"${project.properties.appUpdateUrl}\"")
+ }
+ debug {
+ signingConfig signingConfigs.release
+ resValue("string", "app_home_url", "\"${project.properties.appHomeUrl}\"")
+ resValue("string", "app_copyright", "\"${project.properties.appCopyright}\"")
+ resValue("string", "app_vendor", "\"${project.properties.appVendor}\"")
+ resValue("string", "app_name2", "\"${project.properties.appName}\"")
+ resValue("string", "app_update_url", "\"${project.properties.appUpdateUrl}\"")
}
}
compileOptions {
@@ -39,4 +90,9 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.github.Justson.AgentWeb:agentweb-core:v4.1.9-androidx' // (必选)
implementation 'com.github.Justson.AgentWeb:agentweb-filechooser:v4.1.9-androidx'
-}
\ No newline at end of file
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'cn.hutool:hutool-all:5.5.4'
+
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ //compile 'com.amap.api:location:latest.integration'
+}
diff --git a/app/libs/AMap_Location_V6.4.9_20241226.jar b/app/libs/AMap_Location_V6.4.9_20241226.jar
new file mode 100644
index 0000000..648692e
Binary files /dev/null and b/app/libs/AMap_Location_V6.4.9_20241226.jar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7961336..b5e001d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,33 +13,63 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/nsgk/ruralWeb/FullscreenActivity.java b/app/src/main/java/com/nsgk/ruralWeb/FullscreenActivity.java
index d776d22..ad23702 100644
--- a/app/src/main/java/com/nsgk/ruralWeb/FullscreenActivity.java
+++ b/app/src/main/java/com/nsgk/ruralWeb/FullscreenActivity.java
@@ -1,28 +1,62 @@
package com.nsgk.ruralWeb;
import android.annotation.SuppressLint;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
import android.view.KeyEvent;
+import android.view.View;
import android.view.ViewGroup;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.WebBackForwardList;
+import android.webkit.WebHistoryItem;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
import android.widget.LinearLayout;
+import android.widget.ProgressBar;
import android.widget.RelativeLayout;
+import android.widget.Toast;
import com.just.agentweb.AgentWeb;
+import com.just.agentweb.AgentWebConfig;
import com.just.agentweb.DefaultWebClient;
+import com.just.agentweb.WebChromeClient;
+import com.just.agentweb.WebViewClient;
+import com.nsgk.ruralWeb.sys.NSConstants;
+import com.nsgk.ruralWeb.sys.NSPreference;
+import com.nsgk.ruralWeb.utils.NSContextUtils;
+import com.nsgk.ruralWeb.utils.NSStr;
+import com.nsgk.ruralWeb.web.NSEnvWindowObject;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import cn.hutool.core.util.StrUtil;
+
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*/
public class FullscreenActivity extends AppCompatActivity {
+ private static final String ID_TAG = FullscreenActivity.class.getName();
+ public static final int PERMISSION_LOCATION_REQUEST_CODE = 0x1001;
private AgentWeb mAgentWeb;
+ private NSPreference preference;
+ private NSEnvWindowObject envWindowObject;
+
+ private String m_lastUrl;
+ private String m_mainUrl;
+ private View m_progressIndicator;
@SuppressLint("SetJavaScriptEnabled")
@Override
@@ -30,12 +64,71 @@ public class FullscreenActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fullscreen);
+ String appHomeUrl = GetHomeUrl();
+ boolean logcatConsole = GetPreference().GetBool(NSPreference.LOGCAT_CONSOLE_OUTPUT, NSContextUtils.BuildIsDebug(this));
+ if(logcatConsole)
+ Log.i(ID_TAG, logcatConsole ? "输出控制台到logcat" : "不输出控制台");
+ //appHomeUrl = "http://192.168.0.250:85/sunVillage_info/login";
+ Log.i(ID_TAG, "App home url: " + appHomeUrl);
// init();
- mAgentWeb = AgentWeb.with(this)//
- .setAgentWebParent((RelativeLayout) findViewById(R.id.ll), -1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))//传入AgentWeb的父控件。
+ AgentWeb.PreAgentWeb builder = AgentWeb.with(this)//
+ .setAgentWebParent(findViewById(R.id.ll), -1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))//传入AgentWeb的父控件。
.useDefaultIndicator(-1, 3)//设置进度条颜色与高度,-1为默认值,高度为2,单位为dp。
- .setWebViewClient(new com.just.agentweb.WebViewClient() {
+ .setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int newProgress)
+ {
+ super.onProgressChanged(view, newProgress);
+ if(newProgress != 100)
+ return;
+ String url = view.getUrl();
+ if(IsInDefaultPage())
+ {
+ m_mainUrl = url;
+ DumpMainUrl(url);
+ Log.i(ID_TAG, "设定主页地址: " + m_mainUrl);
+ }
+ else if(IsDefaultPage(url))
+ {
+ m_mainUrl = null;
+ Log.i(ID_TAG, "清空主页地址");
+ }
+ m_lastUrl = url;
+ }
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage consoleMessage)
+ {
+ if(logcatConsole)
+ {
+ String text = "[" + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber() + "] " + consoleMessage.message();
+ switch(consoleMessage.messageLevel())
+ {
+ case WARNING:
+ Log.w("Console", text);
+ break;
+ case ERROR:
+ Log.e("Console", text);
+ break;
+ case DEBUG:
+ Log.d("Console", text);
+ break;
+ case LOG:
+ default:
+ Log.i("Console", text);
+ break;
+ }
+ }
+ return super.onConsoleMessage(consoleMessage);
+ }
+
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
+ callback.invoke(origin, true, false);
+ super.onGeolocationPermissionsShowPrompt(origin, callback);
+ }
+ })
+ .setWebViewClient(new WebViewClient() {
})//WebViewClient , 与 WebView 使用一致 ,但是请勿获取WebView调用setWebViewClient(xx)方法了,会覆盖AgentWeb DefaultWebClient,同时相应的中间件也会失效。
.setSecurityType(AgentWeb.SecurityType.STRICT_CHECK) //严格模式 Android 4.2.2 以下会放弃注入对象 ,使用AgentWebView没影响。
.setMainFrameErrorView(R.layout.agentweb_error_page, -1) //参数1是错误显示的布局,参数2点击刷新控件ID -1表示点击整个布局都刷新, AgentWeb 3.0.0 加入。
@@ -43,7 +136,51 @@ public class FullscreenActivity extends AppCompatActivity {
.interceptUnkownUrl() //拦截找不到相关页面的Url AgentWeb 3.0.0 加入。
.createAgentWeb()//创建AgentWeb。
.ready()//设置 WebSettings。
- .go("http://218.59.175.43:71/homesteadLogin"); //WebView载入该url地址的页面并显示。
+ ;
+
+ boolean openDef = IsDefaultPage(appHomeUrl);
+
+ if(!openDef)
+ {
+ Log.i(ID_TAG, "载入主页: " + appHomeUrl);
+ RestoreCookie(); // 重载cookie
+ m_mainUrl = appHomeUrl;
+ }
+ else
+ {
+ Log.i(ID_TAG, "载入登录页");
+ GetPreference().Remove(NSPreference.COOKIES); // Remove cookies
+ }
+
+ mAgentWeb = builder.go(appHomeUrl); //WebView载入该url地址的页面并显示。
+
+ WebSettings settings = GetWebView().getSettings();
+
+ int fontScale = GetPreference().GetInt(NSPreference.FONT_SCALE);
+ Log.i(ID_TAG, "全局字体缩放: " + fontScale + "%");
+ if(fontScale > 0 && fontScale != 100)
+ settings.setTextZoom(fontScale);
+
+ settings.setDatabaseEnabled(true);
+ settings.setDomStorageEnabled(true);
+ settings.setGeolocationEnabled(true);
+
+ // 注入宿主对象
+ envWindowObject = new NSEnvWindowObject(this, new Handler(), GetWebView());
+ mAgentWeb.getJsInterfaceHolder().addJavaObject("_Native_object", envWindowObject);
+ mAgentWeb.getJsInterfaceHolder().addJavaObject("Android", envWindowObject);
+
+ // 等待条指示器
+ m_progressIndicator = getLayoutInflater().inflate(R.layout.location_indicator, null);
+ m_progressIndicator.findViewById(R.id.terminate_button).setOnClickListener((View view) -> {
+ envWindowObject.TerminateLocation();
+ });
+ m_progressIndicator.setVisibility(View.GONE);
+ m_progressIndicator.setZ(99);
+ RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ params.addRule(RelativeLayout.CENTER_IN_PARENT);
+ ((RelativeLayout)findViewById(R.id.ll)).addView(m_progressIndicator, params);
+
/* 上边url是各个APP项目的入口地址
事项审批 http://116.255.223.226:82/yinnongLogin 图标 ic_launcher_sxsp 或者 ic_launcher_yhzl
@@ -59,10 +196,15 @@ public class FullscreenActivity extends AppCompatActivity {
一体机 http://47.98.113.57:81 图标 ic_launcher_njytj
*/
+ Log.d(ID_TAG, "UI线程: " + Thread.currentThread().getId());
}
@Override
protected void onPause() {
+ if(IsRecordUrl())
+ {
+ DumpCookie();
+ }
mAgentWeb.getWebLifeCycle().onPause();
super.onPause();
}
@@ -75,25 +217,165 @@ public class FullscreenActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
+ if(!IsRecordUrl())
+ {
+ Log.d(ID_TAG, "Remove url and cookie record");
+ GetPreference().Remove(NSPreference.COOKIES);
+ GetPreference().Remove(NSPreference.LAST_ACCESS_URL);
+ }
mAgentWeb.getWebLifeCycle().onDestroy();
+ envWindowObject.OnDestroy();
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
-// if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
-// mWebView.goBack();
-// return true;
-// }
+ if (keyCode == KeyEvent.KEYCODE_BACK && HandleBackKeyEvent(1)) {
+ return true;
+ }
if (mAgentWeb.handleKeyEvent(keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
+ @Override
+ public void onBackPressed()
+ {
+ if(!HandleBackKeyEvent(2))
+ super.onBackPressed();
+ }
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == PERMISSION_LOCATION_REQUEST_CODE) {
+ int granted = 0;
+ for(int i = 0; i < permissions.length; i++)
+ {
+ boolean b = grantResults[i] == PackageManager.PERMISSION_GRANTED;
+ Log.i(ID_TAG, String.format("请求权限: %s -> %s", permissions[i], b ? "通过" : "拒绝"));
+ if(b)
+ granted++;
+ }
+ if (granted < permissions.length) {
+ Toast.makeText(this, "需要定位权限", Toast.LENGTH_LONG).show();
+ NSContextUtils.RequestLocationPermission(this, FullscreenActivity.PERMISSION_LOCATION_REQUEST_CODE);
+ }
+ }
+ }
+
+ private NSPreference GetPreference()
+ {
+ if(null == preference)
+ preference = new NSPreference(this);
+ return preference;
+ }
+
+ private boolean IsRecordUrl()
+ {
+ return GetPreference().GetBool(NSPreference.OPEN_LAST_URL);
+ }
+
+ private String GetHomeUrl()
+ {
+ String url = null;
+ if(IsRecordUrl())
+ url = GetPreference().GetString(NSPreference.LAST_ACCESS_URL, NSConstants.AppHomeUrl());
+ if(StrUtil.isBlank(url))
+ url = NSConstants.AppHomeUrl();
+ return url;
+ }
+
+ private WebView GetWebView()
+ {
+ return mAgentWeb.getWebCreator().getWebView();
+ }
+
+ private void DumpMainUrl(String url)
+ {
+ if(StrUtil.isNotEmpty(url))
+ GetPreference().SetString(NSPreference.LAST_ACCESS_URL, url);
+ else
+ GetPreference().Remove(NSPreference.LAST_ACCESS_URL);
+ Log.d(ID_TAG, "Dump URL: " + url);
+ }
+
+ private void DumpCookie()
+ {
+ String url = NSStr.GetUrlPart(NSConstants.AppHomeUrl());
+ String cookies = AgentWebConfig.getCookiesByUrl(url);
+ GetPreference().SetString(NSPreference.COOKIES, cookies);
+ Log.d(ID_TAG, "Dump cookie: " + "url=" + url + ", cookie=" + cookies);
+ }
+
+ private void RestoreCookie()
+ {
+ String cookies = GetPreference().GetString(NSPreference.COOKIES);
+ if(StrUtil.isNotBlank(cookies))
+ {
+ String url = NSStr.GetUrlPart(NSConstants.AppHomeUrl());
+ AgentWebConfig.syncCookie(url, cookies);
+ Log.d(ID_TAG, "Restore cookie: " + "url=" + url + ", cookie=" + cookies);
+ }
+ }
+
+ private boolean IsInDefaultPage()
+ {
+ return IsDefaultPage(m_lastUrl);
+ }
+
+ private boolean IsDefaultPage(String url)
+ {
+ return NSConstants.AppHomeUrl().equals(url);
+ }
+
+ private void History()
+ {
+ WebBackForwardList webBackForwardList = GetWebView().copyBackForwardList();
+ for(int i = 0; i < webBackForwardList.getSize(); i++)
+ {
+ WebHistoryItem item = webBackForwardList.getItemAtIndex(i);
+ Log.d(ID_TAG, StrUtil.format("{}: {} {}", i, item.getTitle(), item.getUrl()));
+ }
+ }
+
+ public void SetIndicatorVisible(boolean on)
+ {
+ m_progressIndicator.setVisibility(on ? View.VISIBLE : View.GONE);
+ }
+
+ private boolean HandleBackKeyEvent(int from)
+ {
+ if(!IsRecordUrl())
+ return false;
+ WebView webView = GetWebView();
+ if(null == webView)
+ return false;
+
+ boolean inDefaultPage = IsInDefaultPage();
+ //System.err.println(m_lastUrl +" -> " + inDefaultPage + " = " + webView.canGoBack());
+ if(inDefaultPage)
+ {
+ if(from == 1 && !webView.canGoBack())
+ {
+ finish();
+ return true;
+ }
+ else
+ return false;
+ }
+ if (!webView.canGoBack() && !inDefaultPage) {
+ webView.loadUrl(NSConstants.AppHomeUrl());
+ webView.clearHistory();
+ //History();
+ return true;
+ }
+ return false;
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nsgk/ruralWeb/SettingsActivity.java b/app/src/main/java/com/nsgk/ruralWeb/SettingsActivity.java
new file mode 100644
index 0000000..af7d01f
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/SettingsActivity.java
@@ -0,0 +1,27 @@
+package com.nsgk.ruralWeb;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.nsgk.ruralWeb.ui.SettingsFragment;
+
+public class SettingsActivity extends AppCompatActivity
+{
+ private static final String ID_TAG = SettingsActivity.class.getName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ setTitle(R.string.settings_name);
+
+ getSupportFragmentManager().beginTransaction().replace(android.R.id.content, new SettingsFragment()).commit();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/WelcomeActivity.java b/app/src/main/java/com/nsgk/ruralWeb/WelcomeActivity.java
index de561e9..f68d558 100644
--- a/app/src/main/java/com/nsgk/ruralWeb/WelcomeActivity.java
+++ b/app/src/main/java/com/nsgk/ruralWeb/WelcomeActivity.java
@@ -9,10 +9,13 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
+import com.nsgk.ruralWeb.sys.NSConstants;
+
public class WelcomeActivity extends AppCompatActivity {
private ImageView imageView;
private LinearLayout llcenter;
@@ -36,12 +39,38 @@ public class WelcomeActivity extends AppCompatActivity {
}
};
+ private String GetTrimString(String str)
+ {
+ if(null == str || str.isEmpty())
+ return null;
+ str = str.trim();
+ if(str.isEmpty())
+ return null;
+ return str;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_welcome);
imageView = findViewById(R.id.imageView);
llcenter = findViewById(R.id.llcenter);
+
+ // 动态设置启动页版权信息
+ String text = GetTrimString(NSConstants.AppCopyright());
+ if(null != text)
+ {
+ TextView copyrightText = findViewById(R.id.welcome_text_copyright);
+ copyrightText.setText(text);
+ }
+ // 动态设置启动页厂商名称
+ text = GetTrimString(NSConstants.AppVendor());
+ if(null != text)
+ {
+ TextView vendorText = findViewById(R.id.welcome_text_vendor);
+ vendorText.setText(text);
+ }
+
Animation animation = AnimationUtils.loadAnimation(this, R.anim.img_anim);
animation.start();
handler.sendEmptyMessageDelayed(1, 1000);
diff --git a/app/src/main/java/com/nsgk/ruralWeb/enums/NSEnums.java b/app/src/main/java/com/nsgk/ruralWeb/enums/NSEnums.java
new file mode 100644
index 0000000..cb9a5e2
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/enums/NSEnums.java
@@ -0,0 +1,23 @@
+package com.nsgk.ruralWeb.enums;
+
+public final class NSEnums
+{
+ public final class LocationMode
+ {
+ public static final String SYSTEM = "system";
+ public static final String GAODE = "gaode";
+ public static final String REALTIME = "realtime";
+ public static final String H5 = "h5";
+
+ private LocationMode() {}
+ }
+
+ public final class LocationProvider
+ {
+ public static final String AUTO = "auto";
+
+ private LocationProvider() {}
+ }
+
+ private NSEnums() {}
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/location/NSAMapLocation.java b/app/src/main/java/com/nsgk/ruralWeb/location/NSAMapLocation.java
new file mode 100644
index 0000000..7af0885
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/location/NSAMapLocation.java
@@ -0,0 +1,462 @@
+package com.nsgk.ruralWeb.location;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+
+import com.amap.api.location.AMapLocation;
+import com.amap.api.location.AMapLocationClient;
+import com.amap.api.location.AMapLocationClientOption;
+import com.amap.api.location.AMapLocationListener;
+import com.amap.api.location.CoordinateConverter;
+import com.amap.api.location.DPoint;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+// 主线程调用, 无线程安全
+public class NSAMapLocation implements AMapLocationListener
+{
+ // 定位场景: 默认无场景
+ public static final int FLAG_PURPOSE_SIGNIN = 1; // 签到
+ public static final int FLAG_PURPOSE_TRANSPORT = 1 << 1; // 出行
+ public static final int FLAG_PURPOSE_SPORT = 1 << 2; // 运动
+
+ // 定位模式
+ public static final int FLAG_MODE_HIGH = 1 << 3; // 高精度模式
+ public static final int FLAG_MODE_SAVING = 1 << 4; // 低功耗模式
+ public static final int FLAG_MODE_SENSORS = 1 << 5; // 仅设备模式
+
+ // 获取一次定位结果: 默认为false
+ public static final int FLAG_ONCE = 1 << 6; // true
+
+ // 获取最近3s内精度最高的一次定位结果: 默认为false
+ public static final int FLAG_ONCE_LATEST = 1 << 7; // true
+
+ // 设置是否返回地址信息: 默认返回地址信息
+ public static final int FLAG_NO_NEED_ADDRESS = 1 << 8; // false
+
+ // 设置是否允许模拟位置: 默认为true
+ public static final int FLAG_MOCK_DISABLE = 1 << 9; // false
+
+ // 关闭缓存机制: 默认缓存
+ public static final int FLAG_CACHE_DISABLE = 1 << 10; // false
+
+ // 线程
+ public static final int FLAG_BUILTIN_THREAD = 1 << 11; // 使用内建线程
+ public static final int FLAG_CUSTOM_THREAD = 1 << 12; // 使用外部线程
+ public static final int FLAG_MAIN_THREAD = 1 << 13; // 使用主线程
+
+ public static final int DEFAULT_FLAG = FLAG_MODE_HIGH | FLAG_NO_NEED_ADDRESS;
+
+
+ public static final int OPTION_INTERVAL = 1; // 定位间隔(毫秒) 默认1000
+ public static final int OPTION_HTTP_TIMEOUT = 2; // 定位间隔(毫秒) 默认20000
+
+ private static final int DEFAULT_INTERVAL = 1000;
+ private static final int DEFAULT_HTTP_TIMEOUT = 20000;
+
+ private static final String ID_TAG = NSSystemLocation.class.getName();
+
+ private final Object m_lock = new Object();
+
+ //声明AMapLocationClient类对象
+ public AMapLocationClient mLocationClient = null;
+ //声明定位回调监听器
+ public AMapLocationListener mLocationListener = this;
+ //声明AMapLocationClientOption对象
+ public AMapLocationClientOption mLocationOption = null;
+ private final Context m_context;
+ private int m_flag = DEFAULT_FLAG;
+ private boolean m_isPreInit = false;
+ private NSLocationInfo m_lastLocation = null;
+ private HandlerThread m_thread = null;
+ private Looper m_customLooper = null;
+ private AtomicInteger m_readOnce = new AtomicInteger(-1);
+ private Map m_options = new HashMap<>();
+
+ public NSAMapLocation(Context context)
+ {
+ m_context = context;
+ }
+
+ public boolean Init(int flag)
+ {
+ if(IsInitialized())
+ {
+ Log.e(ID_TAG, "定位已初始化");
+ return true;
+ }
+
+ try
+ {
+ m_readOnce.set(-1);
+ m_flag = flag < 0 ? DEFAULT_FLAG : flag;
+
+ // 在构造AMapLocationClient 之前必须进行合规检查,设置接口之前保证隐私政策合规
+ PreInit();
+
+ Context applicationContext = m_context.getApplicationContext();
+ //初始化定位
+ if(HasFlag(FLAG_BUILTIN_THREAD))
+ {
+ m_thread = new HandlerThread("AMap内建定位");
+ m_thread.start();
+ mLocationClient = new AMapLocationClient(m_thread.getLooper(), applicationContext);
+ Log.d(ID_TAG, "定位使用内建线程: " + m_thread.getId());
+ }
+ else if(HasFlag(FLAG_CUSTOM_THREAD))
+ {
+ if(null == m_customLooper)
+ {
+ throw new RuntimeException("请先传入线程Looper");
+ }
+ mLocationClient = new AMapLocationClient(m_customLooper, applicationContext);
+ Log.d(ID_TAG, "定位使用用户指定线程: " + m_customLooper.getThread().getId());
+ }
+ else if(HasFlag(FLAG_MAIN_THREAD))
+ {
+ Looper mainLooper = Looper.getMainLooper();
+ mLocationClient = new AMapLocationClient(mainLooper, applicationContext);
+ Log.d(ID_TAG, "定位使用主线程: " + mainLooper.getThread().getId());
+ }
+ else
+ {
+ mLocationClient = new AMapLocationClient(applicationContext);
+ Log.d(ID_TAG, "定位使用当前线程: " + Thread.currentThread().getId());
+ }
+
+ //设置定位回调监听
+ mLocationClient.setLocationListener(mLocationListener);
+
+ InitOptions();
+
+ mLocationClient.setLocationOption(mLocationOption);
+
+ //V6.4.9版本起 client设置逆地理回调
+/* mLocationClient.setReGeoLocationCallback(new IReGeoLocationCallback()
+ {
+ @Override
+ public void onReGeoLocation(AMapLocation reGeoLocation)
+ {
+ Log.i(ID_TAG, reGeoLocation.toStr());
+ }
+ });*/
+
+ Log.i(ID_TAG, "定位初始化完成");
+ return true;
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ mLocationClient = null;
+ return false;
+ }
+ }
+
+ public void SetThread(Looper looper)
+ {
+ CheckInitialization(false);
+ m_customLooper = looper;
+ }
+
+ public void SetThread(HandlerThread thread)
+ {
+ CheckInitialization(false);
+ m_customLooper = thread.getLooper();
+ }
+
+ public boolean IsRunning()
+ {
+ return IsInitialized() && mLocationClient.isStarted();
+ }
+
+ public void Start()
+ {
+ Run(-1);
+ }
+
+ public void Stop()
+ {
+ CheckInitialization(true);
+
+ //设置场景模式后最好调用一次stop,再调用start以保证场景模式生效
+ mLocationClient.stopLocation();
+
+ Log.i(ID_TAG, "高德定位停止");
+ }
+
+ public void Shutdown()
+ {
+ if(!IsInitialized())
+ {
+ Log.e(ID_TAG, "请先初始化");
+ return;
+ }
+
+ TerminateRead();
+ Stop();
+
+ mLocationClient.onDestroy();
+ mLocationClient = null;
+ if(null != m_thread)
+ {
+ m_thread.quit();
+ m_thread = null;
+ Log.d(ID_TAG, "高德定位内建线程结束");
+ }
+
+ Log.i(ID_TAG, "高德定位销毁");
+ }
+
+ public void PreInit()
+ {
+ if(m_isPreInit)
+ return;
+ Context applicationContext = m_context.getApplicationContext();
+ /** 设置包含隐私政策,并展示用户授权弹窗 必须在AmapLocationClient实例化之前调用
+ *
+ * @param context
+ * @param isContains: 是隐私权政策是否包含高德开平隐私权政策 true是包含
+ * @param isShow: 隐私权政策是否弹窗展示告知用户 true是展示
+ * @since 5.6.0
+ */
+ AMapLocationClient.updatePrivacyShow(applicationContext, true, true);
+ /**
+ * 设置是否同意用户授权政策 必须在AmapLocationClient实例化之前调用
+ * @param context
+ * @param isAgree:隐私权政策是否取得用户同意 true是用户同意
+ *
+ * @since 5.6.0
+ */
+ AMapLocationClient.updatePrivacyAgree(applicationContext, true);
+ Log.i(ID_TAG, "定位预初始化");
+ m_isPreInit = true;
+ }
+
+ public boolean IsInitialized()
+ {
+ return null != mLocationClient;
+ }
+
+ private void CheckInitialization(boolean inited)
+ {
+ if(IsInitialized() != inited)
+ {
+ String msg = inited ? "定位未初始化" : "定位已初始化";
+ Log.e(ID_TAG, msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ private boolean HasFlag(int f)
+ {
+ return (m_flag & f) != 0;
+ }
+
+ public void SetOption(int type, Object value)
+ {
+ CheckInitialization(false);
+ m_options.put(type, value);
+ }
+
+ @SuppressLint("unchecked")
+ private T GetOption(int type, T def)
+ {
+ Object o = m_options.get(type);
+ if(null == o)
+ return def;
+ return (T) o;
+ }
+
+ private void InitOptions()
+ {
+ //初始化AMapLocationClientOption对象
+ mLocationOption = new AMapLocationClientOption();
+
+ /**
+ * 设置定位场景,目前支持三种场景(签到、出行、运动,默认无场景)
+ */
+ if(HasFlag(FLAG_PURPOSE_SIGNIN))
+ mLocationOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn);
+ else if(HasFlag(FLAG_PURPOSE_TRANSPORT))
+ mLocationOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.Transport);
+ else if(HasFlag(FLAG_PURPOSE_SPORT))
+ mLocationOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.Sport);
+
+ //设置定位模式为AMapLocationMode.Hight_Accuracy,高精度模式。
+ if(HasFlag(FLAG_MODE_HIGH))
+ mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
+ //设置定位模式为AMapLocationMode.Battery_Saving,低功耗模式。
+ else if(HasFlag(FLAG_MODE_SAVING))
+ mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Battery_Saving);
+ //设置定位模式为AMapLocationMode.Device_Sensors,仅设备模式。
+ else if(HasFlag(FLAG_MODE_SENSORS))
+ mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Device_Sensors);
+
+ //获取一次定位结果:
+ //该方法默认为false。
+ if(HasFlag(FLAG_ONCE))
+ mLocationOption.setOnceLocation(true);
+
+ //获取最近3s内精度最高的一次定位结果:
+ //设置setOnceLocationLatest(boolean b)接口为true,启动定位时SDK会返回最近3s内精度最高的一次定位结果。如果设置其为true,setOnceLocation(boolean b)接口也会被设置为true,反之不会,默认为false。
+ if(HasFlag(FLAG_ONCE_LATEST))
+ mLocationOption.setOnceLocationLatest(true);
+
+ //设置定位间隔,单位毫秒,默认为2000ms,最低1000ms。
+ int interval = GetOption(OPTION_INTERVAL, DEFAULT_INTERVAL);
+ mLocationOption.setInterval(interval);
+
+ //设置是否返回地址信息(默认返回地址信息)
+ if(HasFlag(FLAG_NO_NEED_ADDRESS))
+ mLocationOption.setNeedAddress(false);
+
+ //设置是否允许模拟位置,默认为true,允许模拟位置
+ if(HasFlag(FLAG_MOCK_DISABLE))
+ mLocationOption.setMockEnable(false);
+
+ //单位是毫秒,默认30000毫秒,建议超时时间不要低于8000毫秒。
+ int timeout = GetOption(OPTION_HTTP_TIMEOUT, DEFAULT_HTTP_TIMEOUT);
+ mLocationOption.setHttpTimeOut(timeout);
+
+ //关闭缓存机制
+ if(HasFlag(FLAG_CACHE_DISABLE))
+ mLocationOption.setLocationCacheEnable(false);
+ }
+
+ private void Run(int num)
+ {
+ CheckInitialization(true);
+
+ m_readOnce.set(num);
+
+ //设置场景模式后最好调用一次stop,再调用start以保证场景模式生效
+ mLocationClient.stopLocation();
+ mLocationClient.startLocation();
+
+ Log.i(ID_TAG, "定位开始");
+ }
+
+ /*
+ 在调用线程中执行
+ */
+ @Override
+ public void onLocationChanged(AMapLocation amapLocation)
+ {
+ Log.d(ID_TAG, "AMap定位回调线程: " + Thread.currentThread().getId() + " -> " + amapLocation);
+ SetLastLocation(amapLocation);
+ if(null != amapLocation)
+ {
+ if(m_readOnce.get() > 0)
+ {
+ m_readOnce.decrementAndGet();
+ if(m_readOnce.get() == 0)
+ {
+ synchronized(m_lock)
+ {
+ m_lock.notifyAll();
+ }
+ }
+ }
+ }
+ }
+
+ private double[] Convert(double longitude, double latitude)
+ {
+ try
+ {
+ //初始化坐标转换类
+ CoordinateConverter converter = new CoordinateConverter(m_context.getApplicationContext());
+ converter.from(CoordinateConverter.CoordType.GPS);
+ //设置需要转换的坐标
+ converter.coord(new DPoint(latitude, longitude));
+ //转换成高德坐标
+ DPoint destPoint = converter.convert();
+ double dx = destPoint.getLongitude() - longitude;
+ double dy = destPoint.getLatitude() - latitude;
+ longitude = longitude - dx;
+ latitude = latitude - dy;
+ return new double[]{
+ longitude, latitude
+ };
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void SetLastLocation(AMapLocation amapLocation)
+ {
+ if(null != amapLocation)
+ {
+ double longitude = amapLocation.getLongitude();
+ double latitude = amapLocation.getLatitude();
+ double[] coord = Convert(longitude, latitude);
+ longitude = coord[0];
+ latitude = coord[1];
+ m_lastLocation = new NSLocationInfo(amapLocation.getProvider(), longitude, latitude);
+ }
+ else
+ {
+ m_lastLocation = null;
+ }
+ }
+
+ /**
+ * 同步调用, 会锁住线程
+ */
+ public NSLocationInfo Read(int count, int timeout)
+ {
+ CheckInitialization(true);
+/* if(IsRunning())
+ {
+ throw new RuntimeException("请先停止定位");
+ }*/
+
+ if(timeout < 0)
+ timeout = 0;
+ Log.d(ID_TAG, "AMap定位线程: " + Thread.currentThread().getId() + ", 超时设置: " + timeout);
+ synchronized(m_lock)
+ {
+ try
+ {
+ CleanLastLocation();
+ Run(count);
+ m_lock.wait(timeout);
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ finally
+ {
+ Stop();
+ }
+ }
+ return m_lastLocation;
+ }
+
+ public NSLocationInfo GetLastLocation()
+ {
+ return m_lastLocation;
+ }
+
+ public void TerminateRead()
+ {
+ synchronized(m_lock)
+ {
+ Log.i(ID_TAG, "AMap中止定位");
+ m_lock.notifyAll();
+ }
+ }
+
+ public void CleanLastLocation()
+ {
+ m_lastLocation = null;
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/location/NSLastKnownLocation.java b/app/src/main/java/com/nsgk/ruralWeb/location/NSLastKnownLocation.java
new file mode 100644
index 0000000..a1e35c7
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/location/NSLastKnownLocation.java
@@ -0,0 +1,84 @@
+package com.nsgk.ruralWeb.location;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.nsgk.ruralWeb.sys.NSConstants;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+public class NSLastKnownLocation
+{
+ private static final String ID_TAG = NSLastKnownLocation.class.getName();
+
+ private final Context m_context;
+ private NSLocationInfo m_lastLocation = null;
+
+ public NSLastKnownLocation(Context context)
+ {
+ m_context = context;
+ }
+
+ @SuppressLint("MissingPermission")
+ public NSLocationInfo Read(String type)
+ {
+ NSLocationInfo loc = null;
+ try
+ {
+ LocationManager lm = (LocationManager) m_context.getSystemService(Context.LOCATION_SERVICE);
+
+ List allProviders = lm.getAllProviders();
+ Log.i(ID_TAG, "使用最近所有定位提供器: " + allProviders);
+ Map map = new HashMap<>();
+ for(String provider : allProviders)
+ {
+ Location lastKnownLocation = lm.getLastKnownLocation(provider);
+ if(null == lastKnownLocation)
+ continue;
+ NSLocationInfo l = new NSLocationInfo(provider, lastKnownLocation.getLongitude(), lastKnownLocation.getLatitude());
+ map.put(provider, l);
+ }
+
+ Log.i(ID_TAG, "最近定位提供器结果: " + map);
+ if(null != type && !type.isEmpty() && map.containsKey(type))
+ loc = map.get(type);
+ else if(map.containsKey(LocationManager.GPS_PROVIDER))
+ loc = map.get(LocationManager.GPS_PROVIDER);
+ else if(map.containsKey(LocationManager.NETWORK_PROVIDER))
+ loc = map.get(LocationManager.NETWORK_PROVIDER);
+ else if(map.containsKey(NSConstants.FUSED_PROVIDER))
+ loc = map.get(NSConstants.FUSED_PROVIDER);
+ else if(map.containsKey(LocationManager.PASSIVE_PROVIDER))
+ loc = map.get(LocationManager.PASSIVE_PROVIDER);
+ if(null == loc)
+ Log.i(ID_TAG, "所有提供器无最近定位");
+ else
+ {
+ Log.i(ID_TAG, "使用提供器最近定位: " + loc.provider);
+ m_lastLocation = loc;
+ }
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ return loc;
+ }
+
+ public NSLocationInfo GetLastLocation()
+ {
+ return m_lastLocation;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nsgk/ruralWeb/location/NSLocationInfo.java b/app/src/main/java/com/nsgk/ruralWeb/location/NSLocationInfo.java
new file mode 100644
index 0000000..6d7266a
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/location/NSLocationInfo.java
@@ -0,0 +1,25 @@
+package com.nsgk.ruralWeb.location;
+
+public final class NSLocationInfo
+{
+ public final String provider;
+ public final double longitude;
+ public final double latitude;
+
+ public NSLocationInfo(String provider, double longitude, double latitude)
+ {
+ this.provider = provider;
+ this.longitude = longitude;
+ this.latitude = latitude;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "定位信息: " +
+ "提供器=" + provider +
+ "; 经度=" + longitude +
+ "; 纬度=" + latitude
+ ;
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/location/NSRealtimeLocation.java b/app/src/main/java/com/nsgk/ruralWeb/location/NSRealtimeLocation.java
new file mode 100644
index 0000000..9ce1c01
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/location/NSRealtimeLocation.java
@@ -0,0 +1,380 @@
+package com.nsgk.ruralWeb.location;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.amap.api.location.AMapLocation;
+import com.amap.api.location.AMapLocationClient;
+import com.amap.api.location.AMapLocationClientOption;
+import com.amap.api.location.AMapLocationListener;
+import com.amap.api.location.CoordinateConverter;
+import com.amap.api.location.DPoint;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+// 主线程调用, 无线程安全
+public class NSRealtimeLocation implements LocationListener
+{
+ // 定位提供器
+ public static final int FLAG_PROVIDER_NETWORK = 1; // 网络定位
+ public static final int FLAG_PROVIDER_GPS = 1 << 1; // GPS定位
+
+ // 定位提供器优先级
+ public static final int FLAG_PRIORITY_NETWORK = 1 << 2; // 网络定位优先
+ public static final int FLAG_PRIORITY_GPS = 1 << 3; // GPS定位优先
+
+ // 线程
+ public static final int FLAG_BUILTIN_THREAD = 1 << 4; // 使用内建线程
+ public static final int FLAG_CUSTOM_THREAD = 1 << 5; // 使用外部线程
+ public static final int FLAG_MAIN_THREAD = 1 << 6; // 使用主线程
+
+ public static final int DEFAULT_FLAG = FLAG_PROVIDER_NETWORK | FLAG_PROVIDER_GPS | FLAG_PRIORITY_NETWORK;
+
+
+ public static final int OPTION_MIN_TIME = 1; // 最小定位间隔(毫秒) 默认60000
+ public static final int OPTION_MIN_DISTANCE = 2; // 最小定位距离变化(米) 默认10
+
+ private static final int DEFAULT_MIN_TIME = 1000;
+ private static final int DEFAULT_MIN_DISTANCE = 1;
+
+ private static final String ID_TAG = NSRealtimeLocation.class.getName();
+
+ private final Object m_lock = new Object();
+
+ private LocationManager m_locationManager;
+ private final Context m_context;
+ private int m_flag = DEFAULT_FLAG;
+ private NSLocationInfo m_lastLocation = null;
+ private HandlerThread m_thread = null;
+ private Looper m_customLooper = null;
+ private AtomicInteger m_readOnce = new AtomicInteger(-1);
+ private Map m_options = new HashMap<>();
+ private String m_provider = null;
+ private Looper m_looper = null;
+
+ public NSRealtimeLocation(Context context)
+ {
+ m_context = context;
+ }
+
+ public boolean Init(int flag)
+ {
+ if(IsInitialized())
+ {
+ Log.e(ID_TAG, "定位已初始化");
+ return true;
+ }
+
+ if(!HasFlag(FLAG_PROVIDER_NETWORK | FLAG_PROVIDER_GPS))
+ {
+ Log.e(ID_TAG, "请设置定位提供器");
+ return false;
+ }
+
+ try
+ {
+ m_readOnce.set(-1);
+ m_flag = flag < 0 ? DEFAULT_FLAG : flag;
+
+ m_locationManager = (LocationManager) m_context.getSystemService(Context.LOCATION_SERVICE);
+
+ //初始化定位
+ if(HasFlag(FLAG_BUILTIN_THREAD))
+ {
+ m_thread = new HandlerThread("系统实时内建定位");
+ m_thread.start();
+ m_looper = m_thread.getLooper();
+ Log.d(ID_TAG, "定位使用内建线程: " + m_thread.getId());
+ }
+ else if(HasFlag(FLAG_CUSTOM_THREAD))
+ {
+ if(null == m_customLooper)
+ {
+ throw new RuntimeException("请先传入线程Looper");
+ }
+ m_looper = m_customLooper;
+ Log.d(ID_TAG, "定位使用用户指定线程: " + m_customLooper.getThread().getId());
+ }
+ else if(HasFlag(FLAG_MAIN_THREAD))
+ {
+ Looper mainLooper = Looper.getMainLooper();
+ m_looper = mainLooper;
+ Log.d(ID_TAG, "定位使用主线程: " + mainLooper.getThread().getId());
+ }
+ else
+ {
+ m_looper = Looper.myLooper();
+ Log.d(ID_TAG, "定位使用当前线程: " + Thread.currentThread().getId());
+ }
+
+ Log.i(ID_TAG, "定位初始化完成");
+ return true;
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ m_locationManager = null;
+ return false;
+ }
+ }
+
+ public void SetThread(Looper looper)
+ {
+ CheckInitialization(false);
+ m_customLooper = looper;
+ }
+
+ public void SetThread(HandlerThread thread)
+ {
+ CheckInitialization(false);
+ m_customLooper = thread.getLooper();
+ }
+
+ public boolean IsRunning()
+ {
+ return IsInitialized() && null != m_provider;
+ }
+
+ public void Start()
+ {
+ Run(-1);
+ }
+
+ public void Stop()
+ {
+ CheckInitialization(true);
+
+ m_locationManager.removeUpdates(this);
+ m_provider = null;
+
+ Log.i(ID_TAG, "实时定位停止");
+ }
+
+ public void Shutdown()
+ {
+ if(!IsInitialized())
+ {
+ Log.e(ID_TAG, "请先初始化");
+ return;
+ }
+
+ TerminateRead();
+ Stop();
+
+ m_locationManager = null;
+ if(null != m_thread)
+ {
+ m_thread.quit();
+ m_thread = null;
+ Log.d(ID_TAG, "实时定位内建线程结束");
+ }
+
+ Log.i(ID_TAG, "实时定位销毁");
+ }
+
+ public boolean IsInitialized()
+ {
+ return null != m_locationManager;
+ }
+
+ private void CheckInitialization(boolean inited)
+ {
+ if(IsInitialized() != inited)
+ {
+ String msg = inited ? "定位未初始化" : "定位已初始化";
+ Log.e(ID_TAG, msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ private boolean HasFlag(int f)
+ {
+ return (m_flag & f) != 0;
+ }
+
+ public void SetOption(int type, Object value)
+ {
+ //CheckInitialization(false);
+ m_options.put(type, value);
+ }
+
+ @SuppressLint("unchecked")
+ private T GetOption(int type, T def)
+ {
+ Object o = m_options.get(type);
+ if(null == o)
+ return def;
+ return (T) o;
+ }
+
+ private void InitOptions()
+ {
+ List providers = new ArrayList<>();
+ m_provider = null;
+ if(HasFlag(FLAG_PROVIDER_NETWORK))
+ {
+ if(m_locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER))
+ providers.add(LocationManager.NETWORK_PROVIDER);
+ else
+ Log.w(ID_TAG, "网络定位提供器未启用");
+ }
+ if(HasFlag(FLAG_PROVIDER_GPS))
+ {
+ if(m_locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER))
+ providers.add(LocationManager.GPS_PROVIDER);
+ else
+ Log.w(ID_TAG, "GPS定位提供器未启用");
+ }
+ if(providers.isEmpty())
+ throw new RuntimeException("无定位提供器");
+
+ if(HasFlag(FLAG_PRIORITY_NETWORK))
+ {
+ if(providers.contains(LocationManager.NETWORK_PROVIDER))
+ m_provider = LocationManager.NETWORK_PROVIDER;
+ else
+ m_provider = LocationManager.GPS_PROVIDER;
+ }
+ else if(HasFlag(FLAG_PRIORITY_GPS))
+ {
+ if(providers.contains(LocationManager.GPS_PROVIDER))
+ m_provider = LocationManager.GPS_PROVIDER;
+ else
+ m_provider = LocationManager.NETWORK_PROVIDER;
+ }
+/* else
+ {
+ if(providers.contains(LocationManager.NETWORK_PROVIDER))
+ m_provider = LocationManager.NETWORK_PROVIDER;
+ else
+ m_provider = LocationManager.GPS_PROVIDER;
+ }*/
+
+ if(null == m_provider)
+ throw new RuntimeException("未确定定位提供器");
+
+ Log.i(ID_TAG, "使用定位器: " + m_provider);
+ }
+
+ @SuppressLint("MissingPermission")
+ private void Run(int num)
+ {
+ CheckInitialization(true);
+
+ Stop();
+
+ m_readOnce.set(num);
+
+ InitOptions();
+
+ int minTime = GetOption(OPTION_MIN_TIME, DEFAULT_MIN_TIME);
+ int minDistance = GetOption(OPTION_MIN_DISTANCE, DEFAULT_MIN_DISTANCE);
+ m_locationManager.requestLocationUpdates(m_provider, minTime, minDistance, this, m_looper);
+
+ Location lastKnownLocation = m_locationManager.getLastKnownLocation(m_provider);
+ SetLastLocation(lastKnownLocation);
+
+ Log.i(ID_TAG, "定位开始");
+ }
+
+ private void SetLastLocation(Location location)
+ {
+ if(null != location)
+ {
+ m_lastLocation = new NSLocationInfo(location.getProvider(), location.getLongitude(), location.getLatitude());
+ }
+ else
+ {
+ m_lastLocation = null;
+ }
+ }
+
+ /**
+ * 同步调用, 会锁住线程
+ */
+ public NSLocationInfo Read(int count, int timeout)
+ {
+ CheckInitialization(true);
+/* if(IsRunning())
+ {
+ throw new RuntimeException("请先停止定位");
+ }*/
+
+ if(timeout < 0)
+ timeout = 0;
+ Log.d(ID_TAG, "实时定位线程: " + Thread.currentThread().getId() + ", 超时设置: " + timeout);
+ synchronized(m_lock)
+ {
+ try
+ {
+ CleanLastLocation();
+ Run(count);
+ m_lock.wait(timeout);
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ finally
+ {
+ Stop();
+ }
+ }
+ return m_lastLocation;
+ }
+
+ public NSLocationInfo GetLastLocation()
+ {
+ return m_lastLocation;
+ }
+
+ public void CleanLastLocation()
+ {
+ m_lastLocation = null;
+ }
+
+ /*
+ 在调用线程中执行
+ */
+ @Override
+ public void onLocationChanged(@NonNull Location location)
+ {
+ Log.d(ID_TAG, "系统实时定位线程: " + Thread.currentThread().getId() + " -> " + location);
+ SetLastLocation(location);
+ //if(null != location)
+ {
+ if(m_readOnce.get() > 0)
+ {
+ m_readOnce.decrementAndGet();
+ if(m_readOnce.get() == 0)
+ {
+ synchronized(m_lock)
+ {
+ m_lock.notifyAll();
+ }
+ }
+ }
+ }
+ }
+
+ public void TerminateRead()
+ {
+ synchronized(m_lock)
+ {
+ Log.i(ID_TAG, "系统实时中止定位");
+ m_lock.notifyAll();
+ }
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/location/NSSystemLocation.java b/app/src/main/java/com/nsgk/ruralWeb/location/NSSystemLocation.java
new file mode 100644
index 0000000..8df913f
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/location/NSSystemLocation.java
@@ -0,0 +1,161 @@
+package com.nsgk.ruralWeb.location;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+public class NSSystemLocation
+{
+ private static final String ID_TAG = NSSystemLocation.class.getName();
+
+ private final Context m_context;
+ private NSLocationInfo m_lastLocation = null;
+ private volatile CancellationSignal m_cancellationSignal;
+ private volatile CompletableFuture m_future;
+
+ public NSSystemLocation(Context context)
+ {
+ m_context = context;
+ }
+
+ @SuppressLint("MissingPermission")
+ public NSLocationInfo Read(String provider, int timeout)
+ {
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
+ {
+ Log.w(ID_TAG, "获取当前定位请求Android 11+");
+ return null;
+ }
+
+ NSLocationInfo loc = null;
+ try
+ {
+ LocationManager lm = (LocationManager) m_context.getSystemService(Context.LOCATION_SERVICE);
+ if(null != provider)
+ {
+ if(LocationManager.GPS_PROVIDER.equals(provider))
+ {
+ if(!lm.isProviderEnabled(provider))
+ {
+ Log.w(ID_TAG, "GPS定位提供器未启用, 尝试网络定位器");
+ provider = LocationManager.NETWORK_PROVIDER;
+ if(!lm.isProviderEnabled(provider))
+ {
+ Log.w(ID_TAG, "网络定位提供器也未启用");
+ provider = null;
+ }
+ }
+ }
+ else if(LocationManager.NETWORK_PROVIDER.equals(provider))
+ {
+ Log.w(ID_TAG, "网络定位提供器未启用, 尝试GPS定位器");
+ if(!lm.isProviderEnabled(provider))
+ {
+ provider = LocationManager.GPS_PROVIDER;
+ if(!lm.isProviderEnabled(provider))
+ {
+ Log.w(ID_TAG, "GPS定位提供器也未启用");
+ provider = null;
+ }
+ }
+ }
+ }
+
+ if(null == provider)
+ {
+ Criteria criteria = new Criteria();
+ criteria.setAccuracy(Criteria.ACCURACY_FINE); // 高精度
+// criteria.setPowerRequirement(Criteria.POWER_HIGH);
+// criteria.setAltitudeRequired(false);
+ provider = lm.getBestProvider(criteria, true);
+ }
+ Log.i(ID_TAG, "使用高精度定位提供器: " + provider);
+
+ if(null == provider)
+ {
+ Log.i(ID_TAG, "无法获取高精度定位提供器");
+ return null;
+ }
+
+ m_cancellationSignal = new CancellationSignal();
+ m_future = new CompletableFuture<>();
+ lm.getCurrentLocation(provider, m_cancellationSignal, m_context.getMainExecutor(), m_future::complete);
+ Location location;
+ if(timeout > 0)
+ location = m_future.get(timeout, TimeUnit.MILLISECONDS);
+ else
+ location = m_future.get();
+ m_cancellationSignal = null;
+ m_future = null;
+ if(null == location)
+ {
+ Log.i(ID_TAG, "无法使用高精度提供器定位");
+ return null;
+ }
+ loc = new NSLocationInfo(provider, location.getLongitude(), location.getLatitude());
+ Log.i(ID_TAG, "使用高精度提供器获取定位: " + loc);
+ m_lastLocation = loc;
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ m_cancellationSignal = null;
+ m_future = null;
+ }
+ return loc;
+ }
+
+ private void ShowToast(final String message, int duration)
+ {
+ ((Activity)m_context).runOnUiThread((new Runnable() {
+ @Override
+ public void run()
+ {
+ Toast.makeText(m_context, message, duration).show();
+ }
+ }));
+ }
+
+ public void TerminateRead()
+ {
+ CancellationSignal signal = m_cancellationSignal;
+ if(null != signal && !signal.isCanceled())
+ {
+ Log.i(ID_TAG, "系统中止定位: 1. 取消定位");
+ signal.cancel();
+ }
+ CompletableFuture future = m_future;
+ if(null != future && !future.isCancelled())
+ {
+ Log.i(ID_TAG, "系统中止定位: 2. 取消等待结果");
+ future.cancel(true);
+ }
+ }
+
+ public void Shutdown()
+ {
+ Log.i(ID_TAG, "销毁系统定位");
+ TerminateRead();
+ }
+
+ public NSLocationInfo GetLastLocation()
+ {
+ return m_lastLocation;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nsgk/ruralWeb/sys/NSConstants.java b/app/src/main/java/com/nsgk/ruralWeb/sys/NSConstants.java
new file mode 100644
index 0000000..6e01d87
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/sys/NSConstants.java
@@ -0,0 +1,92 @@
+package com.nsgk.ruralWeb.sys;
+
+import android.location.LocationManager;
+
+import com.nsgk.ruralWeb.BuildConfig;
+import com.nsgk.ruralWeb.enums.NSEnums;
+
+public final class NSConstants
+{
+ /**
+ * App首页链接地址
+ * 可以在 /gradle.properties 里配置 appHomeUrl=链接地址, 不要携带双引号
+ * 也可以在命令行添加 -PappHomeUrl="链接地址", 双引号可携带也可不携带
+ */
+ public static String AppHomeUrl()
+ {
+ return BuildConfig.APP_HOME_URL;
+ }
+
+ /**
+ * App启动页版权文本
+ * 可以在 /gradle.properties 里配置 appCopyright=文本, 不要携带双引号
+ * 也可以在命令行添加 -PappCopyright="文本", 双引号可携带也可不携带
+ */
+ public static String AppCopyright()
+ {
+ return BuildConfig.APP_COMPYRIGHT;
+ }
+
+ /**
+ * App启动页厂商文本
+ * 可以在 /gradle.properties 里配置 appVendor=文本, 不要携带双引号
+ * 也可以在命令行添加 -PappVendor="文本", 双引号可携带也可不携带
+ */
+ public static String AppVendor()
+ {
+ return BuildConfig.APP_VENDOR;
+ }
+
+ /**
+ * App图标
+ */
+ public static String AppIcon()
+ {
+ return BuildConfig.APP_ICON;
+ }
+
+ /**
+ * App更新下载地址
+ * 可以在 /gradle.properties 里配置 appUpdateUrl=链接地址, 不要携带双引号
+ * 也可以在命令行添加 -PappUpdateUrl="链接地址", 双引号可携带也可不携带
+ */
+ public static String AppUpdateUrl()
+ {
+ return BuildConfig.APP_UPDATE_URL;
+ }
+
+ public static boolean IsHttps()
+ {
+ return AppHomeUrl().startsWith("https://");
+ }
+
+ public static boolean IsDebug()
+ {
+ return BuildConfig.DEBUG;
+ }
+
+ public static boolean IsAMapLocationEnabled()
+ {
+ return true;
+ }
+
+ // 偏好默认值
+ public static final String DEFAULT_LOCATION_MODE = NSEnums.LocationMode.REALTIME;
+ public static final int DEFAULT_LOCATION_GAODE_INTERVAL = 1000;
+ public static final int DEFAULT_LOCATION_GAODE_READ_COUNT = 1;
+ public static final String DEFAULT_LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
+ public static final int DEFAULT_LOCATION_REALTIME_INTERVAL = 1000;
+ public static final int DEFAULT_LOCATION_REALTIME_DISTANCE = 0; // 1
+ public static final int DEFAULT_LOCATION_REALTIME_READ_COUNT = 1;
+ public static final boolean DEFAULT_OPEN_LAST_URL = false;
+ public static final int DEFAULT_FONT_SCALE = 100;
+ public static final boolean DEFAULT_LOGCAT_CONSOLE_OUTPUT = false;
+ public static final int DEFAULT_LOCATION_TIMEOUT = 0;
+
+
+ public static final String FUSED_PROVIDER = "fused";
+ public static final String APK_UPDATE_DOWNLOAD_DIR = "update";
+ public static final String APK_UPDATE_DOWNLOAD_FILE = "latest.apk";
+
+ private NSConstants() {}
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/sys/NSPreference.java b/app/src/main/java/com/nsgk/ruralWeb/sys/NSPreference.java
new file mode 100644
index 0000000..2eb3b0f
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/sys/NSPreference.java
@@ -0,0 +1,81 @@
+package com.nsgk.ruralWeb.sys;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.nsgk.ruralWeb.utils.NSStr;
+
+public final class NSPreference
+{
+ public static final String LOCATION_MODE = "location_mode";
+ public static final String LOCATION_GAODE_INTERVAL = "location_gaode_interval";
+ public static final String LOCATION_GAODE_READ_COUNT = "location_gaode_read_count";
+ public static final String LOCATION_PROVIDER = "location_provider";
+ public static final String LOCATION_REALTIME_INTERVAL = "location_realtime_interval";
+ public static final String LOCATION_REALTIME_DISTANCE = "location_realtime_distance";
+ public static final String LOCATION_REALTIME_READ_COUNT = "location_realtime_read_count";
+ public static final String OPEN_LAST_URL = "open_last_url";
+ public static final String LAST_ACCESS_URL = "last_access_url";
+ public static final String COOKIES = "cookies";
+ public static final String FONT_SCALE = "font_scale";
+ public static final String LOGCAT_CONSOLE_OUTPUT = "logcat_console_output";
+ public static final String LOCATION_TIMEOUT = "location_timeout";
+ public static final String RESET_SETTINGS = "RESET_SETTINGS";
+ public static final String VERSION = "VERSION";
+ public static final String UPDATE_DOWNLOAD = "UPDATE_DOWNLOAD";
+
+ private final Context context;
+
+ public NSPreference(Context context)
+ {
+ this.context = context;
+ }
+
+ public SharedPreferences Read()
+ {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public SharedPreferences.Editor Write()
+ {
+ return PreferenceManager.getDefaultSharedPreferences(context).edit();
+ }
+
+ public String GetString(String name, String... def)
+ {
+ return Read().getString(name, null != def && def.length > 0 ? def[0] : null);
+ }
+
+ public boolean GetBool(String name, boolean... def)
+ {
+ return Read().getBoolean(name, null != def && def.length > 0 ? def[0] : false);
+ }
+
+ public void Remove(String name)
+ {
+ Write().remove(name).commit();
+ }
+
+ public void SetString(String name, String val)
+ {
+ Write().putString(name, val).commit();
+ }
+
+ public void SetInt(String name, int val)
+ {
+ Write().putInt(name, val).commit();
+ }
+
+ public int GetInt(String name, int... def)
+ {
+ return Read().getInt(name, null != def && def.length > 0 ? def[0] : 0);
+ }
+
+
+ public int GetIntFromString(String name, int defVal)
+ {
+ String str = Read().getString(name, "");
+ return NSStr.parseInt_s(str, defVal);
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/ui/SettingsFragment.java b/app/src/main/java/com/nsgk/ruralWeb/ui/SettingsFragment.java
new file mode 100644
index 0000000..ad42f14
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/ui/SettingsFragment.java
@@ -0,0 +1,506 @@
+package com.nsgk.ruralWeb.ui;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.provider.Settings;
+import android.text.Html;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.app.NotificationCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceManager;
+import androidx.preference.SeekBarPreference;
+
+import com.nsgk.ruralWeb.BuildConfig;
+import com.nsgk.ruralWeb.R;
+import com.nsgk.ruralWeb.enums.NSEnums;
+import com.nsgk.ruralWeb.sys.NSConstants;
+import com.nsgk.ruralWeb.sys.NSPreference;
+import com.nsgk.ruralWeb.utils.NSContextUtils;
+import com.nsgk.ruralWeb.utils.NSMisc;
+import com.nsgk.ruralWeb.utils.NSStr;
+
+import java.io.File;
+import java.util.Objects;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.StreamProgress;
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+
+public class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener
+{
+ private static final String ID_TAG = SettingsFragment.class.getName();
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.settings_preference);
+ Preference preference;
+ final Context context = getContext();
+
+ preference = findPreference(NSPreference.LOCATION_MODE);
+ preference.setDefaultValue(NSConstants.DEFAULT_LOCATION_MODE);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_GAODE_INTERVAL);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_GAODE_INTERVAL);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_GAODE_READ_COUNT);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_GAODE_READ_COUNT);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.OPEN_LAST_URL);
+ preference.setDefaultValue(NSConstants.DEFAULT_OPEN_LAST_URL);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_PROVIDER);
+ preference.setDefaultValue(NSConstants.DEFAULT_LOCATION_PROVIDER);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_REALTIME_INTERVAL);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_REALTIME_INTERVAL);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_REALTIME_DISTANCE);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_REALTIME_DISTANCE);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_REALTIME_READ_COUNT);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_REALTIME_READ_COUNT);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.FONT_SCALE);
+ preference.setDefaultValue(NSConstants.DEFAULT_FONT_SCALE);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOGCAT_CONSOLE_OUTPUT);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOGCAT_CONSOLE_OUTPUT);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.LOCATION_TIMEOUT);
+ preference.setDefaultValue("" + NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ preference.setOnPreferenceChangeListener(this);
+
+ preference = findPreference(NSPreference.RESET_SETTINGS);
+ preference.setOnPreferenceClickListener(this);
+
+ preference = findPreference(NSPreference.VERSION);
+ preference.setOnPreferenceClickListener(this);
+
+ preference = findPreference(NSPreference.UPDATE_DOWNLOAD);
+ preference.setOnPreferenceClickListener(this);
+ if(StrUtil.isBlank(NSConstants.AppUpdateUrl()))
+ preference.setVisible(false);
+
+ // 总是隐藏高德的设置
+ if(!NSConstants.IsAMapLocationEnabled() || !NSContextUtils.BuildIsDebug(context))
+ {
+ preference = findPreference("location_gaode");
+ preference.setVisible(false);
+ }
+ }
+
+ private boolean CheckValueIsNumber(Object newValue, Integer min)
+ {
+ if(null != newValue)
+ {
+ String str = newValue.toString();
+ if(StrUtil.isNotBlank(str))
+ {
+ if(!NumberUtil.isInteger(str))
+ {
+ Toast.makeText(getContext(), "请输入数字", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ int i = Integer.parseInt(str);
+ if(null != min)
+ {
+ if(i < min)
+ {
+ Toast.makeText(getContext(), "必须大于等于" + min, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String key = preference.getKey();
+
+ switch(key)
+ {
+ case NSPreference.LOCATION_GAODE_INTERVAL:
+ if(!CheckValueIsNumber(newValue, 1000))
+ return false;
+ break;
+ case NSPreference.LOCATION_GAODE_READ_COUNT:
+ if(!CheckValueIsNumber(newValue, 1))
+ return false;
+ break;
+ case NSPreference.LOCATION_REALTIME_INTERVAL:
+ if(!CheckValueIsNumber(newValue, 1000))
+ return false;
+ break;
+ case NSPreference.LOCATION_REALTIME_DISTANCE:
+ if(!CheckValueIsNumber(newValue, 0))
+ return false;
+ break;
+ case NSPreference.LOCATION_REALTIME_READ_COUNT:
+ if(!CheckValueIsNumber(newValue, 1))
+ return false;
+ break;
+ case NSPreference.LOCATION_TIMEOUT:
+ if(!CheckValueIsNumber(newValue, 0))
+ return false;
+ break;
+ case NSPreference.FONT_SCALE: {
+ int i = (int)newValue;
+ int newi = Math.round((float)i / 10.0f) * 10;
+ if(i < 50)
+ i = 50;
+ if(i != newi)
+ {
+ getView().post(() -> {
+ ((SeekBarPreference)preference).setValue(newi);
+ preference.callChangeListener(newi);
+ });
+ return false;
+ }
+ break;
+ }
+ case NSPreference.LOCATION_MODE:
+ if(!NSConstants.IsAMapLocationEnabled() && Objects.equals(NSEnums.LocationMode.GAODE, newValue))
+ {
+ Log.w(ID_TAG, "高德定位被禁用");
+ return false;
+ }
+ if(Objects.equals(NSEnums.LocationMode.H5, newValue))
+ {
+ if(!NSConstants.IsHttps())
+ {
+ Log.w(ID_TAG, "非https不支持H5定位");
+ Toast.makeText(getContext(), "当前不支持H5定位", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+ break;
+ }
+
+ SetSummary(key, newValue);
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ String key = preference.getKey();
+ final Context context = getContext();
+
+ switch(key)
+ {
+ case NSPreference.RESET_SETTINGS:
+ OpenQueryDialog("警告", "确定要重置到默认设置?", (dialog, which) -> {
+ ResetSettings();
+ SetSummary(null, null);
+ Toast.makeText(context, "设置已重置", Toast.LENGTH_LONG).show();
+ });
+ break;
+ case NSPreference.VERSION:
+ OpenAbout();
+ break;
+ case NSPreference.UPDATE_DOWNLOAD:
+ DownloadUpdate();
+ //DownloadUpdateExternally();
+ break;
+ }
+ return false;
+ }
+
+ private void DownloadUpdateExternally()
+ {
+ String updateUrl = NSConstants.AppUpdateUrl();
+ if(StrUtil.isBlank(updateUrl))
+ return;
+
+ Context context = getContext();
+ Toast.makeText(context, "正在打开下载地址......", Toast.LENGTH_SHORT).show();
+ NSContextUtils.OpenUrlExternally(context, updateUrl);
+ }
+
+ private void DownloadUpdate()
+ {
+ String updateUrl = NSConstants.AppUpdateUrl();
+ if(StrUtil.isBlank(updateUrl))
+ return;
+
+ Handler handler = new Handler(Looper.myLooper());
+ Context context = getContext();
+ Log.d(ID_TAG, "下载apk地址: " + updateUrl);
+ Toast.makeText(context, "开始下载......", Toast.LENGTH_SHORT).show();
+
+ int icon = context.getResources().getIdentifier("ic_launcher_" + NSConstants.AppIcon()/* + "_round"*/, "mipmap", context.getApplicationContext().getPackageName());
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ String channelId = "nsgk";
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel notificationChannel = notificationManager.getNotificationChannel(channelId);
+ if(null == notificationChannel)
+ {
+ NotificationChannel channel = new NotificationChannel(channelId, "下载更新", NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setDescription("下载更新");
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
+ builder.setProgress(0, 0, true)
+ .setSmallIcon(icon)
+ .setTicker("下载更新")
+ .setOngoing(true)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle("下载更新")
+ .setContentText("开始下载......")
+ ;
+ int notifyId = 0x1003;
+ Notification notification = builder.build();
+ notificationManager.notify(notifyId, notification);
+
+ // 创建文件夹并删除缓存文件
+ File externalFilesDir = context.getExternalFilesDir(null);
+ String dir = externalFilesDir.getAbsolutePath() + File.separator + NSConstants.APK_UPDATE_DOWNLOAD_DIR;
+ Log.d(ID_TAG, "创建下载缓存目录: " + dir);
+ notificationManager.notify(notifyId, builder.setContentText("创建下载缓存目录").build());
+ FileUtil.mkdir(dir);
+
+ String file = dir + File.separator + NSConstants.APK_UPDATE_DOWNLOAD_FILE;
+ Log.d(ID_TAG, "删除历史缓存文件: " + file);
+ notificationManager.notify(notifyId, builder.setContentText("删除历史缓存文件").build());
+ FileUtil.del(file);
+
+ new Thread(() -> {
+ long bytes = HttpUtil.downloadFile(updateUrl, new File(file), new StreamProgress() {
+ @Override
+ public void start()
+ {
+ handler.post(() -> {
+ notificationManager.notify(notifyId, builder.setContentText("开始下载......").setProgress(100, 0, false).build());
+ });
+ }
+
+ @Override
+ public void progress(long progressSize)
+ {
+ handler.post(() -> {
+ notificationManager.notify(notifyId, builder.setContentText("下载: " + FileUtil.readableFileSize(progressSize)).setProgress(0, 0, true).build());
+ });
+ }
+
+ @Override
+ public void finish()
+ {
+ handler.post(() -> {
+ notificationManager.notify(notifyId, builder.setContentText("下载完成").setProgress(100, 100, false).setOngoing(false).build());
+ });
+ }
+ });
+ Log.d(ID_TAG, "下载apk文件: " + FileUtil.readableFileSize(bytes));
+ if(bytes > 0)
+ {
+ handler.postDelayed(() -> {
+ notificationManager.notify(notifyId, builder.setContentText("准备安装更新").setProgress(0, 0, false).setOngoing(false).build());
+ Toast.makeText(context, "准备安装......", Toast.LENGTH_SHORT).show();
+ NSContextUtils.InstallApk(context, file);
+
+// handler.postDelayed(() -> {
+// notificationManager.cancel(notifyId);
+// }, 2000);
+ }, 1000);
+ }
+ else
+ {
+ handler.postDelayed(() -> {
+ notificationManager.notify(notifyId, builder.setContentText("下载更新失败").setProgress(0, 0, false).setOngoing(false).build());
+ Toast.makeText(context, "下载更新失败", Toast.LENGTH_LONG).show();
+
+// handler.postDelayed(() -> {
+// notificationManager.cancel(notifyId);
+// }, 2000);
+ }, 1000);
+ }
+ }).start();
+ }
+
+ private void ResetSettings()
+ {
+ NSPreference preference = new NSPreference(getContext());
+ preference.Write()
+ .putString(NSPreference.LOCATION_MODE, NSConstants.DEFAULT_LOCATION_MODE)
+ .putString(NSPreference.LOCATION_GAODE_INTERVAL, "" + NSConstants.DEFAULT_LOCATION_GAODE_INTERVAL)
+ .putString(NSPreference.LOCATION_GAODE_READ_COUNT, "" + NSConstants.DEFAULT_LOCATION_GAODE_READ_COUNT)
+ .putBoolean(NSPreference.OPEN_LAST_URL, NSConstants.DEFAULT_OPEN_LAST_URL)
+ .putString(NSPreference.LOCATION_PROVIDER, NSConstants.DEFAULT_LOCATION_PROVIDER)
+ .putString(NSPreference.LOCATION_REALTIME_INTERVAL, "" + NSConstants.DEFAULT_LOCATION_REALTIME_INTERVAL)
+ .putString(NSPreference.LOCATION_REALTIME_DISTANCE, "" + NSConstants.DEFAULT_LOCATION_REALTIME_DISTANCE)
+ .putString(NSPreference.LOCATION_REALTIME_READ_COUNT, "" + NSConstants.DEFAULT_LOCATION_REALTIME_READ_COUNT)
+ .putInt(NSPreference.FONT_SCALE, NSConstants.DEFAULT_FONT_SCALE)
+ .putString(NSPreference.LOCATION_TIMEOUT, "" + NSConstants.DEFAULT_LOCATION_TIMEOUT)
+ .putBoolean(NSPreference.LOGCAT_CONSOLE_OUTPUT, NSConstants.DEFAULT_LOGCAT_CONSOLE_OUTPUT)
+ .commit()
+ ;
+ }
+
+ private void OpenQueryDialog(String title, String message, DialogInterface.OnClickListener listener)
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(title);
+ builder.setMessage(message);
+ builder.setIcon(R.drawable.no_words);
+ builder.setPositiveButton("确定", listener);
+ builder.setNegativeButton("取消", null);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private void OpenAbout()
+ {
+ Context context = getContext();
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ String msg = "
" + NSContextUtils.tr(context, R.string.company) + "
" + NSContextUtils.tr(context, R.string.copyright);
+ msg = "" + msg + "
";
+ builder.setTitle(R.string.app_name2);
+ builder.setMessage(Html.fromHtml(msg, Html.FROM_HTML_MODE_LEGACY, null, null));
+ int icon = context.getResources().getIdentifier("ic_launcher_" + NSConstants.AppIcon()/* + "_round"*/, "mipmap", context.getApplicationContext().getPackageName());
+ builder.setIcon(icon);
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ SetSummary(null, null);
+ }
+
+ public void SetSummary(String key, Object newValue)
+ {
+ Context context = getContext();
+ Preference preference;
+ String summary;
+ String value;
+ boolean b;
+ int i;
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if(key == null || NSPreference.LOCATION_MODE.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_MODE);
+ value = newValue != null ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_MODE, NSConstants.DEFAULT_LOCATION_MODE);
+ summary = NSContextUtils.GetListName(context, value, R.array.location_mode_values, R.array.location_mode_labels);
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_GAODE_INTERVAL.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_GAODE_INTERVAL);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_GAODE_INTERVAL, "" + NSConstants.DEFAULT_LOCATION_GAODE_INTERVAL);
+ summary = value + "毫秒";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_GAODE_READ_COUNT.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_GAODE_READ_COUNT);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_GAODE_READ_COUNT, "" + NSConstants.DEFAULT_LOCATION_GAODE_READ_COUNT);
+ summary = value + "次";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.OPEN_LAST_URL.equals(key))
+ {
+ preference = findPreference(NSPreference.OPEN_LAST_URL);
+ b = newValue != null ? (Boolean)newValue : sharedPreferences.getBoolean(NSPreference.OPEN_LAST_URL, NSConstants.DEFAULT_OPEN_LAST_URL);
+ summary = b ? "打开最近页面" : "打开默认页面";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_PROVIDER.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_PROVIDER);
+ value = newValue != null ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_PROVIDER, NSConstants.DEFAULT_LOCATION_PROVIDER);
+ summary = NSContextUtils.GetListName(context, value, R.array.location_provider_values, R.array.location_provider_labels);
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_REALTIME_INTERVAL.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_REALTIME_INTERVAL);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_REALTIME_INTERVAL, "" + NSConstants.DEFAULT_LOCATION_REALTIME_INTERVAL);
+ summary = value + "毫秒";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_REALTIME_DISTANCE.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_REALTIME_DISTANCE);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_REALTIME_DISTANCE, "" + NSConstants.DEFAULT_LOCATION_REALTIME_DISTANCE);
+ summary = value + "米";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_REALTIME_READ_COUNT.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_REALTIME_READ_COUNT);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_REALTIME_READ_COUNT, "" + NSConstants.DEFAULT_LOCATION_REALTIME_READ_COUNT);
+ summary = value + "次";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.FONT_SCALE.equals(key))
+ {
+ preference = findPreference(NSPreference.FONT_SCALE);
+ i = newValue != null ? (int)newValue : sharedPreferences.getInt(NSPreference.FONT_SCALE, NSConstants.DEFAULT_FONT_SCALE);
+ if(i == 0)
+ i = 100;
+ summary = i + "%";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOCATION_TIMEOUT.equals(key))
+ {
+ preference = findPreference(NSPreference.LOCATION_TIMEOUT);
+ value = newValue != null && StrUtil.isNotBlank(newValue.toString()) ? newValue.toString() : sharedPreferences.getString(NSPreference.LOCATION_TIMEOUT, "" + NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ i = NSStr.parseInt_s(value, 0);
+ summary = i > 0 ? i + "毫秒" : "不超时";
+ preference.setSummary(summary);
+ }
+
+ if(key == null || NSPreference.LOGCAT_CONSOLE_OUTPUT.equals(key))
+ {
+ preference = findPreference(NSPreference.LOGCAT_CONSOLE_OUTPUT);
+ b = newValue != null ? (Boolean)newValue : sharedPreferences.getBoolean(NSPreference.LOGCAT_CONSOLE_OUTPUT, NSConstants.DEFAULT_LOGCAT_CONSOLE_OUTPUT);
+ summary = b ? "控制台输出到logcat" : "不输出控制台";
+ preference.setSummary(summary);
+ }
+
+ findPreference(NSPreference.VERSION).setSummary(NSContextUtils.GetAppVersion(getContext()));
+ }
+}
+
diff --git a/app/src/main/java/com/nsgk/ruralWeb/utils/NSContextUtils.java b/app/src/main/java/com/nsgk/ruralWeb/utils/NSContextUtils.java
new file mode 100644
index 0000000..de1c308
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/utils/NSContextUtils.java
@@ -0,0 +1,122 @@
+package com.nsgk.ruralWeb.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Settings;
+import android.widget.Toast;
+
+import androidx.core.content.FileProvider;
+
+import com.nsgk.ruralWeb.sys.NSConstants;
+
+import java.io.File;
+
+public final class NSContextUtils
+{
+ public static void RequestLocationPermission(Activity context, int requestCode)
+ {
+ Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ context.startActivityForResult(intent, requestCode);
+ }
+
+ public static void OpenAppSetting(Context context)
+ {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package", context.getApplicationContext().getPackageName(), null);
+ intent.setData(uri);
+ context.startActivity(intent);
+ }
+
+ public static String GetAppVersion(Context context)
+ {
+ String version = "UNKNOWN";
+ try
+ {
+ PackageManager manager = context.getPackageManager();
+ PackageInfo info = manager.getPackageInfo(context.getApplicationContext().getPackageName(), 0);
+ version = info.versionName;
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ return version;
+ }
+
+ public static String GetListName(Context context, String value, int keyResource, int nameResource, String... def)
+ {
+ Resources resources = context.getResources();
+ String[] keys = resources.getStringArray(keyResource);
+ String[] names = resources.getStringArray(nameResource);
+
+ for(int i = 0; i < keys.length; i++)
+ {
+ if(keys[i].equals(value))
+ return names[i];
+ }
+ return null != def && def.length > 0 ? def[0] : null;
+ }
+
+ public static String tr(Context context, int id, Object...args)
+ {
+ return context.getResources().getString(id, args);
+ }
+
+ public static void OpenUrlExternally(Context context, String url)
+ {
+ Uri uri = Uri.parse(url);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ context.startActivity(intent);
+ }
+
+ public static void InstallApk(Context context, String apkFile)
+ {
+ File file = new File(apkFile);
+ if(!file.isFile())
+ return;
+
+ String packageName = context.getPackageName();
+ Uri apkUri = FileProvider.getUriForFile(context, packageName + ".fileprovider", file);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (!context.getPackageManager().canRequestPackageInstalls()) {
+ Toast.makeText(context, "Android 8以上安装apk请求允许未知来源App", Toast.LENGTH_LONG).show();
+ Intent settingsIntent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + packageName));
+ context.startActivity(settingsIntent);
+ } else {
+ context.startActivity(intent);
+ }
+ } else {
+ context.startActivity(intent);
+ }
+ }
+
+ public static boolean BuildIsDebug(Context context)
+ {
+ try
+ {
+ ApplicationInfo info = context.getApplicationInfo();
+ System.err.println(((info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) + " -> " + NSConstants.IsDebug());
+ return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 || NSConstants.IsDebug();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ return NSConstants.IsDebug(); // default is release
+ }
+ }
+
+ private NSContextUtils() {}
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/utils/NSMisc.java b/app/src/main/java/com/nsgk/ruralWeb/utils/NSMisc.java
new file mode 100644
index 0000000..dfec389
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/utils/NSMisc.java
@@ -0,0 +1,19 @@
+package com.nsgk.ruralWeb.utils;
+
+public final class NSMisc
+{
+ public static void noexcept(Runnable runnable)
+ {
+ if(null != runnable)
+ {
+ try
+ {
+ runnable.run();
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/utils/NSStr.java b/app/src/main/java/com/nsgk/ruralWeb/utils/NSStr.java
new file mode 100644
index 0000000..b902911
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/utils/NSStr.java
@@ -0,0 +1,38 @@
+package com.nsgk.ruralWeb.utils;
+
+import java.net.URL;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.URLUtil;
+
+public final class NSStr
+{
+ public static String GetUrlPart(String str)
+ {
+ URL url = URLUtil.url(str);
+ String protocol = url.getProtocol();
+ String host = url.getHost();
+ int port = url.getPort();
+ StringBuilder buf = new StringBuilder();
+ buf.append(protocol).append("://").append(host);
+ if(port >= 0)
+ buf.append(':').append(port);
+ return buf.toString();
+ }
+
+ public static int parseInt_s(String str, int...def)
+ {
+ int res = null != def && def.length > 0 ? def[0] : 0;
+ if(StrUtil.isBlank(str))
+ return res;
+ try
+ {
+ return Integer.parseInt(str);
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ return res;
+ }
+ }
+}
diff --git a/app/src/main/java/com/nsgk/ruralWeb/web/NSEnvWindowObject.java b/app/src/main/java/com/nsgk/ruralWeb/web/NSEnvWindowObject.java
new file mode 100644
index 0000000..a5b214e
--- /dev/null
+++ b/app/src/main/java/com/nsgk/ruralWeb/web/NSEnvWindowObject.java
@@ -0,0 +1,468 @@
+package com.nsgk.ruralWeb.web;
+
+import android.app.Activity;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.location.LocationManager;
+import android.os.Handler;
+import android.util.Log;
+import android.webkit.JavascriptInterface;
+import android.webkit.ValueCallback;
+import android.webkit.WebView;
+import android.widget.Toast;
+
+import androidx.core.app.ActivityCompat;
+
+import com.nsgk.ruralWeb.FullscreenActivity;
+import com.nsgk.ruralWeb.R;
+import com.nsgk.ruralWeb.enums.NSEnums;
+import com.nsgk.ruralWeb.location.NSLastKnownLocation;
+import com.nsgk.ruralWeb.location.NSLocationInfo;
+import com.nsgk.ruralWeb.location.NSAMapLocation;
+import com.nsgk.ruralWeb.location.NSRealtimeLocation;
+import com.nsgk.ruralWeb.location.NSSystemLocation;
+import com.nsgk.ruralWeb.sys.NSConstants;
+import com.nsgk.ruralWeb.sys.NSPreference;
+import com.nsgk.ruralWeb.utils.NSContextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class NSEnvWindowObject
+{
+ private static final String ID_TAG = NSEnvWindowObject.class.getName();
+
+ private final Handler m_handler;
+ private final Context m_context;
+ private final WebView m_webView;
+
+ private final NSSystemLocation m_systemLocation;
+ private final NSLastKnownLocation m_lastKnownLocation;
+ private final NSAMapLocation m_amapLocation;
+ private final NSRealtimeLocation m_realtimeLocation;
+
+ private final NSPreference m_preference;
+ // 最近一次获取到的定位坐标, 如果获取定位失败则返回此值. 可能不需要, 因为PASSIVE_PROVIDER就为最近的定位
+ private String m_lastLocation = null;
+
+ private static final int LOCATION_STATE_READY = 0; // 准备
+ private static final int LOCATION_STATE_PROCESSING = 1; // 进行中
+ private static final int LOCATION_STATE_FINISH = 2; // 完成
+ private static final int LOCATION_STATE_TERMINATED = 3; // 中止
+
+ private AtomicInteger m_locationState = new AtomicInteger(LOCATION_STATE_READY);
+
+ public NSEnvWindowObject(Context context, Handler handler, WebView webView)
+ {
+ m_context = context;
+ m_handler = handler;
+ m_webView = webView;
+ m_systemLocation = new NSSystemLocation(m_context);
+ m_lastKnownLocation = new NSLastKnownLocation(m_context);
+ m_amapLocation = new NSAMapLocation(m_context);
+ m_realtimeLocation = new NSRealtimeLocation(m_context);
+ m_preference = new NSPreference(m_context);
+ }
+
+ public void OnDestroy()
+ {
+ if(m_amapLocation.IsInitialized())
+ m_amapLocation.Shutdown();
+ if(m_realtimeLocation.IsInitialized())
+ m_realtimeLocation.Shutdown();
+ m_systemLocation.Shutdown();
+ }
+
+ @JavascriptInterface
+ public void Toast(final String message)
+ {
+ ShowToast(message, Toast.LENGTH_LONG);
+ }
+
+ private void ShowToast(final String message, int duration)
+ {
+ RunOnUIThread(() -> {
+ Toast.makeText(m_context, message, duration).show();
+ });
+ }
+
+ @JavascriptInterface
+ public void Log(final String message)
+ {
+ Log.i(ID_TAG, message);
+ }
+
+ @JavascriptInterface
+ public String GetLocation(final String type)
+ {
+ Log.d(ID_TAG, "Web线程: " + Thread.currentThread().getId());
+
+ if(!CheckLocationPermission())
+ {
+ return m_lastLocation;
+ }
+
+ SetLocationState(LOCATION_STATE_READY);
+ ShowIndicator();
+ NSLocationInfo loc;
+ String location = CurrentLocationMode();
+ String provider = m_preference.GetString(NSPreference.LOCATION_PROVIDER, NSConstants.DEFAULT_LOCATION_PROVIDER);
+ Log.d(ID_TAG, "定位提供器: " + provider);
+ SetLocationState(LOCATION_STATE_PROCESSING);
+ switch(location)
+ {
+ case NSEnums.LocationMode.GAODE:
+ loc = GetAMapLocation();
+ break;
+ case NSEnums.LocationMode.REALTIME:
+ loc = GetRealtimeLocation();
+ break;
+ case NSEnums.LocationMode.SYSTEM:
+ default:
+ loc = GetCurrentLocation(provider);
+ break;
+ }
+ SetLocationState(LOCATION_STATE_FINISH, LOCATION_STATE_PROCESSING);
+ if(null == loc)
+ {
+ if(IsLocationState(LOCATION_STATE_FINISH))
+ ShowToast("定位失败, 请先开启位置服务", Toast.LENGTH_LONG);
+ loc = GetLastKnownLocation(provider);
+ }
+
+ HideIndicator();
+ return SetLastLocation(loc);
+ }
+
+ public String CurrentLocationMode()
+ {
+ return m_preference.GetString(NSPreference.LOCATION_MODE, NSConstants.DEFAULT_LOCATION_MODE);
+ }
+
+ @JavascriptInterface
+ public String GetLocationMode()
+ {
+ return CurrentLocationMode();
+ }
+
+ @JavascriptInterface
+ public int GetLocationTimeout()
+ {
+ return m_preference.GetIntFromString(NSPreference.LOCATION_TIMEOUT, NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ }
+
+ @JavascriptInterface
+ public String SelectLocationMode()
+ {
+ final Object lock = new Object();
+ String current = CurrentLocationMode();
+ String[] res = { current };
+ synchronized(lock) {
+ try
+ {
+ RunOnUIThread(() -> {
+ AlertDialog.Builder builder = new AlertDialog.Builder(m_context);
+ String[] locationModeLabels = m_context.getResources().getStringArray(R.array.location_mode_labels);
+ String[] locationModeValues = m_context.getResources().getStringArray(R.array.location_mode_values);
+
+ List locationModeList = new ArrayList<>(Arrays.asList(locationModeLabels));
+ List locationModeValueList = new ArrayList<>(Arrays.asList(locationModeValues));
+
+ int[] selected = { locationModeValueList.indexOf(current) };
+
+ builder.setTitle("选择定位方式")
+ .setSingleChoiceItems(locationModeList.toArray(new String[0]), selected[0], (DialogInterface dialog, int which) -> {
+ selected[0] = which;
+ })
+ //.setCancelable(false)
+ ;
+
+ builder.setPositiveButton("确定", (DialogInterface dialog, int which) -> {
+ if(selected[0] >= 0)
+ {
+ String mode = locationModeValueList.get(selected[0]);
+ if(NSEnums.LocationMode.H5.equalsIgnoreCase(mode))
+ {
+ if(!NSConstants.IsHttps())
+ {
+ Toast.makeText(m_context, "当前不支持H5定位", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ m_preference.SetString(NSPreference.LOCATION_MODE, mode);
+ res[0] = mode;
+ String name = locationModeList.get(selected[0]);
+ Toast.makeText(m_context, "使用" + name + "定位", Toast.LENGTH_SHORT).show();
+ }
+ else
+ Toast.makeText(m_context, "请选择定位方式", Toast.LENGTH_SHORT).show();
+ });
+ builder.setNeutralButton("默认", (DialogInterface dialog, int which) -> {
+ res[0] = NSConstants.DEFAULT_LOCATION_MODE;
+ m_preference.SetString(NSPreference.LOCATION_MODE, res[0]);
+ int choose = locationModeValueList.indexOf(res[0]);
+ String name = locationModeList.get(choose);
+ Toast.makeText(m_context, "使用" + name + "定位", Toast.LENGTH_SHORT).show();
+ });
+ builder.setNegativeButton("取消", null);
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnDismissListener((DialogInterface d) -> {
+ synchronized(lock) {
+ lock.notifyAll();
+ }
+ });
+ dialog.show();
+ });
+ lock.wait();
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ return res[0];
+ }
+
+ private String ReturnLocation(NSLocationInfo loc)
+ {
+ return loc.longitude + "," + loc.latitude;
+ }
+
+ private String SetLastLocation(NSLocationInfo loc)
+ {
+ if(null != loc)
+ m_lastLocation = ReturnLocation(loc);
+ return m_lastLocation;
+ }
+
+ private NSLocationInfo GetCurrentLocation(String type)
+ {
+ Log.i(ID_TAG, "使用系统当前定位");
+ NSLocationInfo loc = null;
+ try
+ {
+ int timeout = m_preference.GetIntFromString(NSPreference.LOCATION_TIMEOUT, NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ long start = System.currentTimeMillis();
+ loc = m_systemLocation.Read(type, timeout);
+ long end = System.currentTimeMillis();
+ Log.i(ID_TAG, "系统当前定位结果: " + loc + ", 耗时=" + (end - start) + "毫秒");
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ return loc;
+ }
+
+ private NSLocationInfo GetLastKnownLocation(String type)
+ {
+ Log.i(ID_TAG, "使用系统最近定位");
+ NSLocationInfo loc;
+ try
+ {
+ long start = System.currentTimeMillis();
+ loc = m_lastKnownLocation.Read(type);
+ long end = System.currentTimeMillis();
+ Log.i(ID_TAG, "系统定位最近结果: " + loc + ", 耗时=" + (end - start) + "毫秒");
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ return m_lastKnownLocation.GetLastLocation();
+ }
+
+ private NSLocationInfo GetRealtimeLocation()
+ {
+ Log.i(ID_TAG, "使用系统实时定位");
+ NSLocationInfo loc = null;
+ try
+ {
+ NSPreference preference = new NSPreference(m_context);
+ if(!m_realtimeLocation.IsInitialized())
+ {
+ m_realtimeLocation.SetOption(NSRealtimeLocation.OPTION_MIN_TIME, preference.GetIntFromString(NSPreference.LOCATION_REALTIME_INTERVAL, NSConstants.DEFAULT_LOCATION_REALTIME_INTERVAL));
+ m_realtimeLocation.SetOption(NSRealtimeLocation.OPTION_MIN_DISTANCE, preference.GetIntFromString(NSPreference.LOCATION_REALTIME_DISTANCE, NSConstants.DEFAULT_LOCATION_REALTIME_DISTANCE));
+ int flag = NSRealtimeLocation.DEFAULT_FLAG | NSRealtimeLocation.FLAG_BUILTIN_THREAD;
+ String provider = preference.GetString(NSPreference.LOCATION_PROVIDER, NSConstants.DEFAULT_LOCATION_PROVIDER);
+ if(LocationManager.GPS_PROVIDER.equals(provider))
+ {
+ flag &= ~NSRealtimeLocation.FLAG_PRIORITY_NETWORK;
+ flag |= NSRealtimeLocation.FLAG_PRIORITY_GPS;
+ }
+ m_realtimeLocation.Init(flag);
+ }
+ long start = System.currentTimeMillis();
+ int count = preference.GetIntFromString(NSPreference.LOCATION_REALTIME_READ_COUNT, NSConstants.DEFAULT_LOCATION_REALTIME_READ_COUNT);
+ int timeout = preference.GetIntFromString(NSPreference.LOCATION_TIMEOUT, NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ loc = m_realtimeLocation.Read(count, timeout);
+ long end = System.currentTimeMillis();
+ Log.i(ID_TAG, "系统实时定位结果: " + loc + ", 耗时=" + (end - start) + "毫秒");
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ return loc;
+ }
+
+ public NSLocationInfo GetAMapLocation()
+ {
+ Log.i(ID_TAG, "使用高德定位");
+ NSLocationInfo loc = null;
+ try
+ {
+ NSPreference preference = new NSPreference(m_context);
+ if(!m_amapLocation.IsInitialized())
+ {
+ m_amapLocation.SetOption(NSAMapLocation.OPTION_INTERVAL, preference.GetIntFromString(NSPreference.LOCATION_GAODE_INTERVAL, NSConstants.DEFAULT_LOCATION_GAODE_INTERVAL));
+ m_amapLocation.Init(NSAMapLocation.DEFAULT_FLAG | NSAMapLocation.FLAG_BUILTIN_THREAD);
+ }
+ long start = System.currentTimeMillis();
+ int count = preference.GetIntFromString(NSPreference.LOCATION_GAODE_READ_COUNT, NSConstants.DEFAULT_LOCATION_GAODE_READ_COUNT);
+ int timeout = preference.GetIntFromString(NSPreference.LOCATION_TIMEOUT, NSConstants.DEFAULT_LOCATION_TIMEOUT);
+ loc = m_amapLocation.Read(count, timeout);
+ long end = System.currentTimeMillis();
+ Log.i(ID_TAG, "高德定位结果: " + loc + ", 耗时=" + (end - start) + "毫秒");
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ return loc;
+ }
+
+ private boolean CheckLocationPermission()
+ {
+ if(ActivityCompat.checkSelfPermission(m_context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(m_context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
+ {
+ Activity activity = (Activity) m_context;
+
+ if (activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) && activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)) // do not ask
+ {
+ Toast.makeText(m_context, "请先允许定位服务", Toast.LENGTH_LONG).show();
+ NSContextUtils.RequestLocationPermission(activity, FullscreenActivity.PERMISSION_LOCATION_REQUEST_CODE);
+ }
+ else
+ {
+ activity.requestPermissions(new String[] { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, FullscreenActivity.PERMISSION_LOCATION_REQUEST_CODE);
+ }
+
+ return false;
+ }
+ else
+ return true;
+ }
+
+ protected void RunOnUIThread(Runnable runnable)
+ {
+ if(null != m_handler)
+ m_handler.post(runnable);
+ else if(m_context instanceof Activity)
+ ((Activity)m_context).runOnUiThread(runnable);
+ else
+ {
+ Log.e(ID_TAG, "无法在UI线程中执行");
+ }
+ }
+
+ protected void CallJSFunc(final String func, Object...args)
+ {
+ StringBuilder sb = new StringBuilder();
+ for(int i = 0; i < args.length; i++)
+ {
+ sb.append("'").append(args[i].toString()).append("'");
+ if(i < args.length - 1)
+ sb.append(", ");
+ }
+ final String arg = sb.toString();
+ RunOnUIThread(() -> {
+ String script = "javascript:typeof(" + func + ") == 'function' && " + func + "(" + arg + ");";
+ Log.e(ID_TAG, String.format("调用js函数: 函数(%s), 参数(%s)", func, arg));
+ m_webView.evaluateJavascript(script, new ValueCallback() {
+ @Override
+ public void onReceiveValue(String value) {
+ Log.e(ID_TAG, String.format("js函数 %s 返回: %s", func, value));
+ }
+ });
+ });
+ }
+
+ protected void CallJSFunc_beforeAndroid4_4(final String func, Object...args)
+ {
+ StringBuilder sb = new StringBuilder();
+ for(int i = 0; i < args.length; i++)
+ {
+ sb.append("'").append(args[i].toString()).append("'");
+ if(i < args.length - 1)
+ sb.append(", ");
+ }
+ final String arg = sb.toString();
+ RunOnUIThread(() -> {
+ Log.e(ID_TAG, String.format("调用js函数: 函数(%s), 参数(%s)", func, arg));
+ String script = "javascript:typeof(" + func + ") == 'function' && " + func + "(" + arg + ");";
+ Log.e(ID_TAG, script);
+ m_webView.loadUrl(script, null);
+ });
+ }
+
+ protected void ShowIndicator()
+ {
+ RunOnUIThread(() -> {
+ ((FullscreenActivity)(m_context)).SetIndicatorVisible(true);
+ });
+ }
+
+ protected void HideIndicator()
+ {
+ RunOnUIThread(() -> {
+ ((FullscreenActivity)(m_context)).SetIndicatorVisible(false);
+ });
+ }
+
+ public void TerminateLocation()
+ {
+ if(!IsLocationState(LOCATION_STATE_PROCESSING))
+ return;
+ SetLocationState(LOCATION_STATE_TERMINATED, LOCATION_STATE_PROCESSING);
+ String location = CurrentLocationMode();
+ Log.i(ID_TAG, "中止定位: " + location);
+ switch(location)
+ {
+ case NSEnums.LocationMode.GAODE:
+ m_amapLocation.TerminateRead();
+ break;
+ case NSEnums.LocationMode.REALTIME:
+ m_realtimeLocation.TerminateRead();
+ break;
+ case NSEnums.LocationMode.SYSTEM:
+ m_systemLocation.TerminateRead();
+ break;
+ }
+
+ HideIndicator();
+ }
+
+ private void SetLocationState(int st)
+ {
+ m_locationState.set(st);
+ }
+
+ private void SetLocationState(int st, int cur)
+ {
+ m_locationState.compareAndSet(cur, st);
+ }
+
+ private boolean IsLocationState(int st)
+ {
+ return m_locationState.get() == st;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/indicator_background.xml b/app/src/main/res/drawable/indicator_background.xml
new file mode 100644
index 0000000..825c101
--- /dev/null
+++ b/app/src/main/res/drawable/indicator_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml
index 88e7cac..cd07a83 100644
--- a/app/src/main/res/layout/activity_welcome.xml
+++ b/app/src/main/res/layout/activity_welcome.xml
@@ -32,19 +32,21 @@
android:layout_marginBottom="8dp">
+ android:text="@string/copyright" />
+ android:text="@string/company" />
diff --git a/app/src/main/res/layout/location_indicator.xml b/app/src/main/res/layout/location_indicator.xml
new file mode 100644
index 0000000..2628d40
--- /dev/null
+++ b/app/src/main/res/layout/location_indicator.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..f00ff3d
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ - 系统(实时)
+ - 系统
+ - H5
+ - 高德
+
+
+
+ - realtime
+ - system
+ - h5
+ - gaode
+
+
+
+ - 卫星
+ - 网络
+
+
+
+ - gps
+ - network
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 731c223..1784f08 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,4 +3,9 @@
宅基地
Dummy Button
DUMMY\nCONTENT
+ Copyright © 2024 ZNRX All Rights Reserved.
+ 中农融信(北京)科技股份有限公司
+
+
+ 设置
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..c4dd1c9
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/settings_preference.xml b/app/src/main/res/xml/settings_preference.xml
new file mode 100644
index 0000000..84bd735
--- /dev/null
+++ b/app/src/main/res/xml/settings_preference.xml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 0000000..f93806d
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
index 6826e61..c308b18 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,4 +14,27 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
\ No newline at end of file
+android.useAndroidX=true
+
+# Signing
+storeFile=../app-keystore.jks
+storePassword=ns61GK32x%
+keyAlias=nsgk_rural_web
+keyPassword=ns61GK32x%
+
+# Command line arguments
+# [String] App home url, it will save to @string/app_home_url
+appHomeUrl=http://218.59.175.43:71/yinnongLogin
+# [String] App custom name, it will be set on res/values/strings.xml
+appName=@string/app_name
+# [String] App icon mipmap key name in res/mipmaps, icon = ic_launcher_${appIconKey}, round icon = ic_launcher_${appIconKey}_round
+appIconKey=yhzl
+# [String] App copyright name, it will save to @string/app_copyright. default using @string/copyright
+appCopyright=
+# [String] App vendor name, it will save to @string/app_vendor. default using @string/company
+appVendor=
+# [String] App update download url, it will save to @string/app_update_url
+# http://218.59.175.43:8090/nsgk/qixingguan.apk
+appUpdateUrl=
+# gaode amap key
+amapKey=490bef43ef0182379aa1a8bacc45d054
\ No newline at end of file
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/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/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/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/安装-发布包.bat b/安装-发布包.bat
new file mode 100644
index 0000000..8057af5
--- /dev/null
+++ b/安装-发布包.bat
@@ -0,0 +1,7 @@
+@echo off
+
+echo װapk()
+
+call adb install app\build\outputs\apk\release\app-release.apk
+
+pause;
diff --git a/安装-调试包.bat b/安装-调试包.bat
new file mode 100644
index 0000000..7ea6d3e
--- /dev/null
+++ b/安装-调试包.bat
@@ -0,0 +1,7 @@
+@echo off
+
+echo װapk(Debug)
+
+call adb install app\build\outputs\apk\debug\app-debug.apk
+
+pause;
diff --git a/打包-debug.bat b/打包-debug.bat
new file mode 100644
index 0000000..aa2bbd9
--- /dev/null
+++ b/打包-debug.bat
@@ -0,0 +1,10 @@
+@echo off
+
+echo apk()
+
+call gradlew assembleDebug
+
+echo app-debug.apkĿ¼......
+start "" app\build\outputs\apk\debug
+
+pause;
diff --git a/打包-正式.bat b/打包-正式.bat
new file mode 100644
index 0000000..aaaf6ee
--- /dev/null
+++ b/打包-正式.bat
@@ -0,0 +1,48 @@
+@echo off
+
+echo apk(ʽ)
+
+echo ÷: %0 Appҳӵַ App Appͼ "AppȨϢ" AppϢ
+echo ʹAppĿgradle.propertiesĬ
+
+set NUM_ARG=0
+for %%x in (%*) do (
+ set /a NUM_ARG+=1
+)
+
+echo : %NUM_ARG%
+
+if %NUM_ARG% GEQ 5 (
+echo Appҳӵַ: %1
+echo App: %2
+echo Appͼ: ic_launcher_%3; Բͼ: ic_launcher_%3_round
+echo AppȨϢ: %4
+echo AppϢ: %5
+call gradlew assembleRelease -PappHomeUrl=%1 -PappName=%2 -PappIconKey=%3 -PappCopyright=%4 -PappVendor=%5
+) else if %NUM_ARG% GEQ 4 (
+echo Appҳӵַ: %1
+echo App: %2
+echo Appͼ: ic_launcher_%3 Բͼ: ic_launcher_%3_round
+echo AppȨϢ: %4
+call gradlew assembleRelease -PappHomeUrl=%1 -PappName=%2 -PappIconKey=%3 -PappCopyright=%4
+) else if %NUM_ARG% GEQ 3 (
+echo Appҳӵַ: %1
+echo App: %2
+echo Appͼ: ic_launcher_%3; Բͼ: ic_launcher_%3_round
+call gradlew assembleRelease -PappHomeUrl=%1 -PappName=%2 -PappIconKey=%3
+) else if %NUM_ARG% GEQ 2 (
+echo Appҳӵַ: %1
+echo App: %2
+call gradlew assembleRelease -PappHomeUrl=%1 -PappName=%2
+) else if %NUM_ARG% GEQ 1 (
+echo Appҳӵַ: %1
+call gradlew assembleRelease -PappHomeUrl=%1
+) else (
+echo ʹgradle.propertiesĬ
+call gradlew assembleRelease
+)
+
+echo app-release.apkĿ¼......
+start "" app\build\outputs\apk\release
+
+pause;
diff --git a/生成keystore.bat b/生成keystore.bat
new file mode 100644
index 0000000..aa52917
--- /dev/null
+++ b/生成keystore.bat
@@ -0,0 +1,15 @@
+@echo off
+
+echo keystore
+
+call keytool -genkeypair -v -keystore app-keystore.jks -alias nsgk_rural_web -keyalg RSA -keysize 2048 -validity 365000
+
+rem Կ: ns61GK32x%
+rem ʲô? ZZL
+rem ֯λʲô? NSGK
+rem ֯ʲô? NSGK
+rem ڵijлʲô?
+rem ڵʡ//ʲô?
+rem õλ˫ĸ/ʲô? CN
+
+pause;