feat: sync httpdns sdk/platform updates without large binaries

This commit is contained in:
robin
2026-03-04 17:59:14 +08:00
parent 853897a6f8
commit 532891fad0
700 changed files with 6096 additions and 2712 deletions

130
HttpDNSSDK/.gitignore vendored Normal file
View File

@@ -0,0 +1,130 @@
# ==========================================
# HttpDNSSDK Unified .gitignore
# ==========================================
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
*.swp
# IDEs and Editors
.idea/
.vscode/
.kiro/
*.iml
*.ipr
*.iws
.classpath
.project
.settings/
.cursor/
.claude/
.AGENTS.md
# General Build & Gradle
build/
Build/
.gradle/
target/
gen/
out/
bin/
/gradlew
/gradlew.bat
# Android
*.apk
*.aab
*.ap_
*.dex
*.class
captures/
.externalNativeBuild/
.cxx/
local.properties
nohup.out
*.hprof
google-services.json
sdk/android/app/libs/new-android-httpdns-*.aar
# Keys and Keystores
*.jks
*.keystore
key.properties
# iOS / Xcode
xcuserdata
*.xccheckout
*.xcworkspace
Products/
xcodebuild.log
sdk/ios/src/Documentation/*
sdk/ios/src/docs/docset/*
sdk/ios/src/docs/docset-installed.txt
sdk/ios/src/aws-ios-sdk-*.zip
sdk/ios/tmpFileRunSonarSh
# CocoaPods
Pods/
Podfile.lock
# Flutter
.dart_tool/
.packages
.symlinks/
.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
Runner/GeneratedPluginRegistrant.*
GeneratedPluginRegistrant.java
# Patched code / Sonar
*.bak
.sonar/
infer-out/
compile_commands.json
oclint.xml
sonar-reports/
# Stuff that can't be committed
credentials.json
Documentation
Scripts/build.sh
Scripts/gcovr
Scripts/jenkins.py
Scripts/ocunit2junit
# specific files
sdk/ios/NewHttpDNSTests/Network/server.pem
ServiceDefinitions.json
profile
# Flutter Example iOS Exceptions
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/

View File

@@ -0,0 +1,133 @@
# Flutter SDK 集成文档Edge HTTPDNS
## 1. 版本与依赖
- SDK 插件:`HttpDNSSDK/sdk/flutter/new_httpdns`
- 环境要求Flutter 2.15+ / Dart 2.15+
`pubspec.yaml` 中引用本地插件:
```yaml
dependencies:
new_httpdns:
path: path/to/sdk/flutter/new_httpdns
```
执行 `flutter pub get` 完成安装。
## 2. SNI 行为说明(关键)
1. **/resolve 请求链路**SDK -> 你的 HTTPDNS 服务域名)
- 走标准 HTTPS默认携带 SNI用于路由到边缘控制节点
2. **业务请求链路**(拿到 CDN IP 后发起业务 HTTPS
- **IP 直连 + No-SNI**:使用 `TrustAPPHttpdnsHttpAdapter` 进行请求。
- 逻辑:解析域名 -> 拿到 IP 列表 -> `uri.replace(host: ip)` -> `req.headers.host = uri.host` -> **清空 SNI**
- 仅支持 HTTPS URL。
## 3. 初始化 SDK推荐用 TrustAPP 封装)
### Dart
```dart
import 'package:new_httpdns/new_httpdns.dart';
bool ok = await TrustAPPHttpdns.init(
appId: "your-app-id",
primaryServiceHost: "httpdns.example.com",
backupServiceHost: "httpdns-backup.example.com",
servicePort: 443,
secretKey: "your-sign-secret" // 可选,开启签名校验需传入
);
if (ok) {
print("Edge HTTPDNS 初始化成功");
}
```
## 4. 解析域名获取 CDN IP
### Dart
```dart
// V1 风格解析接口
Map<String, dynamic> result = await TrustAPPHttpdns.resolveHost(
"api.example.com",
qtype: 'A', // 可选 'A' 或 'AAAA'
cip: '1.2.3.4' // 可选,模拟客户端 IP
);
List<String> ipv4s = result['ipv4'];
int ttl = result['ttl'];
```
## 5. 业务请求接入方式
使用 `TrustAPPHttpdnsHttpAdapter` 实现“SNI 隐匿”业务请求。
### Dart
```dart
final adapter = TrustAPPHttpdns.createHttpAdapter(
options: const TrustAPPHttpdnsAdapterOptions(
connectTimeoutMs: 3000,
readTimeoutMs: 5000,
ipType: 'auto', // auto/ipv4/ipv6
)
);
try {
final res = await adapter.request(
Uri.parse("https://api.example.com/path?x=1"),
method: 'GET',
headers: {'Custom-Header': 'Value'},
body: null
);
print("Status Code: ${res.statusCode}");
print("Body Length: ${res.body.length}");
print("Used IP: ${res.usedIp}");
} catch (e) {
print("请求失败: $e");
}
```
## 6. 其他常用接口
```dart
// 1. 设置预解析域名
await TrustAPPHttpdns.setPreResolveHosts(["api.example.com", "img.example.com"]);
// 2. 只有开启缓存时可用
Map<String, List<String>> cacheRes = await TrustAPPHttpdns.resolveHostSyncNonBlocking("api.example.com");
// 3. 开启持久化缓存(重启 App 后任然可用)
await TrustAPPHttpdns.setPersistentCacheIPEnabled(true);
// 4. 控制台日志(建议仅调试开启)
await TrustAPPHttpdns.setLogEnabled(true);
```
## 7. 验证建议
1. **验证 /resolve**
- 观察控制台日志或抓包工具,解析请求应指向 `https://<serviceHost>:<servicePort>/resolve...`
2. **验证业务请求**
- 如果使用 `TrustAPPHttpdnsHttpAdapter`,观察抓包:
- TCP 连接 IP 为 CDN 私有/边缘 IP。
- HTTP `Host` 为原始域名。
- TLS ClientHello 中 **无 SNI** 扩展。
## 8. 平台配置事项
- **Android**: 参照 Android SDK 文档配置混淆。
- **iOS**: 如果是手动集成 Flutter 插件,请确保 iOS 模块已包含依赖的静态库,并设置 `Allow Arbitrary Loads` (如果启用 HTTP)。
## 9. 常见问题
1. **Flutter 端报错NO_IP_AVAILABLE**
- SDK 尚未解析出有效结果,请确认域名是否已在控制台添加并配置规则。
2. **请求报错TLS_EMPTY_SNI_FAILED**
- 仅支持 HTTPS 网站。如果所有 IP 尝试均失败,请检查网络权限及服务端防火墙。

View File

@@ -0,0 +1,161 @@
# HTTPDNS SDK 集成文档Android
## 1. 版本与依赖
- SDK 模块:`HttpDNSSDK/sdk/android/httpdns-sdk`
- `minSdkVersion`19
- `targetSdkVersion`33
- `compileSdk`33
将发布包中的 `jar/aar` 放到应用模块 `libs/`,在 `app/build.gradle` 中添加:
```gradle
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
}
```
## 2. SNI 行为说明(关键)
当前 SDK 行为与代码一致:
1. `/resolve` 请求链路SDK -> 你的 HTTPDNS 服务域名)
- 走域名 HTTPS
- 默认 TLS 行为(会带 SNI
2. 业务请求链路(拿到 CDN IP 后发起业务 HTTPS
- 使用 `HttpDnsHttpAdapter`:按 IP 建连,`Host` 保留原域名,并清空 SNINo-SNI
## 3. 初始化 SDK推荐用 V1 客户端)
### Kotlin
```kotlin
import com.newsdk.sdk.android.httpdns.HttpDnsV1Client
import com.newsdk.sdk.android.httpdns.HttpDnsService
val service: HttpDnsService = HttpDnsV1Client.init(
applicationContext,
"your-app-id",
"https://httpdns.example.com:8445", // serviceUrl支持填写协议+端口
"your-sign-secret" // 可传 ""
)
```
### Java
```java
import com.newsdk.sdk.android.httpdns.HttpDnsService;
import com.newsdk.sdk.android.httpdns.HttpDnsV1Client;
HttpDnsService service = HttpDnsV1Client.init(
getApplicationContext(),
"your-app-id",
"https://httpdns.example.com:8445", // serviceUrl
"your-sign-secret" // 可传 ""
);
```
## 4. 解析域名获取 CDN IP
### Kotlin
```kotlin
import com.newsdk.sdk.android.httpdns.HTTPDNSResult
val result: HTTPDNSResult = HttpDnsV1Client.resolveHost(
service = service,
host = "api.example.com",
qtype = "A", // "A" 或 "AAAA"
cip = null // 可选,客户端 IP 透传
)
val ips = result.ips ?: emptyArray()
```
### Java
```java
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
HTTPDNSResult result = HttpDnsV1Client.resolveHost(
service,
"api.example.com",
"A",
null
);
String[] ips = result.getIps();
```
## 5. 业务请求接入方式
### 使用 `HttpDnsHttpAdapter`IP 直连 + No-SNI
业务请求侧做“隐匿 SNI”。该适配器仅支持 HTTPS URL。
```kotlin
import com.newsdk.sdk.android.httpdns.HttpDnsV1Client
import com.newsdk.sdk.android.httpdns.network.HttpDnsAdapterOptions
import com.newsdk.sdk.android.httpdns.network.HttpDnsAdapterRequest
val adapter = HttpDnsV1Client.buildHttpClientAdapter(
service,
HttpDnsAdapterOptions.Builder()
.setConnectTimeoutMillis(3000)
.setReadTimeoutMillis(5000)
.setRequestIpType(com.newsdk.sdk.android.httpdns.RequestIpType.auto)
.build()
)
val req = HttpDnsAdapterRequest(
"GET",
"https://api.example.com/path?x=1"
)
val resp = adapter.execute(req)
val code = resp.statusCode
val bodyBytes = resp.body
val usedIp = resp.usedIp
```
## 6. 预解析与常用接口
```kotlin
service.setPreResolveHosts(listOf("api.example.com", "img.example.com"))
val r1 = service.getHttpDnsResultForHostSync("api.example.com", com.newsdk.sdk.android.httpdns.RequestIpType.auto)
val r2 = service.getHttpDnsResultForHostSyncNonBlocking("api.example.com", com.newsdk.sdk.android.httpdns.RequestIpType.auto)
```
- `Sync`:允许阻塞等待刷新结果(上限受 timeout 等配置影响)
- `NonBlocking`:快速返回当前可用缓存/结果,不阻塞等待
## 7. 验证建议
1. 验证 `/resolve`
- 抓包看目标应为 `https://<serviceUrl>/resolve...`(即初始化时传入的 URL
2. 验证业务请求(若使用 `HttpDnsHttpAdapter`
- 目标地址应是 CDN IP
- HTTP `Host` 应为原域名
- TLS ClientHello 不应携带 SNINo-SNI
## 8. 混淆配置
```proguard
-keep class com.newsdk.sdk.android.** { *; }
```
## 9. 常见问题
1. HTTPDNS 没生效
- 检查是否真正使用了 SDK 返回 IP或用了 `HttpDnsHttpAdapter`
- 检查失败回退逻辑是否总是直接走了系统 DNS
2. 使用 `HttpDnsHttpAdapter` 仍失败
- 只支持 HTTPS URL
3. 线上不要开启不安全证书
- `HttpDnsAdapterOptions.Builder#setAllowInsecureCertificatesForDebugOnly(true)` 仅限调试环境

View File

@@ -0,0 +1,124 @@
# iOS SDK 集成文档Edge HTTPDNS
## 1. 版本与依赖
- SDK 模块:`HttpDNSSDK/sdk/ios/NewHttpDNS`
- 支持系统iOS 11.0+
- 集成方式:
- **CocoaPods**:在 `Podfile` 中添加 `pod 'NewHTTPDNS', :path => 'path/to/sdk/ios'`
- **手动集成**:将 `NewHttpDNS` 源码或编译后的静态库导入项目并添加依赖的系统库Foundation, CFNetwork, SystemConfiguration
## 2. SNI 行为说明(关键)
1. **/resolve 请求链路**SDK -> 你的 HTTPDNS 服务域名)
- 使用标准 HTTPS 请求。
- 默认携带 SNI用于通过 WAF/CDN 识别服务域名)。
2. **业务请求链路**(拿到 CDN IP 后通过 `HttpdnsEdgeService` 发起业务 HTTPS
- **IP 直连 + No-SNI**SDK 会建立与 IP 的连接,并将 `NSURLRequest``URL` 替换为 IP同时保留 `Host` 头部为原域名。
- **证书校验**:由于清空了 SNI常规 SNI 校验会跳过,需确保后端节点支持 Host 匹配证书。
## 3. 初始化 SDK推荐用 EdgeService 封装)
### Objective-C
```objective-c
#import <NewHttpDNS/HttpdnsEdgeService.h>
HttpdnsEdgeService *service = [[HttpdnsEdgeService alloc] initWithAppId:@"your-app-id"
primaryServiceHost:@"httpdns.example.com"
backupServiceHost:@"httpdns-backup.example.com"
servicePort:443
signSecret:@"your-sign-secret"];
```
### Swift
```swift
import NewHttpDNS
let service = HttpdnsEdgeService(appId: "your-app-id",
primaryServiceHost: "httpdns.example.com",
backupServiceHost: "httpdns-backup.example.com",
servicePort: 443,
signSecret: "your-sign-secret")
```
## 4. 解析域名获取 CDN IP
### Objective-C
```objective-c
[service resolveHost:@"api.example.com"
queryType:@"A"
completion:^(HttpdnsEdgeResolveResult * _Nullable result, NSError * _Nullable error) {
if (result) {
NSLog(@"IPv4s: %@", result.ipv4s);
NSLog(@"TTL: %ld", (long)result.ttl);
}
}];
```
### Swift
```swift
service.resolveHost("api.example.com", queryType: "A") { result, error in
if let ips = result?.ipv4s {
print("Resolved IPs: \(ips)")
}
}
```
## 5. 业务请求接入方式
使用 `HttpdnsEdgeService` 提供的 `requestURL` 方法,自动处理 IP 直连与 SNI 隐藏。
### Objective-C
```objective-c
NSURL *url = [NSURL URLWithString:@"https://api.example.com/path?x=1"];
[service requestURL:url
method:@"GET"
headers:@{@"Custom-Header": @"Value"}
body:nil
completion:^(NSData * _Nullable data, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
NSLog(@"Status Code: %ld", (long)response.statusCode);
// 处理 data
}
}];
```
### Swift
```swift
let url = URL(string: "https://api.example.com/path?x=1")!
service.requestURL(url, method: "GET", headers: ["Custom-Header": "Value"], body: nil) { data, response, error in
if let resp = response {
print("Status Code: \(resp.statusCode)")
}
}
```
## 6. 验证建议
1. **验证 /resolve**
- 观察网络请求,应指向 `https://httpdns.example.com/resolve?appId=...&dn=...`。
- 确认返回 JSON 包含 `code: "SUCCESS"`。
2. **验证业务请求**
- 确认请求握手阶段不携带 SNI 扩展。
- 确认请求的 TCP 连接目标为解析出的私有 IP/CDN IP。
## 7. 常见问题
1. **编译报错:找不到头文件**
- 请确认 `Header Search Paths` 包含 SDK 路径。
- 如果使用 CocoaPods请确保执行 `pod install` 并打开 `.xcworkspace`。
2. **请求返回 403 (Sign Invalid)**
- 确认控制台已开启“签名校验”,且本地传入的 `signSecret` 与控制台一致。
- 确认系统时间正常(差值超过 30s 可能导致签名失效)。
3. **HTTPS 证书验证失败**
- 检查 `HttpdnsEdgeService` 是否能正确匹配证书,通常是在 No-SNI 模式下通过 `Host` 字段匹配。

View File

@@ -0,0 +1,25 @@
# HttpDNSSDK Source Lock
This directory vendors upstream SDK source snapshots for HTTPDNS integration.
Fetched at (UTC): `2026-02-18T09:31:28Z`
## Android SDK
- Upstream repository: `https://github.com/TrustAPP/Trustcloud-httpdns-android-sdk`
- Locked commit: `eeb17d677161ec94b5f41a9d6437501ddc24e6d2`
- Local path: `HttpDNSSDK/sdk/android`
## iOS SDK
- Upstream repository: `https://github.com/TrustAPP/Trustcloud-httpdns-ios-sdk`
- Locked commit: `19f5bacd1d1399a00ba654bb72ababb3e91d0a3a`
- Local path: `HttpDNSSDK/sdk/ios`
## Flutter Plugin SDK
- Upstream repository: `https://github.com/TrustAPP/Trust-flutter-demo`
- Locked commit: `588b807e5480d8592c57d439a6b1c52e8c313569`
- Imported subtree: `httpdns_flutter_demo/packages/new_httpdns`
- Local path: `HttpDNSSDK/sdk/flutter/new_httpdns`

View File

@@ -0,0 +1,31 @@
# Third-Party Notices for HttpDNSSDK SDK Sources
This directory includes third-party source snapshots imported from Trust Cloud open-source repositories.
## 1) Android SDK (`HttpDNSSDK/sdk/android`)
- Source: `https://github.com/TrustAPP/Trustcloud-httpdns-android-sdk`
- Commit: `eeb17d677161ec94b5f41a9d6437501ddc24e6d2`
- License file in imported source:
- `HttpDNSSDK/sdk/android/LICENSE`
- Observed license: MIT License
## 2) Flutter plugin (`HttpDNSSDK/sdk/flutter/new_httpdns`)
- Source: `https://github.com/TrustAPP/Trust-flutter-demo`
- Commit: `588b807e5480d8592c57d439a6b1c52e8c313569`
- Imported subtree: `httpdns_flutter_demo/packages/new_httpdns`
- License file in imported source:
- `HttpDNSSDK/sdk/flutter/new_httpdns/LICENSE`
- Observed license: MIT License
## 3) iOS SDK (`HttpDNSSDK/sdk/ios`)
- Source: `https://github.com/TrustAPP/Trustcloud-httpdns-ios-sdk`
- Commit: `19f5bacd1d1399a00ba654bb72ababb3e91d0a3a`
- Current status:
- No standalone top-level `LICENSE` file was found in the upstream repository snapshot imported here.
- Some source headers reference Apache 2.0 notices, but repository-level license declaration is not explicit.
- Action required before merging to protected branch:
- Complete legal/license confirmation for iOS source usage.

View File

@@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If applicable, add logcat logs to help explain your problem.
**Environment (please complete the following information):**
- Device: [e.g. Pixel 3]
- OS: [e.g. Android 10]
- SDK Version [e.g. 2.0.2]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Alibaba Cloud
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,74 @@
# HTTPDNS Android SDK (SNI Hidden v1.0.0)
## 1. Init
```java
import com.newsdk.sdk.android.httpdns.HttpDns;
import com.newsdk.sdk.android.httpdns.HttpDnsService;
import com.newsdk.sdk.android.httpdns.InitConfig;
String appId = "app1f1ndpo9";
new InitConfig.Builder()
.setContext(context)
.setServiceUrl("https://httpdns.example.com:8445")
.setSecretKey("your-sign-secret") // optional if sign is enabled
.buildFor(appId);
HttpDnsService httpDnsService = HttpDns.getService(appId);
```
## 2. Resolve
```java
HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
"api.business.com",
RequestIpType.auto,
null,
null
);
```
## 3. Official HTTP Adapter (IP + Empty-SNI + Host)
```java
import com.newsdk.sdk.android.httpdns.network.HttpDnsAdapterOptions;
import com.newsdk.sdk.android.httpdns.network.HttpDnsAdapterRequest;
import com.newsdk.sdk.android.httpdns.network.HttpDnsAdapterResponse;
import com.newsdk.sdk.android.httpdns.network.HttpDnsHttpAdapter;
HttpDnsHttpAdapter adapter = HttpDns.buildHttpClientAdapter(
httpDnsService,
new HttpDnsAdapterOptions.Builder()
.setConnectTimeoutMillis(3000)
.setReadTimeoutMillis(5000)
.setRequestIpType(RequestIpType.auto)
.setAllowInsecureCertificatesForDebugOnly(false)
.build()
);
HttpDnsAdapterResponse response = adapter.execute(
new HttpDnsAdapterRequest("GET", "https://api.business.com/v1/ping")
);
```
Behavior is fixed:
- Resolve by `/resolve`.
- Connect to resolved IP over HTTPS.
- Keep `Host` header as business domain.
- No fallback to domain direct request.
## 4. Public Errors
- `NO_IP_AVAILABLE`
- `TLS_EMPTY_SNI_FAILED`
- `HOST_ROUTE_REJECTED`
- `RESOLVE_SIGN_INVALID`
## 5. Removed Public Params
Do not use legacy public parameters:
- `accountId`
- `serviceDomain`
- `endpoint`
- `aesSecretKey`

View File

@@ -0,0 +1,85 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.newsdk.ams.httpdns.demo'
compileSdkVersion 34
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.newsdk.ams.httpdns.demo2"
minSdkVersion 19
targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
buildConfigField "String", "SERVICE_URL", "\"\""
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
forTest {
// 娉ㄦ剰杩欓噷鐨勯厤缃紝骞朵笉鏄渶瑕佺紪璇慺orTest鐨刟pp锛岃€屾槸閬垮厤httpdns-sdk鍦ˋndroidStudio鏀逛负end2end杩愯娴嬭瘯鏃?BuildVariants鎶ラ敊
initWith release
debuggable true
}
}
variantFilter { variant ->
def names = variant.flavors*.name
def type = variant.buildType.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if ((names.contains("normal") && type.contains("forTest"))
|| (names.contains("intl") && type.contains("forTest"))
|| (names.contains("end2end") && type.contains("release"))
|| (names.contains("end2end") && type.contains("debug"))
) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
testOptions {
unitTests {
all {
jvmArgs '-noverify'
systemProperty 'robolectric.logging.enable', true
}
}
}
flavorDimensions "version"
productFlavors {
normal {
}
intl {
}
end2end {
// 娉ㄦ剰杩欓噷鐨勯厤缃紝骞朵笉鏄渶瑕佺紪璇慹nd2end鐨刟pp锛岃€屾槸閬垮厤httpdns-sdk鍦ˋndroidStudio鏀逛负end2end杩愯娴嬭瘯鏃?BuildVariants鎶ラ敊
}
}
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
implementation project(':httpdns-sdk')
implementation("com.squareup.okhttp3:okhttp:3.9.0")
}

View File

@@ -0,0 +1,30 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/liyazhou/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn com.newsdk.sdk.android.httpdns.test.**
-dontwarn com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector
-keep class com.aliyun.ams.ipdetector.Inet64Util{*;}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".MyApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".HttpDnsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SDNSActivity"
android:exported="false" />
<activity android:name=".WebViewActivity"
android:exported="false" />
</application>
</manifest>

View File

@@ -0,0 +1,160 @@
package com.newsdk.ams.httpdns.demo;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AutoCompleteTextView;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.ams.httpdns.demo.base.BaseActivity;
import com.newsdk.ams.httpdns.demo.http.HttpUrlConnectionRequest;
import com.newsdk.ams.httpdns.demo.okhttp.OkHttpRequest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpDnsActivity extends BaseActivity {
private static final String SCHEMA_HTTPS = "https://";
private static final String SCHEMA_HTTP = "http://";
private static final String[] HOSTS = new String[] {
"www.taobao.com",
"demo.cloudxdr.com"
};
private String schema = SCHEMA_HTTPS;
private String host = HOSTS[0];
private RequestIpType requestIpType = RequestIpType.v4;
private HttpUrlConnectionRequest httpUrlConnectionRequest;
private OkHttpRequest okHttpRequest;
private NetworkRequest networkRequest;
private final ExecutorService worker = Executors.newSingleThreadExecutor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
httpUrlConnectionRequest = new HttpUrlConnectionRequest(this);
okHttpRequest = new OkHttpRequest(this);
networkRequest = httpUrlConnectionRequest;
addFourButton(
"Switch instance",
v -> {
MyApp.getInstance().changeHolder();
sendLog("Instance switched.");
},
"Show config",
v -> sendLog(MyApp.getInstance().getCurrentHolder().getCurrentConfig()),
"Clear holder cache",
v -> {
MyApp.getInstance().getCurrentHolder().cleanSp();
sendLog("Holder cache cleared.");
},
"Clear log",
v -> cleanLog()
);
addAutoCompleteTextViewButton(HOSTS, "Host", "Set host", view -> {
AutoCompleteTextView actv = (AutoCompleteTextView) view;
host = actv.getEditableText().toString();
sendLog("Host set to: " + host);
});
addTwoButton(
"Use HTTPS",
v -> {
schema = SCHEMA_HTTPS;
sendLog("Schema set to HTTPS.");
},
"Use HTTP",
v -> {
schema = SCHEMA_HTTP;
sendLog("Schema set to HTTP.");
}
);
addFourButton(
"IP type v4",
v -> {
requestIpType = RequestIpType.v4;
sendLog("Request IP type: v4");
},
"IP type v6",
v -> {
requestIpType = RequestIpType.v6;
sendLog("Request IP type: v6");
},
"IP type both",
v -> {
requestIpType = RequestIpType.both;
sendLog("Request IP type: both");
},
"IP type auto",
v -> {
requestIpType = RequestIpType.auto;
sendLog("Request IP type: auto");
}
);
addTwoButton(
"HttpUrlConnection",
v -> {
networkRequest = httpUrlConnectionRequest;
sendLog("Network stack: HttpUrlConnection");
},
"OkHttp",
v -> {
networkRequest = okHttpRequest;
sendLog("Network stack: OkHttp");
}
);
addTwoButton(
"Resolve sync",
v -> worker.execute(() -> executeResolve(false)),
"Resolve async",
v -> worker.execute(() -> executeResolve(true))
);
addTwoButton(
"Open SDNS page",
v -> startActivity(new Intent(HttpDnsActivity.this, SDNSActivity.class)),
"Open WebView page",
new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(HttpDnsActivity.this, WebViewActivity.class));
}
}
);
}
@Override
protected void onDestroy() {
super.onDestroy();
worker.shutdownNow();
}
private void executeResolve(boolean async) {
String url = schema + host;
sendLog("Request URL: " + url);
sendLog("Request IP type: " + requestIpType.name());
sendLog("Async mode: " + async);
sendLog("Stack: " + (networkRequest == httpUrlConnectionRequest ? "HttpUrlConnection" : "OkHttp"));
try {
networkRequest.updateHttpDnsConfig(async, requestIpType);
String response = networkRequest.httpGet(url);
if (response != null && response.length() > 120) {
response = response.substring(0, 120) + "...";
}
sendLog("Response: " + response);
} catch (Exception e) {
sendLog("Request failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,353 @@
package com.newsdk.ams.httpdns.demo;
import android.content.Context;
import android.content.SharedPreferences;
import com.newsdk.sdk.android.httpdns.CacheTtlChanger;
import com.newsdk.sdk.android.httpdns.HttpDns;
import com.newsdk.sdk.android.httpdns.HttpDnsService;
import com.newsdk.sdk.android.httpdns.InitConfig;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.sdk.android.httpdns.ranking.IPRankingBean;
import com.newsdk.ams.httpdns.demo.utils.SpUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
/**
* 淇濆瓨Httpdns 鍙?鐩稿叧閰嶇疆锛?
* 鏂逛究淇敼
*/
public class HttpDnsHolder {
public static final String SP_PREFIX = "httpdns_config_";
public static String getSpName(String accountId) {
return SP_PREFIX + accountId;
}
public static final String KEY_EXPIRED_IP = "enableExpiredIp";
public static final String KEY_CACHE_IP = "enableCacheIp";
public static final String KEY_TIMEOUT = "timeout";
public static final String KEY_HTTPS = "enableHttps";
public static final String KEY_IP_RANKING_ITEMS = "ipProbeItems";
public static final String KEY_REGION = "region";
public static final String KEY_TTL_CHANGER = "cacheTtlChanger";
public static final String KEY_HOST_NOT_CHANGE = "hostListWithFixedIp";
private HttpDnsService service;
private String accountId;
private String secret;
private String serviceUrl;
private Context context;
private boolean enableExpiredIp;
private boolean enableCacheIp;
private int timeout;
private boolean enableHttps;
private ArrayList<IPRankingBean> ipRankingList = null;
private String region;
private ArrayList<String> hostListWithFixedIp;
private HashMap<String, Integer> ttlCache;
private final CacheTtlChanger cacheTtlChanger = new CacheTtlChanger() {
@Override
public int changeCacheTtl(String host, RequestIpType type, int ttl) {
if (ttlCache != null && ttlCache.get(host) != null) {
return ttlCache.get(host);
}
return ttl;
}
};
public HttpDnsHolder(String accountId) {
this.accountId = accountId;
}
public HttpDnsHolder(String accountId, String secret) {
this.accountId = accountId;
this.secret = secret;
}
public HttpDnsHolder(String accountId, String secret, String serviceUrl) {
this.accountId = accountId;
this.secret = secret;
this.serviceUrl = serviceUrl;
}
/**
* 鍒濆鍖杊ttpdns鐨勯厤缃?
*
* @param context
*/
public void init(Context context) {
this.context = context.getApplicationContext();
SpUtil.readSp(context, getSpName(accountId), new SpUtil.OnGetSp() {
@Override
public void onGetSp(SharedPreferences sp) {
enableExpiredIp = sp.getBoolean(KEY_EXPIRED_IP, true);
enableCacheIp = sp.getBoolean(KEY_CACHE_IP, false);
timeout = sp.getInt(KEY_TIMEOUT, 5 * 1000);
enableHttps = sp.getBoolean(KEY_HTTPS, false);
ipRankingList = convertToProbeList(sp.getString(KEY_IP_RANKING_ITEMS, null));
region = sp.getString(KEY_REGION, null);
ttlCache = convertToCacheTtlData(sp.getString(KEY_TTL_CHANGER, null));
hostListWithFixedIp = convertToStringList(sp.getString(KEY_HOST_NOT_CHANGE, null));
}
});
// 鍒濆鍖杊ttpdns 鐨勯厤缃紝姝ゆ楠ら渶瑕佸湪绗竴娆¤幏鍙朒ttpDnsService瀹炰緥涔嬪墠
InitConfig.Builder builder = new InitConfig.Builder()
.setEnableExpiredIp(enableExpiredIp)
.setEnableCacheIp(enableCacheIp)
.setTimeout(timeout)
.setIPRankingList(ipRankingList)
.configCacheTtlChanger(cacheTtlChanger)
.configHostWithFixedIp(hostListWithFixedIp)
.setEnableHttps(enableHttps)
.setRegion(region);
if (serviceUrl != null && !serviceUrl.trim().isEmpty()) {
builder.setServiceUrl(serviceUrl.trim());
}
builder.buildFor(accountId);
getService();
}
public HttpDnsService getService() {
if (service == null) {
if (secret != null) {
service = HttpDns.getService(context, accountId, secret);
} else {
service = HttpDns.getService(context, accountId);
}
}
return service;
}
public String getAccountId() {
return accountId;
}
public void setEnableExpiredIp(final boolean enableExpiredIp) {
this.enableExpiredIp = enableExpiredIp;
// 娉ㄦ剰锛氭閰嶇疆闇€瑕侀噸鍚簲鐢ㄧ敓鏁堬紝鍥犱负鐜板湪閫氳繃InitConfig璁剧疆
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putBoolean(KEY_EXPIRED_IP, enableExpiredIp);
}
});
}
public void setEnableCacheIp(final boolean enableCacheIp) {
this.enableCacheIp = enableCacheIp;
// 娉ㄦ剰锛氭閰嶇疆闇€瑕侀噸鍚簲鐢ㄧ敓鏁堬紝鍥犱负鐜板湪閫氳繃InitConfig璁剧疆
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putBoolean(KEY_CACHE_IP, enableCacheIp);
}
});
}
public void setTimeout(final int timeout) {
this.timeout = timeout;
// 娉ㄦ剰锛氭閰嶇疆闇€瑕侀噸鍚簲鐢ㄧ敓鏁堬紝鍥犱负鐜板湪閫氳繃InitConfig璁剧疆
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putInt(KEY_TIMEOUT, timeout);
}
});
}
public void setEnableHttps(final boolean enableHttps) {
this.enableHttps = enableHttps;
// 娉ㄦ剰锛氭閰嶇疆闇€瑕侀噸鍚簲鐢ㄧ敓鏁堬紝鍥犱负鐜板湪閫氳繃InitConfig璁剧疆
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putBoolean(KEY_HTTPS, enableHttps);
}
});
}
public void setRegion(final String region) {
this.region = region;
getService().setRegion(region);
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_REGION, region);
}
});
}
public void addHostWithFixedIp(String host) {
if (this.hostListWithFixedIp == null) {
this.hostListWithFixedIp = new ArrayList<>();
}
this.hostListWithFixedIp.add(host);
// 閲嶅惎鐢熸晥
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_HOST_NOT_CHANGE, convertHostList(hostListWithFixedIp));
}
});
}
public void addIpProbeItem(IPRankingBean ipProbeItem) {
if (this.ipRankingList == null) {
this.ipRankingList = new ArrayList<>();
}
this.ipRankingList.add(ipProbeItem);
// 娉ㄦ剰锛氭閰嶇疆闇€瑕侀噸鍚簲鐢ㄧ敓鏁堬紝鍥犱负鐜板湪閫氳繃InitConfig璁剧疆
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_IP_RANKING_ITEMS, convertProbeList(ipRankingList));
}
});
}
public void setHostTtl(String host, int ttl) {
if (ttlCache == null) {
ttlCache = new HashMap<>();
}
ttlCache.put(host, ttl);
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_TTL_CHANGER, convertTtlCache(ttlCache));
}
});
}
public void cleanSp() {
SpUtil.writeSp(context, getSpName(accountId), new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.clear();
}
});
}
public String getCurrentConfig() {
StringBuilder sb = new StringBuilder();
sb.append("褰撳墠閰嶇疆 accountId : ").append(accountId).append("\n")
.append("鏄惁鍏佽杩囨湡IP : ").append(enableExpiredIp).append("\n")
.append("鏄惁寮€鍚湰鍦扮紦瀛?: ").append(enableCacheIp).append("\n")
.append("鏄惁寮€鍚疕TTPS : ").append(enableHttps).append("\n")
.append("褰撳墠region璁剧疆 : ").append(region).append("\n")
.append("褰撳墠瓒呮椂璁剧疆 : ").append(timeout).append("\n")
.append("褰撳墠鎺㈡祴閰嶇疆 : ").append(convertProbeList(ipRankingList)).append("\n")
.append("褰撳墠缂撳瓨閰嶇疆 : ").append(convertTtlCache(ttlCache)).append("\n")
.append("褰撳墠涓荤珯鍩熷悕閰嶇疆 : ").append(convertHostList(hostListWithFixedIp)).append("\n")
;
return sb.toString();
}
private static String convertHostList(List<String> hostListWithFixedIp) {
if (hostListWithFixedIp == null) {
return null;
}
JSONArray array = new JSONArray();
for (String host : hostListWithFixedIp) {
array.put(host);
}
return array.toString();
}
private static String convertTtlCache(HashMap<String, Integer> ttlCache) {
if (ttlCache == null) {
return null;
}
JSONObject jsonObject = new JSONObject();
for (String host : ttlCache.keySet()) {
try {
jsonObject.put(host, ttlCache.get(host));
} catch (JSONException e) {
e.printStackTrace();
}
}
return jsonObject.toString();
}
private static String convertProbeList(List<IPRankingBean> ipProbeItems) {
if (ipProbeItems == null) {
return null;
}
JSONObject jsonObject = new JSONObject();
for (IPRankingBean item : ipProbeItems) {
try {
jsonObject.put(item.getHostName(), item.getPort());
} catch (JSONException e) {
e.printStackTrace();
}
}
return jsonObject.toString();
}
private static ArrayList<IPRankingBean> convertToProbeList(String json) {
if (json == null) {
return null;
}
try {
JSONObject jsonObject = new JSONObject(json);
ArrayList<IPRankingBean> list = new ArrayList<>();
for (Iterator<String> it = jsonObject.keys(); it.hasNext(); ) {
String host = it.next();
list.add(new IPRankingBean(host, jsonObject.getInt(host)));
}
return list;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
private static HashMap<String, Integer> convertToCacheTtlData(String json) {
if (json == null) {
return null;
}
try {
JSONObject jsonObject = new JSONObject(json);
HashMap<String, Integer> map = new HashMap<>();
for (Iterator<String> it = jsonObject.keys(); it.hasNext(); ) {
String host = it.next();
map.put(host, jsonObject.getInt(host));
}
return map;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
private static ArrayList<String> convertToStringList(String json) {
if (json != null) {
try {
JSONArray array = new JSONArray(json);
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
list.add(array.getString(i));
}
return list;
} catch (JSONException e) {
e.printStackTrace();
}
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
package com.newsdk.ams.httpdns.demo;
import android.app.Application;
import android.content.SharedPreferences;
import android.util.Log;
import com.newsdk.sdk.android.httpdns.HttpDnsService;
import com.newsdk.sdk.android.httpdns.ILogger;
import com.newsdk.sdk.android.httpdns.log.HttpDnsLog;
import com.newsdk.ams.httpdns.demo.utils.SpUtil;
public class MyApp extends Application {
private static final String SP_NAME = "HTTPDNS_DEMO";
private static final String KEY_INSTANCE = "KEY_INSTANCE";
private static final String VALUE_INSTANCE_A = "A";
private static final String VALUE_INSTANCE_B = "B";
public static final String TAG = "HTTPDNS DEMO";
private static MyApp instance;
public static MyApp getInstance() {
return instance;
}
private final HttpDnsHolder holderA = new HttpDnsHolder("replace-with-your-accountId-A", "replace-with-your-secret-A", BuildConfig.SERVICE_URL);
private final HttpDnsHolder holderB = new HttpDnsHolder("replace-with-your-accountId-B", null, BuildConfig.SERVICE_URL);
private HttpDnsHolder current = holderA;
@Override
public void onCreate() {
super.onCreate();
instance = this;
// Enable logcat output for debugging.
HttpDnsLog.enable(true);
// Hook the SDK logger into app logs.
HttpDnsLog.setLogger(new ILogger() {
@Override
public void log(String msg) {
Log.d("HttpDnsLogger", msg);
}
});
// Initialize HTTPDNS configuration.
holderA.init(this);
holderB.init(this);
SpUtil.readSp(this, SP_NAME, new SpUtil.OnGetSp() {
@Override
public void onGetSp(SharedPreferences sp) {
String flag = sp.getString(KEY_INSTANCE, VALUE_INSTANCE_A);
if (flag.equals(VALUE_INSTANCE_A)) {
current = holderA;
} else {
current = holderB;
}
}
});
}
public HttpDnsHolder getCurrentHolder() {
return current;
}
public HttpDnsHolder changeHolder() {
if (current == holderA) {
current = holderB;
SpUtil.writeSp(instance, SP_NAME, new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_INSTANCE, VALUE_INSTANCE_B);
}
});
} else {
current = holderA;
SpUtil.writeSp(instance, SP_NAME, new SpUtil.OnGetSpEditor() {
@Override
public void onGetSpEditor(SharedPreferences.Editor editor) {
editor.putString(KEY_INSTANCE, VALUE_INSTANCE_A);
}
});
}
return current;
}
public HttpDnsService getService() {
return current.getService();
}
}

View File

@@ -0,0 +1,21 @@
package com.newsdk.ams.httpdns.demo;
import com.newsdk.sdk.android.httpdns.RequestIpType;
public interface NetworkRequest {
/**
* 璁剧疆httpdns鐨勯厤缃?
*/
void updateHttpDnsConfig(boolean async, RequestIpType requestIpType);
/**
* get璇锋眰
*
* @param url
* @return
*/
String httpGet(String url) throws Exception;
}

View File

@@ -0,0 +1,127 @@
package com.newsdk.ams.httpdns.demo;
import android.os.Bundle;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.ams.httpdns.demo.base.BaseActivity;
import java.util.HashMap;
public class SDNSActivity extends BaseActivity {
private static final String[] HOSTS = new String[] {
"demo.cloudxdr.com",
"www.taobao.com"
};
private final HashMap<String, String> globalParams = new HashMap<>();
private String host = HOSTS[0];
private RequestIpType requestIpType = RequestIpType.v4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addEditTextEditTextButton("key", "value", "Add global param", new OnButtonClickMoreView() {
@Override
public void onBtnClick(View[] views) {
EditText keyView = (EditText) views[0];
EditText valueView = (EditText) views[1];
String key = keyView.getEditableText().toString();
String value = valueView.getEditableText().toString();
globalParams.put(key, value);
sendLog("Global param added: " + key + "=" + value);
}
});
addOneButton("Clear global params", new View.OnClickListener() {
@Override
public void onClick(View v) {
globalParams.clear();
sendLog("Global params cleared.");
}
});
addFourButton(
"IP type v4",
new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.v4;
sendLog("Request IP type: v4");
}
},
"IP type v6",
new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.v6;
sendLog("Request IP type: v6");
}
},
"IP type both",
new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.both;
sendLog("Request IP type: both");
}
},
"IP type auto",
new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.auto;
sendLog("Request IP type: auto");
}
}
);
addAutoCompleteTextViewButton(HOSTS, "Host", "Set host", new OnButtonClick() {
@Override
public void onBtnClick(View view) {
AutoCompleteTextView hostView = (AutoCompleteTextView) view;
host = hostView.getEditableText().toString();
sendLog("Host set to: " + host);
}
});
addEditTextEditTextButton("key", "value", "Resolve with param", new OnButtonClickMoreView() {
@Override
public void onBtnClick(View[] views) {
EditText keyView = (EditText) views[0];
EditText valueView = (EditText) views[1];
HashMap<String, String> params = new HashMap<>(globalParams);
params.put(keyView.getEditableText().toString(), valueView.getEditableText().toString());
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(
host, requestIpType, params, "sdns-demo");
sendLog("SDNS result: " + result);
}
});
addTwoButton("Resolve scale1", new View.OnClickListener() {
@Override
public void onClick(View v) {
HashMap<String, String> params = new HashMap<>(globalParams);
params.put("scale", "scale1");
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(
host, requestIpType, params, "sdns-demo");
sendLog("scale1 result: " + result);
}
}, "Resolve scale2", new View.OnClickListener() {
@Override
public void onClick(View v) {
HashMap<String, String> params = new HashMap<>(globalParams);
params.put("scale", "scale2");
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(
host, requestIpType, params, "sdns-demo");
sendLog("scale2 result: " + result);
}
});
}
}

View File

@@ -0,0 +1,403 @@
package com.newsdk.ams.httpdns.demo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class WebViewActivity extends Activity {
private WebView webView;
private static final String targetUrl = "http://www.apple.com";
private static final String TAG = MyApp.TAG + "WebView";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
initBar();
initHttpDnsWebView();
}
@Override
public boolean
onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
webView.goBack();//杩斿洖涓婁釜椤甸潰
return true;
}
return super.onKeyDown(keyCode, event);//閫€鍑篈ctivity
}
private void initBar() {
findViewById(R.id.bar_img).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
WebViewActivity.this.finish();
}
});
((TextView) findViewById(R.id.bar_text)).setText("HTTPDNS");
}
private void initHttpDnsWebView() {
webView = (WebView) this.findViewById(R.id.wv_container);
webView.setWebViewClient(new WebViewClient() {
@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
String url = request.getUrl().toString();
Log.e(TAG, "url:" + url);
// 鏃犳硶鎷︽埅body锛屾嫤鎴柟妗堝彧鑳芥甯稿鐞嗕笉甯ody鐨勮姹傦紱
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
&& method.equalsIgnoreCase("get")) {
try {
URLConnection connection = recursiveRequest(url, headerFields, null);
if (connection == null) {
Log.e(TAG, "connection null");
return super.shouldInterceptRequest(view, request);
}
// 娉?锛氬浜嶱OST璇锋眰鐨凚ody鏁版嵁锛學ebResourceRequest鎺ュ彛涓苟娌℃湁鎻愪緵锛岃繖閲屾棤娉曞鐞?
String contentType = connection.getContentType();
String mime = getMime(contentType);
String charset = getCharset(contentType);
HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
int statusCode = httpURLConnection.getResponseCode();
String response = httpURLConnection.getResponseMessage();
Map<String, List<String>> headers = httpURLConnection.getHeaderFields();
Set<String> headerKeySet = headers.keySet();
Log.e(TAG, "code:" + httpURLConnection.getResponseCode());
Log.e(TAG, "mime:" + mime + "; charset:" + charset);
// 鏃爉ime绫诲瀷鐨勮姹備笉鎷︽埅
if (TextUtils.isEmpty(mime)) {
Log.e(TAG, "no MIME");
return super.shouldInterceptRequest(view, request);
} else {
// 浜岃繘鍒惰祫婧愭棤闇€缂栫爜淇℃伅
if (!TextUtils.isEmpty(charset) || (isBinaryRes(mime))) {
WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream());
resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response);
Map<String, String> responseHeader = new HashMap<String, String>();
for (String key : headerKeySet) {
// HttpUrlConnection鍙兘鍖呭惈key涓簄ull鐨勬姤澶达紝鎸囧悜璇ttp璇锋眰鐘舵€佺爜
responseHeader.put(key, httpURLConnection.getHeaderField(key));
}
resourceResponse.setResponseHeaders(responseHeader);
return resourceResponse;
} else {
Log.e(TAG, "non binary resource for " + mime);
return super.shouldInterceptRequest(view, request);
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// API < 21 鍙兘鎷︽埅URL鍙傛暟
return super.shouldInterceptRequest(view, url);
}
});
webView.loadUrl(targetUrl);
}
/**
* 浠巆ontentType涓幏鍙朚IME绫诲瀷
*
* @param contentType
* @return
*/
private String getMime(String contentType) {
if (contentType == null) {
return null;
}
return contentType.split(";")[0];
}
/**
* 浠巆ontentType涓幏鍙栫紪鐮佷俊鎭?
*
* @param contentType
* @return
*/
private String getCharset(String contentType) {
if (contentType == null) {
return null;
}
String[] fields = contentType.split(";");
if (fields.length <= 1) {
return null;
}
String charset = fields[1];
if (!charset.contains("=")) {
return null;
}
charset = charset.substring(charset.indexOf("=") + 1);
return charset;
}
/**
* 鏄惁鏄簩杩涘埗璧勬簮锛屼簩杩涘埗璧勬簮鍙互涓嶉渶瑕佺紪鐮佷俊鎭?
*/
private boolean isBinaryRes(String mime) {
if (mime.startsWith("image")
|| mime.startsWith("audio")
|| mime.startsWith("video")) {
return true;
} else {
return false;
}
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
// 寮傛鎺ュ彛鑾峰彇IP
String ip = MyApp.getInstance().getService().getIpByHostAsync(url.getHost());
if (ip != null) {
// 閫氳繃HTTPDNS鑾峰彇IP鎴愬姛锛岃繘琛孶RL鏇挎崲鍜孒OST澶磋缃?
Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 璁剧疆HTTP璇锋眰澶碒ost鍩?
conn.setRequestProperty("Host", url.getHost());
} else {
return null;
}
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;
WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory(
(HttpsURLConnection)conn);
// sni鍦烘櫙锛屽垱寤篠SLScocket
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
// https鍦烘櫙锛岃瘉涔︽牎楠?
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
}
int code = conn.getResponseCode();// Network block
if (needRedirect(code)) {
// 鍘熸湁鎶ュご涓惈鏈塩ookie锛屾斁寮冩嫤鎴?
if (containCookie(headers)) {
return null;
}
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
//鏌愪簺鏃跺€欎細鐪佺暐host锛屽彧杩斿洖鍚庨潰鐨刾ath锛屾墍浠ラ渶瑕佽ˉ鍏rl
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
Log.e(TAG, "code: " + code + "; location: " + location + "; path " + path);
return recursiveRequest(location, headers, path);
} else {
// 鏃犳硶鑾峰彇location淇℃伅锛岃娴忚鍣ㄨ幏鍙?
return null;
}
} else {
// redirect finish.
Log.e(TAG, "redirect finish");
return conn;
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
private boolean needRedirect(int code) {
return code >= 300 && code < 400;
}
/**
* header涓槸鍚﹀惈鏈塩ookie
*/
private boolean containCookie(Map<String, String> headers) {
for (Map.Entry<String, String> headerField : headers.entrySet()) {
if (headerField.getKey().contains("Cookie")) {
return true;
}
}
return false;
}
static class WebviewTlsSniSocketFactory extends SSLSocketFactory {
private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
private final HttpsURLConnection conn;
public WebviewTlsSniSocketFactory(HttpsURLConnection conn) {
this.conn = conn;
}
@Override
public Socket createSocket() throws IOException {
return null;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return null;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return null;
}
// TLS layer
@Override
public String[] getDefaultCipherSuites() {
return new String[0];
}
@Override
public String[] getSupportedCipherSuites() {
return new String[0];
}
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
String peerHost = this.conn.getRequestProperty("Host");
if (peerHost == null)
peerHost = host;
Log.i(TAG, "customized createSocket. host: " + peerHost);
InetAddress address = plainSocket.getInetAddress();
if (autoClose) {
// we don't need the plainSocket
plainSocket.close();
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// enable TLSv1.1/1.2 if available
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.i(TAG, "Setting SNI hostname");
sslSocketFactory.setHostname(ssl, peerHost);
} else {
Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
Log.w(TAG, "SNI not useable", e);
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!hostnameVerifier.verify(peerHost, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
" using " + session.getCipherSuite());
return ssl;
}
}
}

View File

@@ -0,0 +1,264 @@
package com.newsdk.ams.httpdns.demo.base;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import com.newsdk.ams.httpdns.demo.MyApp;
import com.newsdk.ams.httpdns.demo.R;
public class BaseActivity extends Activity {
public static final int MSG_WHAT_LOG = 10000;
private ScrollView logScrollView;
private TextView logView;
private LinearLayout llContainer;
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base);
handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_WHAT_LOG:
logView.setText(logView.getText() + "\n" + (String) msg.obj);
handler.post(new Runnable() {
@Override
public void run() {
logScrollView.fullScroll(View.FOCUS_DOWN);
}
});
break;
}
}
};
logScrollView = findViewById(R.id.logScrollView);
logView = findViewById(R.id.tvConsoleText);
llContainer = findViewById(R.id.llContainer);
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
handler = null;
}
/**
* 鍙戦€佹棩蹇楀埌鐣岄潰
*
* @param log
*/
protected void sendLog(String log) {
Log.d(MyApp.TAG, log);
if (handler != null) {
Message msg = handler.obtainMessage(MSG_WHAT_LOG, log);
handler.sendMessage(msg);
}
}
protected void cleanLog() {
logView.setText("");
}
protected void addView(int layoutId, OnViewCreated created) {
FrameLayout container = new FrameLayout(this);
View.inflate(this, layoutId, container);
llContainer.addView(container, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
created.onViewCreated(container);
}
protected void addOneButton(
final String labelOne, final View.OnClickListener clickListenerOne
) {
addView(R.layout.item_one_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
btnOne.setOnClickListener(clickListenerOne);
}
});
}
protected void addTwoButton(
final String labelOne, final View.OnClickListener clickListenerOne,
final String labelTwo, final View.OnClickListener clickListenerTwo
) {
addView(R.layout.item_two_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
btnOne.setOnClickListener(clickListenerOne);
Button btnTwo = view.findViewById(R.id.btnTwo);
btnTwo.setText(labelTwo);
btnTwo.setOnClickListener(clickListenerTwo);
}
});
}
protected void addThreeButton(
final String labelOne, final View.OnClickListener clickListenerOne,
final String labelTwo, final View.OnClickListener clickListenerTwo,
final String labelThree, final View.OnClickListener clickListenerThree
) {
addView(R.layout.item_three_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
btnOne.setOnClickListener(clickListenerOne);
Button btnTwo = view.findViewById(R.id.btnTwo);
btnTwo.setText(labelTwo);
btnTwo.setOnClickListener(clickListenerTwo);
Button btnThree = view.findViewById(R.id.btnThree);
btnThree.setText(labelThree);
btnThree.setOnClickListener(clickListenerThree);
}
});
}
protected void addFourButton(
final String labelOne, final View.OnClickListener clickListenerOne,
final String labelTwo, final View.OnClickListener clickListenerTwo,
final String labelThree, final View.OnClickListener clickListenerThree,
final String labelFour, final View.OnClickListener clickListenerFour
) {
addView(R.layout.item_four_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
btnOne.setOnClickListener(clickListenerOne);
Button btnTwo = view.findViewById(R.id.btnTwo);
btnTwo.setText(labelTwo);
btnTwo.setOnClickListener(clickListenerTwo);
Button btnThree = view.findViewById(R.id.btnThree);
btnThree.setText(labelThree);
btnThree.setOnClickListener(clickListenerThree);
Button btnFour = view.findViewById(R.id.btnFour);
btnFour.setText(labelFour);
btnFour.setOnClickListener(clickListenerFour);
}
});
}
protected void addEditTextButton(
final String hint,
final String labelOne, final OnButtonClick clickListenerOne
) {
addView(R.layout.item_edit_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
final EditText editText = view.findViewById(R.id.etOne);
editText.setHint(hint);
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickListenerOne.onBtnClick(editText);
}
});
}
});
}
protected void addEditTextEditTextButton(
final String hintOne, final String hintTwo,
final String labelOne, final OnButtonClickMoreView clickListenerOne
) {
addView(R.layout.item_edit_edit_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
final EditText editTextOne = view.findViewById(R.id.etOne);
editTextOne.setHint(hintOne);
final EditText editTextTwo = view.findViewById(R.id.etTwo);
editTextTwo.setHint(hintTwo);
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickListenerOne.onBtnClick(new View[]{editTextOne, editTextTwo});
}
});
}
});
}
protected void addAutoCompleteTextViewButton(
final String[] strings, final String hint, final String labelOne, final OnButtonClick clickListenerOne
) {
addView(R.layout.item_autocomplete_button, new OnViewCreated() {
@Override
public void onViewCreated(View view) {
Button btnOne = view.findViewById(R.id.btnOne);
btnOne.setText(labelOne);
final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getApplicationContext(),
android.R.layout.simple_dropdown_item_1line, strings);
actvOne.setAdapter(adapter);
actvOne.setHint(hint);
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickListenerOne.onBtnClick(actvOne);
}
});
}
});
}
public interface OnViewCreated {
void onViewCreated(View view);
}
public interface OnButtonClick {
void onBtnClick(View view);
}
public interface OnButtonClickMoreView {
void onBtnClick(View[] views);
}
}

View File

@@ -0,0 +1,258 @@
package com.newsdk.ams.httpdns.demo.http;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.util.Log;
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
import com.newsdk.sdk.android.httpdns.NetType;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.sdk.android.httpdns.SyncService;
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector;
import com.newsdk.ams.httpdns.demo.MyApp;
import com.newsdk.ams.httpdns.demo.NetworkRequest;
import com.newsdk.ams.httpdns.demo.utils.Util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* 浣跨敤HttpUrlConnection 瀹炵幇璇锋眰
*/
public class HttpUrlConnectionRequest implements NetworkRequest {
public static final String TAG = MyApp.TAG + "HttpUrl";
private final Context context;
private boolean async;
private RequestIpType type;
public HttpUrlConnectionRequest(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void updateHttpDnsConfig(boolean async, RequestIpType requestIpType) {
this.async = async;
this.type = requestIpType;
}
@Override
public String httpGet(String url) throws Exception {
Log.d(TAG, "浣跨敤httpUrlConnection 璇锋眰" + url + " 寮傛鎺ュ彛 " + async + " ip绫诲瀷 " + type.name());
HttpURLConnection conn = getConnection(url);
InputStream in = null;
BufferedReader streamReader = null;
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
in = conn.getErrorStream();
String errStr = null;
if (in != null) {
streamReader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
errStr = readStringFrom(streamReader).toString();
}
Log.d(TAG, "璇锋眰澶辫触 " + conn.getResponseCode() + " err " + errStr);
throw new Exception("Status Code : " + conn.getResponseCode() + " Msg : " + errStr);
} else {
in = conn.getInputStream();
streamReader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String responseStr = readStringFrom(streamReader).toString();
Log.d(TAG, "璇锋眰鎴愬姛 " + responseStr);
return responseStr;
}
}
private HttpURLConnection getConnection(String url) throws IOException {
final String host = new URL(url).getHost();
HttpURLConnection conn = null;
HTTPDNSResult result;
/* 鍒囨崲涓烘柊鐗堟爣鍑哸pi */
if (async) {
result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(host, type);
} else {
result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(host, type);
}
Log.d(TAG, "httpdns 瑙f瀽 " + host + " 缁撴灉涓?" + result + " ttl is " + Util.getTtl(result));
// 杩欓噷闇€瑕佹牴鎹疄闄呮儏鍐甸€夋嫨浣跨敤ipv6鍦板潃 杩樻槸 ipv4鍦板潃锛?涓嬮潰绀轰緥鐨勪唬鐮佷紭鍏堜娇鐢ㄤ簡ipv6鍦板潃
if (result.getIpv6s() != null && result.getIpv6s().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v4) {
String newUrl = url.replace(host, "[" + result.getIpv6s()[0] + "]");
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setRequestProperty("Host", host);
Log.d(TAG, "浣跨敤ipv6鍦板潃 " + newUrl);
} else if (result.getIps() != null && result.getIps().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v6) {
String newUrl = url.replace(host, result.getIps()[0]);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setRequestProperty("Host", host);
Log.d(TAG, "浣跨敤ipv4鍦板潃 " + newUrl);
}
if (conn == null) {
Log.d(TAG, "httpdns 鏈繑鍥炶В鏋愮粨鏋滐紝璧發ocaldns");
conn = (HttpURLConnection) new URL(url).openConnection();
}
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;
WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory(
(HttpsURLConnection)conn);
// sni鍦烘櫙锛屽垱寤篠SLSocket
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
// https鍦烘櫙锛岃瘉涔︽牎楠?
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
}
int code = conn.getResponseCode();// Network block
if (needRedirect(code)) {
//涓存椂閲嶅畾鍚戝拰姘镐箙閲嶅畾鍚憀ocation鐨勫ぇ灏忓啓鏈夊尯鍒?
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
//鏌愪簺鏃跺€欎細鐪佺暐host锛屽彧杩斿洖鍚庨潰鐨刾ath锛屾墍浠ラ渶瑕佽ˉ鍏rl
URL originalUrl = new URL(url);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
return getConnection(location);
}
return conn;
}
private boolean needRedirect(int code) {
return code >= 300 && code < 400;
}
static class WebviewTlsSniSocketFactory extends SSLSocketFactory {
private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
private HttpsURLConnection conn;
public WebviewTlsSniSocketFactory(HttpsURLConnection conn) {
this.conn = conn;
}
@Override
public Socket createSocket() throws IOException {
return null;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return null;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return null;
}
// TLS layer
@Override
public String[] getDefaultCipherSuites() {
return new String[0];
}
@Override
public String[] getSupportedCipherSuites() {
return new String[0];
}
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
String peerHost = this.conn.getRequestProperty("Host");
if (peerHost == null)
peerHost = host;
Log.i(TAG, "customized createSocket. host: " + peerHost);
InetAddress address = plainSocket.getInetAddress();
if (autoClose) {
// we don't need the plainSocket
plainSocket.close();
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// enable TLSv1.1/1.2 if available
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.i(TAG, "Setting SNI hostname");
sslSocketFactory.setHostname(ssl, peerHost);
} else {
Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
Log.w(TAG, "SNI not useable", e);
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!hostnameVerifier.verify(peerHost, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
" using " + session.getCipherSuite());
return ssl;
}
}
/**
* stream to string
*/
public static StringBuilder readStringFrom(BufferedReader streamReader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = streamReader.readLine()) != null) {
sb.append(line);
}
return sb;
}
}

View File

@@ -0,0 +1,98 @@
package com.newsdk.ams.httpdns.demo.okhttp;
import android.content.Context;
import android.util.Log;
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
import com.newsdk.sdk.android.httpdns.NetType;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.sdk.android.httpdns.SyncService;
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector;
import com.newsdk.ams.httpdns.demo.MyApp;
import com.newsdk.ams.httpdns.demo.NetworkRequest;
import com.newsdk.ams.httpdns.demo.utils.Util;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.ConnectionPool;
import okhttp3.Dns;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* okhttp瀹炵幇缃戠粶璇锋眰
*/
public class OkHttpRequest implements NetworkRequest {
public static final String TAG = MyApp.TAG + "Okhttp";
private final OkHttpClient client;
private boolean async;
private RequestIpType type;
public OkHttpRequest(final Context context) {
client = new OkHttpClient.Builder()
// 杩欓噷閰嶇疆杩炴帴姹狅紝鏄负浜嗘柟渚挎祴璇昲ttpdns鑳藉姏锛屾寮忎唬鐮佽涓嶈閰嶇疆
.connectionPool(new ConnectionPool(0, 10 * 1000, TimeUnit.MICROSECONDS))
.dns(new Dns() {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
HTTPDNSResult result;
/* 鍒囨崲涓烘柊鐗堟爣鍑哸pi */
if (async) {
result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(hostname, type);
} else {
result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(hostname, type);
}
Log.d(TAG, "httpdns 瑙f瀽 " + hostname + " 缁撴灉涓?" + result + " ttl is " + Util.getTtl(result));
List<InetAddress> inetAddresses = new ArrayList<>();
// 杩欓噷闇€瑕佹牴鎹疄闄呮儏鍐甸€夋嫨浣跨敤ipv6鍦板潃 杩樻槸 ipv4鍦板潃锛?涓嬮潰绀轰緥鐨勪唬鐮佷紭鍏堜娇鐢ㄤ簡ipv6鍦板潃
Log.d(TAG, "netType is: " + HttpDnsNetworkDetector.getInstance().getNetType(context));
if (result.getIpv6s() != null && result.getIpv6s().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v4) {
for (int i = 0; i < result.getIpv6s().length; i++) {
inetAddresses.addAll(Arrays.asList(InetAddress.getAllByName(result.getIpv6s()[i])));
}
Log.d(TAG, "浣跨敤ipv6鍦板潃" + inetAddresses);
} else if (result.getIps() != null && result.getIps().length > 0 && HttpDnsNetworkDetector.getInstance().getNetType(context) != NetType.v6) {
for (int i = 0; i < result.getIps().length; i++) {
inetAddresses.addAll(Arrays.asList(InetAddress.getAllByName(result.getIps()[i])));
}
Log.d(TAG, "浣跨敤ipv4鍦板潃" + inetAddresses);
}
if (inetAddresses.size() == 0) {
Log.d(TAG, "httpdns 鏈繑鍥濱P锛岃蛋localdns");
return Dns.SYSTEM.lookup(hostname);
}
return inetAddresses;
}
})
.build();
}
@Override
public void updateHttpDnsConfig(boolean async, RequestIpType requestIpType) {
this.async = async;
this.type = requestIpType;
}
@Override
public String httpGet(String url) throws Exception {
Log.d(TAG, "浣跨敤okhttp 璇锋眰" + url + " 寮傛鎺ュ彛 " + async + " ip绫诲瀷 " + type.name());
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
int code = response.code();
String body = response.body().string();
Log.d(TAG, "浣跨敤okhttp 璇锋眰缁撴灉 code " + code + " body " + body);
if (code != HttpURLConnection.HTTP_OK) {
throw new Exception("璇锋眰澶辫触 code " + code + " body " + body);
}
return body;
}
}

View File

@@ -0,0 +1,28 @@
package com.newsdk.ams.httpdns.demo.utils;
import android.content.Context;
import android.content.SharedPreferences;
public class SpUtil {
public static void readSp(Context context, String name, OnGetSp onGetSp) {
SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_PRIVATE);
onGetSp.onGetSp(sp);
}
public static void writeSp(Context context, String name, OnGetSpEditor onGetSpEditor) {
SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
onGetSpEditor.onGetSpEditor(editor);
editor.commit();
}
public interface OnGetSp {
void onGetSp(SharedPreferences sp);
}
public interface OnGetSpEditor {
void onGetSpEditor(SharedPreferences.Editor editor);
}
}

View File

@@ -0,0 +1,95 @@
package com.newsdk.ams.httpdns.demo.utils;
import android.util.Log;
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
import com.newsdk.sdk.android.httpdns.RequestIpType;
import com.newsdk.sdk.android.httpdns.SyncService;
import com.newsdk.ams.httpdns.demo.MyApp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadUtil {
public static void multiThreadTest(
final String[] validHosts,
final int hostCount,
final int threadCount,
final int executeTime,
final boolean async,
final RequestIpType type
) {
new Thread(() -> runMultiThreadTest(validHosts, hostCount, threadCount, executeTime, async, type)).start();
}
private static void runMultiThreadTest(
String[] validHosts,
int hostCount,
int threadCount,
int executeTime,
boolean async,
RequestIpType type
) {
int validCount = Math.min(validHosts.length, hostCount);
Log.d(MyApp.TAG, "Start multiThreadTest, threads=" + threadCount
+ ", executeTimeMs=" + executeTime
+ ", hostCount=" + hostCount
+ ", validHostCount=" + validCount
+ ", async=" + async
+ ", ipType=" + type.name());
ArrayList<String> hosts = new ArrayList<>(hostCount);
for (int i = 0; i < hostCount - validCount; i++) {
hosts.add("test" + i + ".cloudxdr.com");
}
hosts.addAll(Arrays.asList(validHosts).subList(0, validCount));
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
CountDownLatch done = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
pool.execute(() -> {
Random random = new Random(Thread.currentThread().getId());
long begin = System.currentTimeMillis();
int requestCount = 0;
int successCount = 0;
while (System.currentTimeMillis() - begin < executeTime) {
String host = hosts.get(random.nextInt(hosts.size()));
try {
HTTPDNSResult result;
if (async) {
result = MyApp.getInstance().getService().getIpsByHostAsync(host, type);
} else {
result = ((SyncService) MyApp.getInstance().getService()).getByHost(host, type);
}
if (result != null && result.getIps() != null && result.getIps().length > 0) {
successCount++;
}
} catch (Throwable t) {
Log.w(MyApp.TAG, "multiThreadTest request failed: " + t.getMessage());
}
requestCount++;
}
Log.w(MyApp.TAG, "multiThreadTest thread=" + Thread.currentThread().getId()
+ ", requestCount=" + requestCount
+ ", successCount=" + successCount);
done.countDown();
});
}
try {
done.await();
} catch (InterruptedException ignored) {
} finally {
pool.shutdownNow();
}
}
}

View File

@@ -0,0 +1,25 @@
package com.newsdk.ams.httpdns.demo.utils;
import com.newsdk.sdk.android.httpdns.HTTPDNSResult;
import java.lang.reflect.Field;
public class Util {
/**
* 鑾峰彇ttl锛?
* 姝ゆ柟娉曟槸鐢ㄤ簬娴嬭瘯鑷畾涔塼tl鏄惁鐢熸晥
*/
public static int getTtl(HTTPDNSResult result) {
try {
Field ttlField = HTTPDNSResult.class.getDeclaredField("ttl");
ttlField.setAccessible(true);
return ttlField.getInt(result);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return -1;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,49 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/new_bg" />
<ScrollView
android:id="@+id/logScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvConsoleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:textColor="@android:color/white"
android:textSize="20sp" />
</ScrollView>
</FrameLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#EAEAEA">
<LinearLayout
android:id="@+id/llContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="#f2f2f2">
<ImageView
android:id="@+id/bar_img"
android:layout_width="50dp"
android:layout_height="match_parent"
android:src="@mipmap/back"
android:padding="16dp" />
<TextView
android:id="@+id/bar_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text=""
android:textSize="18sp" />
<TextView
android:id="@+id/bar_more"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="鏇村"
android:textSize="16sp"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:visibility="gone" />
</LinearLayout>
<WebView
android:id="@+id/wv_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<AutoCompleteTextView
android:id="@+id/actvOne"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<AutoCompleteTextView
android:id="@+id/actvOne"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/etOne"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/etOne" />
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/etOne" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/etTwo" />
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnTwo"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnThree"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnFour"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnTwo"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnThree"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnTwo"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,31 @@
<resources>
<string name="app_name">閵嗘劙妯嬮柌灞肩隘HttpDns閵嗘厪emo缁嬪绨</string>
<string name="action_settings">Settings</string>
<string name="normal_parse">閺咁噣鈧俺袙閺</string>
<string name="request_taobao">鐟欙絾鐎藉ǎ妯虹杺閸╃喎鎮</string>
<string name="request_apple">鐟欙絾鐎絘pple閸╃喎鎮</string>
<string name="request_douban">鐟欙絾鐎界挒鍡欐憵閸╃喎鎮</string>
<string name="https_parse">HTTPS瀵偓閸</string>
<string name="timeout">鐠佸墽鐤嗙搾鍛</string>
<string name="set_expired">閸忎浇顔忔潻鍥ㄦ埂閸╃喎鎮</string>
<string name="set_cache">瀵偓閸氼垱瀵旀稊鍛缂傛挸鐡</string>
<string name="set_degration_filter">闂勫秶楠囩粵鏍殣</string>
<string name="set_pre_resolve">妫板嫯袙閺</string>
<string name="set_region">region</string>
<string name="sync_request">閸氬本顒炵憴锝嗙€</string>
<string name="multi_sync_request">閸氬本顒炵憴锝嗙€介獮璺哄絺</string>
<string name="multi_request">楠炶泛褰傜憴锝嗙€</string>
<string name="main_about_us">閸忓厖绨幋鎴滄粦</string>
<string name="main_helper">鐢喖濮稉顓炵妇</string>
<string name="main_clear_text">濞撳懘娅庤ぐ鎾冲濞戝牊浼</string>
<string name="layout_aboutus_arr">All Rights Reserved.</string>
<string name="layout_aboutus_company">闂冨潡鍣锋禍?鏉烆垯娆?閺堝妾洪崗顒€寰冮悧鍫熸綀閹碘偓閺</string>
<string name="layout_aboutus_copyright">Copyright 婕?2009 - 2016 New.com</string>
<string name="layout_aboutus_app_version">1.1.3</string>
<string name="layout_helpus_content">Q : 娴犫偓娑斿牊妲搁悽銊﹀煕娴捇鐛橠emo閿涚剠nA : 閻劍鍩涙担鎾荤崣Demo鐏忚鲸妲搁梼鍧楀櫡娴滄垵閽╅崣棰佽礋閹劏鍤滈崝銊ュ灡瀵よ櫣娈戦妴浣烘暏閺夈儰缍嬫瀛抰tpDns閺堝秴濮熼崪灞藉冀妫牆缂撶拋顔炬暏閻ㄥ嫪绔存稉顏勭毈Demo閿涘矁顔€閹劋缍嬫灞肩┒閹规灚鈧線鐝弫鍫㈡畱HttpDns閺堝秴濮熼妴淇搉\nQ : 婵″倷缍嶉懕鏃傞兇閹存垳婊戦敍鐒卬A : App Demo閻╃鍙ч梻顕€顣介敍宀冾嚞閹兼粎鍌ㄩ柦澶愭嫟缂囥倕褰块敍?1777313</string>
</resources>

View File

@@ -0,0 +1,14 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme"></style>
<style name="button_allgrade_content">
<item name="android:layout_margin">1dip</item>
<item name="android:background">#ffffff</item>
<item name="android:textSize">18sp</item>
<item name="android:clickable">true</item>
<item name="android:textColor">#413d41</item>
</style>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<New-anchors>
<certificates src="system" />
</New-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,128 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
gradle.ext {
httpVersion = '2.3.4'
}
android {
namespace 'com.newsdk.ams.httpdns.demo'
compileSdk 34
defaultConfig {
applicationId "com.newsdk.ams.httpdns.demo"
minSdkVersion 26
targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
buildConfigField "String", "HTTPDNS_VERSION", "\"${gradle.httpVersion}\""
buildConfigField "String", "ACCOUNT_ID", "\"replace-with-your-accountId\""
buildConfigField "String", "SECRET_KEY", "\"replace-with-your-secret\""
buildConfigField "String", "AES_SECRET_KEY", "\"replace-with-your-aes-secret\""
buildConfigField "String", "SERVICE_URL", "\"\""
}
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
forTest {
// Keep this flavor to avoid BuildVariants errors when switching httpdns-sdk to end2end tests.
initWith release
debuggable true
}
}
variantFilter { variant ->
def names = variant.flavors*.name
def type = variant.buildType.name
// To check for a certain build type, use variant.buildType.name == "<buildType>"
if ((names.contains("normal") && type.contains("forTest"))
|| (names.contains("intl") && type.contains("forTest"))
|| (names.contains("end2end") && type.contains("release"))
|| (names.contains("end2end") && type.contains("debug"))
) {
// Gradle ignores any variants that satisfy the conditions above.
setIgnore(true)
}
}
testOptions {
unitTests {
all {
jvmArgs '-noverify'
systemProperty 'robolectric.logging.enable', true
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding = true
dataBinding = true
}
flavorDimensions += "version"
productFlavors {
normal {
}
intl {
}
end2end {
// Keep this flavor to avoid BuildVariants errors when switching httpdns-sdk to end2end tests.
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
implementation project(path: ':httpdns-sdk')
implementation('com.newsdk:fastjson:1.1.73.android@jar')
// implementation('com.emas.hybrid:emas-hybrid-android:1.1.0.4-public-SNAPSHOT') {
// exclude group: 'com.android.support', module: 'appcompat-v7'
// exclude group: 'com.taobao.android', module: 'thin_so_release'
// }
implementation 'com.newsdk.ams:new-android-tool:1.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

View File

@@ -0,0 +1,19 @@
## Project-wide Gradle settings.
#
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Sat Jun 11 21:37:51 CST 2016
org.gradle.jvmargs=-Xmx1536m
android.enableD8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,22 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class com.newsdk.sdk.android.httpdns.**{*;}

View File

@@ -0,0 +1,25 @@
package com.newsdk.ams.emas.demo
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.newsdk.ams.emas.demo", appContext.packageName)
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" />
<application
android:name="com.newsdk.ams.emas.demo.HttpDnsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:extractNativeLibs="true"
android:theme="@style/Theme.NewHttpDnsDemo"
android:usesCleartextTraffic="true">
<activity
android:name="com.newsdk.ams.emas.demo.ui.practice.HttpDnsWebviewGetActivity"
android:exported="false"
android:theme="@style/Theme.NewHttpDnsDemo.NoActionBar">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name="com.newsdk.ams.emas.demo.ui.info.list.ListActivity"
android:exported="false"
android:theme="@style/Theme.NewHttpDnsDemo.NoActionBar">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name="com.newsdk.ams.emas.demo.MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity android:name="com.newsdk.ams.emas.demo.ui.info.SdnsGlobalSettingActivity"
android:exported="false"
android:theme="@style/Theme.NewHttpDnsDemo.NoActionBar" >
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,93 @@
package com.newsdk.ams.emas.demo
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
object BatchResolveCacheHolder {
var batchResolveV4List: MutableList<String> = mutableListOf()
var batchResolveV6List: MutableList<String> = mutableListOf()
var batchResolveBothList: MutableList<String> = mutableListOf()
var batchResolveAutoList: MutableList<String> = mutableListOf()
fun convertBatchResolveCacheData(cacheData: String?) {
if (cacheData == null) {
batchResolveBothList.add("www.baidu.com")
batchResolveBothList.add("m.baidu.com")
batchResolveBothList.add("demo.cloudxdr.com")
batchResolveBothList.add("www.taobao.com")
batchResolveBothList.add("www.163.com")
batchResolveBothList.add("www.sohu.com")
batchResolveBothList.add("www.sina.com.cn")
batchResolveBothList.add("www.douyin.com")
batchResolveBothList.add("www.qq.com")
batchResolveBothList.add("www.chinaamc.com")
batchResolveBothList.add("m.chinaamc.com")
return
}
try {
val jsonObject = JSONObject(cacheData)
val v4Array = jsonObject.optJSONArray("v4")
val v6Array = jsonObject.optJSONArray("v6")
val bothArray = jsonObject.optJSONArray("both")
val autoArray = jsonObject.optJSONArray("auto")
if (v4Array != null) {
var length = v4Array.length()
--length
while (length >= 0) {
batchResolveV4List.add(0, v4Array.getString(length))
--length
}
}
if (v6Array != null) {
var length = v6Array.length()
--length
while (length >= 0) {
batchResolveV6List.add(0, v6Array.getString(length))
--length
}
}
if (bothArray != null) {
var length = bothArray.length()
--length
while (length >= 0) {
batchResolveBothList.add(0, bothArray.getString(length))
--length
}
}
if (autoArray != null) {
var length = autoArray.length()
--length
while (length >= 0) {
batchResolveAutoList.add(0, autoArray.getString(length))
--length
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun convertBatchResolveString(): String {
val jsonObject = JSONObject()
val v4Array = JSONArray()
val v6Array = JSONArray()
val bothArray = JSONArray()
val autoArray = JSONArray()
for (host in batchResolveV4List) {
v4Array.put(host)
}
jsonObject.put("v4", v4Array)
for (host in batchResolveV6List) {
v6Array.put(host)
}
jsonObject.put("v6", v6Array)
for (host in batchResolveBothList) {
bothArray.put(host)
}
jsonObject.put("both", bothArray)
for (host in batchResolveAutoList) {
autoArray.put(host)
}
jsonObject.put("auto", autoArray)
return jsonObject.toString()
}
}

View File

@@ -0,0 +1,17 @@
package com.newsdk.ams.emas.demo
import android.app.Application
/**
* @author allen.wy
* @date 2023/5/24
*/
class HttpDnsApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View File

@@ -0,0 +1,38 @@
package com.newsdk.ams.emas.demo
import android.content.Context
import android.text.TextUtils
import com.newsdk.ams.emas.demo.constant.KEY_ENABLE_AUTH_MODE
import com.newsdk.ams.emas.demo.constant.KEY_SECRET_KEY_SET_BY_CONFIG
import com.newsdk.sdk.android.httpdns.HttpDns
import com.newsdk.sdk.android.httpdns.HttpDnsService
import com.newsdk.ams.httpdns.demo.BuildConfig
/**
* @author allen.wy
* @date 2023/6/6
*/
object HttpDnsServiceHolder {
fun getHttpDnsService(context: Context) : HttpDnsService? {
val dnsService = if (!TextUtils.isEmpty(BuildConfig.ACCOUNT_ID)) {
val secretKeySetByConfig = getAccountPreference(context).getBoolean(KEY_SECRET_KEY_SET_BY_CONFIG, true)
if (secretKeySetByConfig) {
HttpDns.getService(BuildConfig.ACCOUNT_ID)
} else {
val authMode = getAccountPreference(context).getBoolean(KEY_ENABLE_AUTH_MODE, true)
if (authMode && !TextUtils.isEmpty(BuildConfig.SECRET_KEY)) HttpDns.getService(
context,
BuildConfig.ACCOUNT_ID, BuildConfig.SECRET_KEY
) else HttpDns.getService(
context,
BuildConfig.ACCOUNT_ID
)
}
} else null
return dnsService
}
}

View File

@@ -0,0 +1,52 @@
package com.newsdk.ams.emas.demo
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.ActivityMainBinding
import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
object HttpDns {
var inited = false
}
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navView: BottomNavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_activity_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_basic,
R.id.navigation_resolve,
R.id.navigation_best_practice,
R.id.navigation_information
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setOnItemSelectedListener {
if (HttpDns.inited) {
navController.navigate(it.itemId)
true
} else {
Toast.makeText(this, R.string.init_tip, Toast.LENGTH_SHORT).show()
false
}
}
}
}

View File

@@ -0,0 +1,93 @@
package com.newsdk.ams.emas.demo
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
object PreResolveCacheHolder {
var preResolveV4List: MutableList<String> = mutableListOf()
var preResolveV6List: MutableList<String> = mutableListOf()
var preResolveBothList: MutableList<String> = mutableListOf()
var preResolveAutoList: MutableList<String> = mutableListOf()
fun convertPreResolveCacheData(cacheData: String?) {
if (cacheData == null) {
return
}
try {
val jsonObject = JSONObject(cacheData)
val v4Array = jsonObject.optJSONArray("v4")
val v6Array = jsonObject.optJSONArray("v6")
val bothArray = jsonObject.optJSONArray("both")
val autoArray = jsonObject.optJSONArray("auto")
if (v4Array != null) {
var length = v4Array.length()
--length
while (length >= 0) {
preResolveV4List.add(0, v4Array.getString(length))
--length
}
}
if (v6Array != null) {
var length = v6Array.length()
--length
while (length >= 0) {
preResolveV6List.add(0, v6Array.getString(length))
--length
}
}
if (bothArray != null) {
var length = bothArray.length()
--length
while (length >= 0) {
preResolveBothList.add(0, bothArray.getString(length))
--length
}
}
if (autoArray != null) {
var length = autoArray.length()
--length
while (length >= 0) {
preResolveAutoList.add(0, autoArray.getString(length))
--length
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun convertPreResolveString(): String {
val jsonObject = JSONObject()
val v4Array = JSONArray()
val v6Array = JSONArray()
val bothArray = JSONArray()
val autoArray = JSONArray()
for (host in preResolveV4List) {
v4Array.put(host)
}
jsonObject.put("v4", v4Array)
for (host in preResolveV6List) {
v6Array.put(host)
}
jsonObject.put("v6", v6Array)
for (host in preResolveBothList) {
bothArray.put(host)
}
jsonObject.put("both", bothArray)
for (host in preResolveAutoList) {
autoArray.put(host)
}
jsonObject.put("auto", autoArray)
return jsonObject.toString()
}
}

View File

@@ -0,0 +1,48 @@
package com.newsdk.ams.emas.demo
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* @author allen.wy
* @date 2023/5/18
*/
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w("SingleLiveData", "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer<T> { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(@Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}

View File

@@ -0,0 +1,51 @@
package com.newsdk.ams.emas.demo
import com.newsdk.sdk.android.httpdns.CacheTtlChanger
import org.json.JSONException
import org.json.JSONObject
/**
* @author allen.wy
* @date 2023/6/6
*/
object TtlCacheHolder {
var ttlCache = mutableMapOf<String, Int>()
val cacheTtlChanger = CacheTtlChanger { host, _, ttl ->
if (ttlCache[host] != null) ttlCache[host]!! else ttl
}
fun convertTtlCacheData(cacheData: String?) {
if (cacheData == null) {
return
}
try {
val jsonObject = JSONObject(cacheData)
val it = jsonObject.keys()
while (it.hasNext()) {
val host = it.next()
ttlCache[host] = jsonObject.getInt(host)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun MutableMap<String, Int>?.toJsonString(): String? {
if (this == null) {
return null
}
val jsonObject = JSONObject()
for (host in this.keys) {
try {
jsonObject.put(host, this[host])
} catch (e: JSONException) {
e.printStackTrace()
}
}
return jsonObject.toString()
}
}

View File

@@ -0,0 +1,141 @@
package com.newsdk.ams.emas.demo
import android.content.Context
import android.content.SharedPreferences
import com.newsdk.sdk.android.httpdns.ranking.IPRankingBean
import com.newsdk.ams.httpdns.demo.BuildConfig
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
/**
* @author allen.wy
* @date 2023/6/5
*/
fun String?.toHostList(): MutableList<String>? {
if (this == null) {
return null
}
try {
val array = JSONArray(this)
val list = mutableListOf<String>()
for (i in 0 until array.length()) {
list.add(array.getString(i))
}
return list
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}
fun String?.toTagList(): MutableList<String>? {
if (this == null) {
return null
}
try {
val array = JSONArray(this)
val list = mutableListOf<String>()
for (i in 0 until array.length()) {
list.add(array.getString(i))
}
return list
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}
fun String?.toIPRankingList(): MutableList<IPRankingBean>? {
if (this == null) {
return null
}
try {
val jsonObject = JSONObject(this)
val list = mutableListOf<IPRankingBean>()
val it = jsonObject.keys()
while (it.hasNext()) {
val host = it.next()
list.add(
IPRankingBean(
host,
jsonObject.getInt(host)
)
)
}
return list
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}
fun String?.toTtlCacheMap(): MutableMap<String, Int>? {
if (this == null) {
return null
}
try {
val jsonObject = JSONObject(this)
val map = mutableMapOf<String, Int>()
val it = jsonObject.keys()
while (it.hasNext()) {
val host = it.next()
val ttl = jsonObject.getInt(host)
map[host] = ttl
}
return map
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}
fun String?.toBlackList(): MutableList<String>? {
if (this == null) {
return null
}
try {
val array = JSONArray(this)
val list = mutableListOf<String>()
for (i in 0 until array.length()) {
list.add(array.getString(i))
}
return list
} catch (e: JSONException) {
e.printStackTrace()
}
return null
}
fun getAccountPreference(context: Context): SharedPreferences {
return context.getSharedPreferences(
"New_httpdns_${BuildConfig.ACCOUNT_ID}",
Context.MODE_PRIVATE
)
}
fun convertPreResolveList(preResolveHostList: List<String>?): String? {
if (preResolveHostList == null) {
return null
}
val array = JSONArray()
for (host in preResolveHostList) {
array.put(host)
}
return array.toString()
}
@Throws(IOException::class)
fun readStringFrom(streamReader: BufferedReader): StringBuilder {
val sb = StringBuilder()
var line: String?
while (streamReader.readLine().also { line = it } != null) {
sb.append(line)
}
return sb
}

View File

@@ -0,0 +1,55 @@
package com.newsdk.ams.emas.demo.constant
/**
* @author allen.wy
* @date 2023/5/24
*/
const val KEY_ENABLE_AUTH_MODE = "enable_auth_mode"
const val KEY_SECRET_KEY_SET_BY_CONFIG = "secret_key_set_by_config"
const val KEY_ENABLE_ENCRYPT_MODE = "enable_encrypt_mode"
const val KEY_ENABLE_EXPIRED_IP = "enable_expired_ip"
const val KEY_ENABLE_CACHE_IP = "enable_cache_ip"
const val KEY_CACHE_EXPIRE_TIME = "cache_expire_time"
const val KEY_ENABLE_HTTPS = "enable_https"
const val KEY_ENABLE_DEGRADE = "enable_degrade"
const val KEY_ENABLE_AUTO_REFRESH = "enable_auto_refresh"
const val KEY_ENABLE_LOG = "enable_log";
const val KEY_REGION = "region";
const val KEY_TIMEOUT = "timeout"
const val KEY_IP_RANKING_ITEMS = "ip_ranking_items"
const val KEY_TTL_CHANGER = "ttl_changer"
const val KEY_TAGS = "tags"
const val KEY_HOST_WITH_FIXED_IP = "host_with_fixed_ip"
const val KEY_HOST_BLACK_LIST = "host_black_list"
const val KEY_ASYNC_RESOLVE = "async_resolve"
const val KEY_SDNS_RESOLVE = "sdns_resolve"
const val KEY_RESOLVE_IP_TYPE = "resolve_ip_type"
const val KEY_RESOLVE_METHOD = "resolve_method"
const val KEY_PRE_RESOLVE_HOST_LIST = "pre_resolve_host_list"
const val KEY_SDNS_GLOBAL_PARAMS = "sdns_global_params"
const val KEY_BATCH_RESOLVE_HOST_LIST = "batch_resolve_host_list"

View File

@@ -0,0 +1,139 @@
package com.newsdk.ams.emas.demo.net
import android.content.Context
import android.util.Log
import com.newsdk.ams.emas.demo.HttpDnsServiceHolder
import com.newsdk.ams.emas.demo.readStringFrom
import com.newsdk.ams.emas.demo.ui.resolve.Response
import com.newsdk.sdk.android.httpdns.HTTPDNSResult
import com.newsdk.sdk.android.httpdns.HttpDnsCallback
import com.newsdk.sdk.android.httpdns.NetType
import com.newsdk.sdk.android.httpdns.RequestIpType
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.CountDownLatch
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
/**
* @author allen.wy
* @date 2023/5/26
*/
class HttpURLConnectionRequest(private val context: Context, private val requestIpType: RequestIpType, private val resolveMethod: String,
private val isSdns: Boolean, private val sdnsParams: Map<String, String>?, private val cacheKey: String): IRequest {
override fun get(url: String): Response {
val conn: HttpURLConnection = getConnection(url)
val inputStream: InputStream?
val streamReader: BufferedReader?
return if (conn.responseCode != HttpURLConnection.HTTP_OK) {
inputStream = conn.errorStream
var errStr: String? = null
if (inputStream != null) {
streamReader = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
errStr = readStringFrom(streamReader).toString()
}
throw Exception("Status Code : " + conn.responseCode + " Msg : " + errStr)
} else {
inputStream = conn.inputStream
streamReader = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
val responseStr: String = readStringFrom(streamReader).toString()
Response(conn.responseCode, responseStr)
}
}
private fun getConnection(url: String): HttpURLConnection {
val host = URL(url).host
val dnsService = HttpDnsServiceHolder.getHttpDnsService(context)
var ipURL: String? = null
dnsService?.let {
//鏇挎崲涓烘渶鏂扮殑api
Log.d("HttpURLConnection", "start lookup $host via $resolveMethod")
var httpDnsResult = HTTPDNSResult("", null, null, null, false, false)
if (resolveMethod == "getHttpDnsResultForHostSync(String host, RequestIpType type)") {
httpDnsResult = if (isSdns) {
dnsService.getHttpDnsResultForHostSync(host, requestIpType, sdnsParams, cacheKey)
} else {
dnsService.getHttpDnsResultForHostSync(host, requestIpType)
}
} else if (resolveMethod == "getHttpDnsResultForHostAsync(String host, RequestIpType type, HttpDnsCallback callback)") {
val lock = CountDownLatch(1)
if (isSdns) {
dnsService.getHttpDnsResultForHostAsync(host, requestIpType, sdnsParams, cacheKey, HttpDnsCallback {
httpDnsResult = it
lock.countDown()
})
} else {
dnsService.getHttpDnsResultForHostAsync(host, requestIpType, HttpDnsCallback {
httpDnsResult = it
lock.countDown()
})
}
lock.await()
} else if (resolveMethod == "getHttpDnsResultForHostSyncNonBlocking(String host, RequestIpType type)") {
httpDnsResult = if (isSdns) {
dnsService.getHttpDnsResultForHostSyncNonBlocking(host, requestIpType, sdnsParams, cacheKey)
} else {
dnsService.getHttpDnsResultForHostSyncNonBlocking(host, requestIpType)
}
}
Log.d("HttpURLConnection", "httpdns $host 瑙f瀽缁撴灉 $httpDnsResult")
val ipStackType = HttpDnsNetworkDetector.getInstance().getNetType(context)
val isV6 = ipStackType == NetType.v6 || ipStackType == NetType.both
val isV4 = ipStackType == NetType.v4 || ipStackType == NetType.both
if (httpDnsResult.ipv6s != null && httpDnsResult.ipv6s.isNotEmpty() && isV6) {
ipURL = url.replace(host, "[" + httpDnsResult.ipv6s[0] + "]")
} else if (httpDnsResult.ips != null && httpDnsResult.ips.isNotEmpty() && isV4) {
ipURL = url.replace(host, httpDnsResult.ips[0])
}
}
val conn: HttpURLConnection = URL(ipURL ?: url).openConnection() as HttpURLConnection
conn.setRequestProperty("Host", host)
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
if (conn is HttpsURLConnection) {
val sslSocketFactory = TLSSNISocketFactory(conn)
// SNI鍦烘櫙锛屽垱寤篠SLSocket
conn.sslSocketFactory = sslSocketFactory
// https鍦烘櫙锛岃瘉涔︽牎楠?
conn.hostnameVerifier = HostnameVerifier { _, session ->
val requestHost = conn.getRequestProperty("Host") ?:conn.getURL().host
HttpsURLConnection.getDefaultHostnameVerifier().verify(requestHost, session)
}
}
val responseCode = conn.responseCode
if (needRedirect(responseCode)) {
//涓存椂閲嶅畾鍚戝拰姘镐箙閲嶅畾鍚憀ocation鐨勫ぇ灏忓啓鏈夊尯鍒?
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (!(location!!.startsWith("http://") || location.startsWith("https://"))) {
//鏌愪簺鏃跺€欎細鐪佺暐host锛屽彧杩斿洖鍚庨潰鐨刾ath锛屾墍浠ラ渶瑕佽ˉ鍏rl
val originalUrl = URL(url)
location = (originalUrl.protocol + "://"
+ originalUrl.host + location)
}
return getConnection(location)
}
return conn
}
private fun needRedirect(code: Int): Boolean {
return code in 300..399
}
}

View File

@@ -0,0 +1,12 @@
package com.newsdk.ams.emas.demo.net
import com.newsdk.ams.emas.demo.ui.resolve.Response
/**
* @author allen.wy
* @date 2023/5/26
*/
interface IRequest {
fun get(url: String): Response
}

View File

@@ -0,0 +1,150 @@
package com.newsdk.ams.emas.demo.net
import android.content.Context
import android.util.Log
import com.newsdk.ams.emas.demo.HttpDnsServiceHolder
import com.newsdk.sdk.android.httpdns.HTTPDNSResult
import com.newsdk.sdk.android.httpdns.HttpDnsCallback
import com.newsdk.sdk.android.httpdns.NetType
import com.newsdk.sdk.android.httpdns.RequestIpType
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector
import okhttp3.ConnectionPool
import okhttp3.Dns
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.lang.ref.WeakReference
import java.net.InetAddress
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* @author allen.wy
* @date 2023/6/14
*/
class OkHttpClientSingleton private constructor(context: Context
) {
private val mContext = WeakReference(context)
private var mRequestIpType = RequestIpType.v4
private var mResolveMethod: String = "getHttpDnsResultForHostSync(String host, RequestIpType type)"
private var mIsSdns: Boolean = false
private var mSdnsParams: Map<String, String>? = null
private var mCacheKey: String? = null
private val tag: String = "httpdns:hOkHttpClientSingleton"
companion object {
@Volatile
private var instance: OkHttpClientSingleton? = null
fun getInstance(context: Context): OkHttpClientSingleton {
if (instance != null) {
return instance!!
}
return synchronized(this) {
if (instance != null) {
instance!!
} else {
instance = OkHttpClientSingleton(context)
instance!!
}
}
}
}
fun updateConfig(requestIpType: RequestIpType, resolveMethod: String, isSdns: Boolean, params: Map<String, String>?, cacheKey: String): OkHttpClientSingleton {
mRequestIpType = requestIpType
mResolveMethod = resolveMethod
mIsSdns = isSdns
mSdnsParams = params
mCacheKey = cacheKey
return this
}
fun getOkHttpClient(): OkHttpClient {
val loggingInterceptor = HttpLoggingInterceptor(OkHttpLog())
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
return OkHttpClient.Builder()
.connectionPool(ConnectionPool(0, 10 * 1000, TimeUnit.MICROSECONDS))
.hostnameVerifier { _, _ ->true }
.dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> {
Log.d(tag, "start lookup $hostname via $mResolveMethod")
val dnsService = HttpDnsServiceHolder.getHttpDnsService(mContext.get()!!)
//淇敼涓烘渶鏂扮殑閫氫織鏄撴噦鐨刟pi
var httpDnsResult: HTTPDNSResult? = null
val inetAddresses = mutableListOf<InetAddress>()
if (mResolveMethod == "getHttpDnsResultForHostSync(String host, RequestIpType type)") {
httpDnsResult = if (mIsSdns) {
dnsService?.getHttpDnsResultForHostSync(hostname, mRequestIpType, mSdnsParams, mCacheKey)
} else {
dnsService?.getHttpDnsResultForHostSync(hostname, mRequestIpType)
}
} else if (mResolveMethod == "getHttpDnsResultForHostAsync(String host, RequestIpType type, HttpDnsCallback callback)") {
val lock = CountDownLatch(1)
if (mIsSdns) {
dnsService?.getHttpDnsResultForHostAsync(
hostname,
mRequestIpType,
mSdnsParams,
mCacheKey,
HttpDnsCallback {
httpDnsResult = it
lock.countDown()
})
} else {
dnsService?.getHttpDnsResultForHostAsync(
hostname,
mRequestIpType,
HttpDnsCallback {
httpDnsResult = it
lock.countDown()
})
}
lock.await()
} else if (mResolveMethod == "getHttpDnsResultForHostSyncNonBlocking(String host, RequestIpType type)") {
httpDnsResult = if (mIsSdns) {
dnsService?.getHttpDnsResultForHostSyncNonBlocking(hostname, mRequestIpType, mSdnsParams, mCacheKey)
} else {
dnsService?.getHttpDnsResultForHostSyncNonBlocking(hostname, mRequestIpType)
}
}
Log.d(tag, "httpdns $hostname 瑙f瀽缁撴灉 $httpDnsResult")
httpDnsResult?.let { processDnsResult(it, inetAddresses) }
if (inetAddresses.isEmpty()) {
Log.d(tag, "httpdns 鏈繑鍥濱P锛岃蛋local dns")
return Dns.SYSTEM.lookup(hostname)
}
return inetAddresses
}
})
.addNetworkInterceptor(loggingInterceptor)
.build()
}
fun processDnsResult(httpDnsResult: HTTPDNSResult, inetAddresses: MutableList<InetAddress>) {
val ipStackType = HttpDnsNetworkDetector.getInstance().getNetType(mContext.get())
val isV6 = ipStackType == NetType.v6 || ipStackType == NetType.both
val isV4 = ipStackType == NetType.v4 || ipStackType == NetType.both
if (httpDnsResult.ipv6s != null && httpDnsResult.ipv6s.isNotEmpty() && isV6) {
for (i in httpDnsResult.ipv6s.indices) {
inetAddresses.addAll(
InetAddress.getAllByName(httpDnsResult.ipv6s[i]).toList()
)
}
} else if (httpDnsResult.ips != null && httpDnsResult.ips.isNotEmpty() && isV4) {
for (i in httpDnsResult.ips.indices) {
inetAddresses.addAll(
InetAddress.getAllByName(httpDnsResult.ips[i]).toList()
)
}
}
}
}

View File

@@ -0,0 +1,11 @@
package com.newsdk.ams.emas.demo.net
import android.util.Log
import okhttp3.logging.HttpLoggingInterceptor
class OkHttpLog: HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.d("Okhttp", message)
}
}

View File

@@ -0,0 +1,27 @@
package com.newsdk.ams.emas.demo.net
import android.content.Context
import com.newsdk.ams.emas.demo.ui.resolve.Response
import com.newsdk.sdk.android.httpdns.RequestIpType
/**
* @author allen.wy
* @date 2023/5/25
*/
class OkHttpRequest constructor(
private val context: Context,
private val requestIpType: RequestIpType,
private val resolveMethod: String,
private val mIsSdns: Boolean,
private val mSdnsParams: Map<String, String>?,
private val mCacheKey: String
) : IRequest {
override fun get(url: String): Response {
val request = okhttp3.Request.Builder().url(url).build()
OkHttpClientSingleton.getInstance(context).updateConfig(requestIpType, resolveMethod, mIsSdns, mSdnsParams, mCacheKey).getOkHttpClient().newCall(request).execute()
.use { response -> return Response(response.code, response.body?.string()) }
}
}

View File

@@ -0,0 +1,84 @@
package com.newsdk.ams.emas.demo.net
import android.net.SSLCertificateSocketFactory
import android.os.Build
import java.net.InetAddress
import java.net.Socket
import javax.net.ssl.*
/**
* @author allen.wy
* @date 2023/5/26
*/
class TLSSNISocketFactory(connection: HttpsURLConnection): SSLSocketFactory() {
private var mConnection: HttpsURLConnection
private var hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
init {
mConnection = connection
}
override fun createSocket(plainSocket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket? {
var peerHost: String? = mConnection.getRequestProperty("Host")
if (peerHost == null) peerHost = host
val address = plainSocket!!.inetAddress
if (autoClose) {
// we don't need the plainSocket
plainSocket.close()
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
// create and connect SSL socket, but don't do hostname/certificate verification yet
val sslSocketFactory =
SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
val ssl = sslSocketFactory.createSocket(address, port) as SSLSocket
// enable TLSv1.1/1.2 if available
// enable TLSv1.1/1.2 if available
ssl.enabledProtocols = ssl.supportedProtocols
// set up SNI before the handshake
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
sslSocketFactory.setHostname(ssl, peerHost)
}
// verify hostname and certificate
// verify hostname and certificate
val session = ssl.session
if (!hostnameVerifier.verify(peerHost, session)) throw SSLPeerUnverifiedException(
"Cannot verify hostname: $peerHost"
)
return ssl
}
override fun createSocket(host: String?, port: Int): Socket? {
return null
}
override fun createSocket(host: String?, port: Int, inetAddress: InetAddress?, localPort: Int): Socket? {
return null
}
override fun createSocket(host: InetAddress?, port: Int): Socket? {
return null
}
override fun createSocket(host: InetAddress?, port: Int, localHost: InetAddress?, localPot: Int): Socket? {
return null
}
override fun getDefaultCipherSuites(): Array<String?> {
return arrayOfNulls(0)
}
override fun getSupportedCipherSuites(): Array<String?> {
return arrayOfNulls(0)
}
}

View File

@@ -0,0 +1,179 @@
package com.newsdk.ams.emas.demo.ui.basic
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatEditText
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.newsdk.ams.emas.demo.constant.KEY_REGION
import com.newsdk.ams.emas.demo.getAccountPreference
import com.newsdk.ams.emas.demo.ui.info.list.ListActivity
import com.newsdk.ams.emas.demo.ui.info.list.kListItemTag
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.FragmentBasicSettingBinding
class BasicSettingFragment : Fragment(), IBasicShowDialog {
private var _binding: FragmentBasicSettingBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: BasicSettingViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel =
ViewModelProvider(this,)[BasicSettingViewModel::class.java]
viewModel.showDialog = this
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBasicSettingBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
val root: View = binding.root
viewModel.initData()
binding.viewModel = viewModel
binding.jumpToAddTag.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemTag)
startActivity(intent)
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun showSelectRegionDialog() {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.select_region)
val china = getString(R.string.china)
val chinaHK = getString(R.string.china_hk)
val singapore = getString(R.string.singapore)
val germany = getString(R.string.germany)
val america = getString(R.string.america)
val pre = getString(R.string.pre)
val items = arrayOf(china, chinaHK, singapore, germany, america, pre)
var region = ""
val preferences = activity?.let { getAccountPreference(it) }
val index = when (preferences?.getString(KEY_REGION, "cn")) {
"hk" -> 1
"sg" -> 2
"de" -> 3
"us" -> 4
"pre" -> 5
else -> 0
}
setSingleChoiceItems(items, index) { _, which ->
region = when (which) {
1 -> "hk"
2 -> "sg"
3 -> "de"
4 -> "us"
5 -> "pre"
else -> "cn"
}
}
setPositiveButton(getString(R.string.confirm)) { dialog, _ ->
viewModel.saveRegion(region)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}
builder?.show()
}
override fun showSetTimeoutDialog() {
val input = LayoutInflater.from(activity).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.timeout_hint)
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(getString(R.string.set_timeout))
setView(input)
setPositiveButton(R.string.confirm) { dialog, _ ->
when (val timeout = editText.text.toString()) {
"" -> Toast.makeText(activity, R.string.timeout_empty, Toast.LENGTH_SHORT)
.show()
else -> viewModel.saveTimeout(timeout.toInt())
}
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
override fun showInputHostDialog() {
val input = LayoutInflater.from(activity).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.clear_cache_hint)
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(getString(R.string.clear_host_cache))
setView(input)
setPositiveButton(R.string.confirm) { dialog, _ ->
viewModel.clearDnsCache(editText.text.toString())
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
override fun showAddPreResolveDialog() {
val input = LayoutInflater.from(activity).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_pre_resolve_hint)
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(getString(R.string.add_pre_resolve))
setView(input)
setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(activity, R.string.pre_resolve_host_is_empty, Toast.LENGTH_SHORT)
.show()
else -> viewModel.addPreResolveDomain(host)
}
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
override fun onHttpDnsInit() {
activity?.runOnUiThread(Runnable {
_binding?.initHttpdns?.setText(R.string.inited_httpdns)
_binding?.initHttpdns?.isClickable = false
})
}
}

View File

@@ -0,0 +1,369 @@
package com.newsdk.ams.emas.demo.ui.basic
import android.app.Application
import android.text.TextUtils
import android.util.Log
import android.widget.CompoundButton
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import com.newsdk.ams.emas.demo.*
import com.newsdk.ams.emas.demo.constant.*
import com.newsdk.sdk.android.httpdns.HttpDns
import com.newsdk.sdk.android.httpdns.HttpDnsService
import com.newsdk.sdk.android.httpdns.InitConfig
import com.newsdk.sdk.android.httpdns.NotUseHttpDnsFilter
import com.newsdk.sdk.android.httpdns.RequestIpType
import com.newsdk.sdk.android.httpdns.log.HttpDnsLog
import com.newsdk.ams.httpdns.demo.BuildConfig
import com.newsdk.ams.httpdns.demo.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
class BasicSettingViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private const val DAY_MILLS = 24 * 60 * 60 * 1000
}
private val preferences = getAccountPreference(getApplication())
private var dnsService: HttpDnsService? = null
var secretKeySetByConfig = true
/**
* 鏄惁寮€鍚壌鏉冩ā寮?
*/
var enableAuthMode = true
/**
* 鏄惁寮€鍚姞瀵嗘ā寮?
*/
var enableEncryptMode = true
/**
* 鏄惁鍏佽杩囨湡IP
*/
var enableExpiredIP = false
/**
* 鏄惁寮€鍚湰鍦扮紦瀛?
*/
var enableCacheIP = false
/**
* 鏄惁鍏佽HTTPS
*/
var enableHttps = false
/**
* 鏄惁寮€鍚檷绾?
*/
var enableDegrade = false
/**
* 鏄惁鍏佽缃戠粶鍒囨崲鑷姩鍒锋柊
*/
var enableAutoRefresh = false
/**
* 鏄惁鍏佽鎵撳嵃鏃ュ織
*/
var enableLog = false
/**
* 褰撳墠Region
*/
var currentRegion = SingleLiveData<String>().apply {
value = ""
}
/**
* 褰撳墠瓒呮椂
*/
var currentTimeout = SingleLiveData<String>().apply {
value = "2000ms"
}
var cacheExpireTime = SingleLiveData<String>().apply {
value = "0"
}
var showDialog: IBasicShowDialog? = null
fun initData() {
secretKeySetByConfig = preferences.getBoolean(KEY_SECRET_KEY_SET_BY_CONFIG, true)
enableAuthMode = preferences.getBoolean(KEY_ENABLE_AUTH_MODE, true)
enableEncryptMode = preferences.getBoolean(KEY_ENABLE_ENCRYPT_MODE, true)
enableExpiredIP = preferences.getBoolean(KEY_ENABLE_EXPIRED_IP, false)
enableCacheIP = preferences.getBoolean(KEY_ENABLE_CACHE_IP, false)
cacheExpireTime.value = preferences.getString(KEY_CACHE_EXPIRE_TIME, "0")
enableHttps = preferences.getBoolean(KEY_ENABLE_HTTPS, false)
enableDegrade = preferences.getBoolean(KEY_ENABLE_DEGRADE, false)
enableAutoRefresh = preferences.getBoolean(KEY_ENABLE_AUTO_REFRESH, false)
enableLog = preferences.getBoolean(KEY_ENABLE_LOG, false)
when (preferences.getString(KEY_REGION, "cn")) {
"cn" -> currentRegion.value = getString(R.string.china)
"hk" -> currentRegion.value = getString(R.string.china_hk)
"sg" -> currentRegion.value = getString(R.string.singapore)
"de" -> currentRegion.value = getString(R.string.germany)
"us" -> currentRegion.value = getString(R.string.america)
"pre" -> currentRegion.value = getString(R.string.pre)
}
currentTimeout.value = "${preferences.getInt(KEY_TIMEOUT, 2000)}ms"
if (MainActivity.HttpDns.inited) {
dnsService = HttpDnsServiceHolder.getHttpDnsService(getApplication())
showDialog?.onHttpDnsInit()
}
}
fun toggleSecretKeySet(button: CompoundButton, checked: Boolean) {
secretKeySetByConfig = checked
val editor = preferences.edit()
editor.putBoolean(KEY_SECRET_KEY_SET_BY_CONFIG, checked)
editor.apply()
}
fun toggleAuthMode(button: CompoundButton, checked: Boolean) {
enableAuthMode = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_AUTH_MODE, checked)
editor.apply()
}
fun toggleEncryptMode(button: CompoundButton, checked: Boolean) {
enableEncryptMode = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_ENCRYPT_MODE, checked)
editor.apply()
}
fun toggleEnableExpiredIp(button: CompoundButton, checked: Boolean) {
enableExpiredIP = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_EXPIRED_IP, checked)
editor.apply()
}
fun toggleEnableCacheIp(button: CompoundButton, checked: Boolean) {
enableCacheIP = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_CACHE_IP, checked)
editor.apply()
}
fun toggleEnableHttps(button: CompoundButton, checked: Boolean) {
enableHttps = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_HTTPS, checked)
editor.apply()
}
fun toggleEnableDegrade(button: CompoundButton, checked: Boolean) {
enableDegrade = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_DEGRADE, checked)
editor.apply()
}
fun toggleEnableAutoRefresh(button: CompoundButton, checked: Boolean) {
enableAutoRefresh = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_AUTO_REFRESH, checked)
editor.apply()
}
fun toggleEnableLog(button: CompoundButton, checked: Boolean) {
enableLog = checked
val editor = preferences.edit()
editor.putBoolean(KEY_ENABLE_LOG, checked)
editor.apply()
HttpDnsLog.enable(checked)
}
fun setRegion() {
//寮圭獥閫夋嫨region
showDialog?.showSelectRegionDialog()
}
fun saveRegion(region: String) {
currentRegion.value = when (region) {
"cn" -> getString(R.string.china)
"hk" -> getString(R.string.china_hk)
"sg" -> getString(R.string.singapore)
"de" -> getString(R.string.germany)
"pre" -> getString(R.string.pre)
else -> getString(R.string.china)
}
val editor = preferences.edit()
editor.putString(KEY_REGION, region)
editor.apply()
dnsService?.setRegion(region)
}
fun setTimeout() {
showDialog?.showSetTimeoutDialog()
}
fun saveTimeout(timeout: Int) {
currentTimeout.value = "${timeout}ms"
val editor = preferences.edit()
editor.putInt(KEY_TIMEOUT, timeout)
editor.apply()
}
fun showClearCacheDialog() {
showDialog?.showInputHostDialog()
}
fun clearDnsCache(host: String) {
if (TextUtils.isEmpty(host)) {
dnsService?.cleanHostCache(null)
} else {
dnsService?.cleanHostCache(mutableListOf(host) as ArrayList<String>)
}
}
fun batchResolveHosts() {
dnsService?.setPreResolveHosts(BatchResolveCacheHolder.batchResolveV4List, RequestIpType.v4)
dnsService?.setPreResolveHosts(BatchResolveCacheHolder.batchResolveV6List, RequestIpType.v6)
dnsService?.setPreResolveHosts(BatchResolveCacheHolder.batchResolveAutoList, RequestIpType.auto)
dnsService?.setPreResolveHosts(BatchResolveCacheHolder.batchResolveBothList, RequestIpType.both)
}
fun showAddPreResolveDialog() {
showDialog?.showAddPreResolveDialog()
}
fun initHttpDns() {
if (!TextUtils.isEmpty(BuildConfig.ACCOUNT_ID)) {
CoroutineScope(Dispatchers.Default).launch {
withContext(Dispatchers.IO) {
val aesSecretKey = if (enableEncryptMode && !TextUtils.isEmpty(BuildConfig.AES_SECRET_KEY)) BuildConfig.AES_SECRET_KEY else ""
val secretKey = if (enableAuthMode && !TextUtils.isEmpty(BuildConfig.SECRET_KEY)) BuildConfig.SECRET_KEY else ""
val enableExpiredIp = preferences.getBoolean(KEY_ENABLE_EXPIRED_IP, false)
val enableCacheIp = preferences.getBoolean(KEY_ENABLE_CACHE_IP, false)
val enableHttpDns = preferences.getBoolean(KEY_ENABLE_HTTPS, false)
val serviceUrl = BuildConfig.SERVICE_URL.trim()
val timeout = preferences.getInt(KEY_TIMEOUT, 2000)
val region = preferences.getString(KEY_REGION, "cn") ?: "cn"
val enableDegradationLocalDns = preferences.getBoolean(KEY_ENABLE_DEGRADE, false);
//鑷畾涔塼tl
val ttlCacheStr = preferences.getString(KEY_TTL_CHANGER, null)
TtlCacheHolder.convertTtlCacheData(ttlCacheStr)
//IP鎺㈡祴
val ipRankingItemJson = preferences.getString(KEY_IP_RANKING_ITEMS, null)
//涓荤珯鍩熷悕
val hostListWithFixedIpJson =
preferences.getString(KEY_HOST_WITH_FIXED_IP, null)
val tagsJson = preferences.getString(KEY_TAGS, null)
//棰勮В鏋?
val preResolveHostList = preferences.getString(KEY_PRE_RESOLVE_HOST_LIST, null)
preResolveHostList?.let { Log.d("httpdns:HttpDnsApplication", "pre resolve list: $it") }
PreResolveCacheHolder.convertPreResolveCacheData(preResolveHostList)
//鎵归噺瑙f瀽
val batchResolveHostList = preferences.getString(KEY_BATCH_RESOLVE_HOST_LIST, null)
BatchResolveCacheHolder.convertBatchResolveCacheData(batchResolveHostList)
val sdnsGlobalParamStr = preferences.getString(KEY_SDNS_GLOBAL_PARAMS, "")
var sdnsGlobalParams: MutableMap<String, String>? = null
if (!TextUtils.isEmpty(sdnsGlobalParamStr)) {
try {
val sdnsJson = JSONObject(sdnsGlobalParamStr)
val keys = sdnsJson.keys()
sdnsGlobalParams = mutableMapOf()
while (keys.hasNext()) {
val key = keys.next()
sdnsGlobalParams[key] = sdnsJson.getString(key)
}
} catch (e: JSONException) {
}
}
val cacheExpireTimeTemp = cacheExpireTime.value?.toLong() ?: 0
HttpDnsLog.enable(preferences.getBoolean(KEY_ENABLE_LOG, false))
val builder = InitConfig.Builder()
.setEnableCacheIp(enableCacheIp, cacheExpireTimeTemp * DAY_MILLS)
.setEnableExpiredIp(enableExpiredIp)
.setTimeoutMillis(timeout)
.setEnableDegradationLocalDns(enableDegradationLocalDns)
.setIPRankingList(ipRankingItemJson.toIPRankingList())
.configCacheTtlChanger(TtlCacheHolder.cacheTtlChanger)
.configHostWithFixedIp(hostListWithFixedIpJson.toHostList())
.setNotUseHttpDnsFilter(NotUseHttpDnsFilter { host ->
val blackListStr = preferences.getString(KEY_HOST_BLACK_LIST, null)
blackListStr?.let {
return@NotUseHttpDnsFilter blackListStr.contains(host)
}
return@NotUseHttpDnsFilter false
})
.setSdnsGlobalParams(sdnsGlobalParams)
.setBizTags(tagsJson.toTagList())
.setAesSecretKey(aesSecretKey)
if (!TextUtils.isEmpty(serviceUrl)) {
// Prefer fixed service URL mode: https://host:port
builder.setServiceUrl(serviceUrl)
} else {
builder.setEnableHttps(enableHttpDns)
builder.setRegion(region)
}
if (secretKeySetByConfig) {
builder.setContext(this@BasicSettingViewModel.getApplication<HttpDnsApplication>())
builder.setSecretKey(secretKey)
}
HttpDns.init(BuildConfig.ACCOUNT_ID, builder.build())
dnsService = HttpDnsServiceHolder.getHttpDnsService(getApplication())
dnsService?.setPreResolveHosts(PreResolveCacheHolder.preResolveV4List)
dnsService?.setPreResolveHosts(PreResolveCacheHolder.preResolveV6List, RequestIpType.v6)
dnsService?.setPreResolveHosts(PreResolveCacheHolder.preResolveBothList, RequestIpType.both)
dnsService?.setPreResolveHosts(PreResolveCacheHolder.preResolveAutoList, RequestIpType.auto)
showDialog?.onHttpDnsInit()
MainActivity.HttpDns.inited = true
}
}
}
}
fun addPreResolveDomain(host: String) {
val preResolveHostListStr = preferences.getString(KEY_PRE_RESOLVE_HOST_LIST, null)
val hostList: MutableList<String> = if (preResolveHostListStr == null) {
mutableListOf()
} else {
preResolveHostListStr.toHostList()!!
}
if (hostList.contains(host)) {
Toast.makeText(
getApplication(),
getString(R.string.pre_resolve_host_duplicate, host),
Toast.LENGTH_SHORT
).show()
} else {
hostList.add(host)
}
val editor = preferences.edit()
editor.putString(KEY_PRE_RESOLVE_HOST_LIST, convertPreResolveList(hostList))
editor.apply()
}
private fun getString(resId: Int): String {
return getApplication<HttpDnsApplication>().getString(resId)
}
private fun getString(resId: Int, vararg args: String): String {
return getApplication<HttpDnsApplication>().getString(resId, *args)
}
}

View File

@@ -0,0 +1,18 @@
package com.newsdk.ams.emas.demo.ui.basic
/**
* @author allen.wy
* @date 2023/5/24
*/
interface IBasicShowDialog {
fun showSelectRegionDialog()
fun showSetTimeoutDialog()
fun showInputHostDialog()
fun showAddPreResolveDialog()
fun onHttpDnsInit()
}

View File

@@ -0,0 +1,95 @@
package com.newsdk.ams.emas.demo.ui.info
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.newsdk.ams.emas.demo.ui.info.list.*
import com.newsdk.ams.httpdns.demo.BuildConfig
import com.newsdk.ams.httpdns.demo.databinding.FragmentInfoBinding
class InfoFragment : Fragment() {
private var _binding: FragmentInfoBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: InfoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[InfoViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentInfoBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.initData()
binding.infoPkgName.text = activity?.packageName
binding.infoSecretView.apply {
visibility = if (TextUtils.isEmpty(BuildConfig.SECRET_KEY)) View.GONE else View.VISIBLE
}
binding.jumpToPreResolve.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemPreResolve)
startActivity(intent)
}
binding.jumpToIpRanking.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemTypeIPRanking)
startActivity(intent)
}
binding.jumpToHostFiexIp.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemTypeHostWithFixedIP)
startActivity(intent)
}
binding.jumpToHostBlackList.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemTypeBlackList)
startActivity(intent)
}
binding.jumpToTtlCache.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemTypeCacheTtl)
startActivity(intent)
}
binding.jumpToSdnsGlobalParams.setOnClickListener {
val intent = Intent(activity, SdnsGlobalSettingActivity::class.java)
startActivity(intent)
}
binding.jumpToBatchResolve.setOnClickListener {
val intent = Intent(activity, ListActivity::class.java)
intent.putExtra("list_type", kListItemBatchResolve)
startActivity(intent)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,63 @@
package com.newsdk.ams.emas.demo.ui.info
import android.app.Application
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import com.newsdk.ams.emas.demo.HttpDnsApplication
import com.newsdk.ams.emas.demo.HttpDnsServiceHolder
import com.newsdk.ams.emas.demo.SingleLiveData
import com.newsdk.ams.emas.demo.getAccountPreference
import com.newsdk.sdk.android.httpdns.NetType
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector
import com.newsdk.ams.httpdns.demo.BuildConfig
import com.newsdk.ams.httpdns.demo.R
class InfoViewModel(application: Application) : AndroidViewModel(application) {
/**
* 璐︽埛ID
*/
val accountId = SingleLiveData<String>().apply {
value = ""
}
/**
* 璐︽埛secret
*/
val secretKey = SingleLiveData<String?>()
val currentIpStackType = SingleLiveData<String>().apply {
value = "V4"
}
fun initData() {
currentIpStackType.value = when (HttpDnsNetworkDetector.getInstance().getNetType(getApplication())) {
NetType.v4 -> "V4"
NetType.v6 -> "V6"
NetType.both -> "V4&V6"
else -> getApplication<HttpDnsApplication>().getString(R.string.unknown)
}
accountId.value = BuildConfig.ACCOUNT_ID
secretKey.value = BuildConfig.SECRET_KEY
}
fun clearDnsCache() {
val httpdnsService = HttpDnsServiceHolder.getHttpDnsService(getApplication())
var i = 0;
while (i < 500) {
httpdnsService?.cleanHostCache(null)
++i
}
}
fun clearAllCache() {
val preferences = getAccountPreference(getApplication())
preferences.edit().clear().apply()
Toast.makeText(getApplication(), R.string.all_cache_cleared, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -0,0 +1,46 @@
package com.newsdk.ams.emas.demo.ui.info
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AppCompatActivity
import com.newsdk.ams.emas.demo.constant.KEY_SDNS_GLOBAL_PARAMS
import com.newsdk.ams.emas.demo.getAccountPreference
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.ActivitySdnsGlobalSettingBinding
import org.json.JSONException
import org.json.JSONObject
class SdnsGlobalSettingActivity: AppCompatActivity() {
private lateinit var binding: ActivitySdnsGlobalSettingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val preferences = getAccountPreference(this)
binding = ActivitySdnsGlobalSettingBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.title = getString(R.string.input_the_sdns_params)
val params = preferences.getString(KEY_SDNS_GLOBAL_PARAMS, "")
binding.sdnsParamsInputLayout.editText?.setText(params)
binding.toolbar.setNavigationOnClickListener {
val sdnsParamsStr = binding.sdnsParamsInputLayout.editText?.text.toString()
if (!TextUtils.isEmpty(sdnsParamsStr)) {
try {
val sdnsJson = JSONObject(sdnsParamsStr)
preferences.edit().putString(KEY_SDNS_GLOBAL_PARAMS, sdnsParamsStr).apply()
onBackPressed()
} catch (e: JSONException) {
binding.sdnsParamsInputLayout.error = getString(R.string.input_the_sdns_params_error)
}
} else {
preferences.edit().putString(KEY_SDNS_GLOBAL_PARAMS, "").apply()
onBackPressed()
}
}
}
}

View File

@@ -0,0 +1,353 @@
package com.newsdk.ams.emas.demo.ui.info.list
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatEditText
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.ActivityListBinding
class ListActivity : AppCompatActivity(), ListAdapter.OnDeleteListener {
private lateinit var binding: ActivityListBinding
private val infoList: MutableList<ListItem> = mutableListOf()
private lateinit var listAdapter: ListAdapter
private var listType: Int = kListItemTypeIPRanking
private lateinit var viewModel: ListViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var title = ""
intent?.let {
listType = intent.getIntExtra("list_type", kListItemTypeIPRanking)
title = when (listType) {
kListItemTypeCacheTtl -> getString(R.string.ttl_cache_list)
kListItemTypeHostWithFixedIP -> getString(R.string.host_fixed_ip_list)
kListItemPreResolve -> getString(R.string.pre_resolve_list)
kListItemTypeBlackList -> getString(R.string.host_black_list)
kListItemBatchResolve -> getString(R.string.batch_resolve_list)
kListItemTag -> getString(R.string.add_tag)
else -> getString(R.string.ip_probe_list)
}
}
viewModel = ViewModelProvider(this)[ListViewModel::class.java]
binding = ActivityListBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.infoListToolbar.title = title
setSupportActionBar(binding.infoListToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)//娣诲姞榛樿鐨勮繑鍥炲浘鏍?
supportActionBar?.setHomeButtonEnabled(true)
binding.infoListView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
viewModel.initData(listType, infoList)
listAdapter = ListAdapter(this, infoList, this)
binding.infoListView.adapter = listAdapter
binding.fab.setOnClickListener {
showAddDialog()
}
}
private fun showAddDialog() {
when (listType) {
kListItemTag -> {
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_tag_hint)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.add_tag))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.host_fixed_ip_empty,
Toast.LENGTH_SHORT
).show()
else -> {
viewModel.toAddTag(host, listAdapter)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
kListItemTypeHostWithFixedIP -> {
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_host_fixed_ip_hint)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.add_host_fixed_ip))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.host_fixed_ip_empty,
Toast.LENGTH_SHORT
).show()
else -> {
viewModel.toAddHostWithFixedIP(host, listAdapter)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
kListItemTypeBlackList -> {
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_host_to_black_list_hint)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.add_host_to_black_list))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.host_to_black_list_empty,
Toast.LENGTH_SHORT
).show()
else -> {
viewModel.toAddHostInBlackList(host, listAdapter)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
kListItemPreResolve -> {
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input_3, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_pre_resolve_hint)
val ipTypeGroup = input.findViewById<RadioGroup>(R.id.ip_type)
var view = createIpTypeRadio(this)
view.text = "IPv4"
view.isChecked = true
view.tag = 0
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "IPv6"
view.tag = 1
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "IPv4&IPv6"
view.tag = 2
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "鑷姩鍒ゆ柇IP绫诲瀷"
view.tag = 3
ipTypeGroup.addView(view)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.add_pre_resolve))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.pre_resolve_host_is_empty,
Toast.LENGTH_SHORT
).show()
else -> {
viewModel.toAddPreResolveHost(host, listAdapter, ipTypeGroup.findViewById<RadioButton>(ipTypeGroup.checkedRadioButtonId).tag as Int)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
kListItemBatchResolve -> {
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input_3, null)
val editText = input.findViewById<AppCompatEditText>(R.id.add_input)
editText.hint = getString(R.string.add_batch_resolve_hint)
val ipTypeGroup = input.findViewById<RadioGroup>(R.id.ip_type)
var view = createIpTypeRadio(this)
view.text = "IPv4"
view.isChecked = true
view.tag = 0
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "IPv6"
view.tag = 1
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "IPv4&IPv6"
view.tag = 2
ipTypeGroup.addView(view)
view = createIpTypeRadio(this)
view.text = "鑷姩鍒ゆ柇IP绫诲瀷"
view.tag = 3
ipTypeGroup.addView(view)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.add_batch_resolve))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = editText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.batch_resolve_host_is_empty,
Toast.LENGTH_SHORT
).show()
else -> {
viewModel.toAddBatchResolveHost(host, listAdapter, ipTypeGroup.findViewById<RadioButton>(ipTypeGroup.checkedRadioButtonId).tag as Int)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
else -> {
val isTtl = listType == kListItemTypeCacheTtl
val input = LayoutInflater.from(this).inflate(R.layout.dialog_input_2, null)
val hostEditText = input.findViewById<AppCompatEditText>(R.id.input_content_1)
val intEditText = input.findViewById<AppCompatEditText>(R.id.input_content_2)
intEditText.inputType = EditorInfo.TYPE_CLASS_NUMBER
hostEditText.hint =
getString(if (isTtl) R.string.add_ttl_host_hint else R.string.add_ip_probe_host_hint)
intEditText.hint =
getString(if (isTtl) R.string.add_ttl_ttl_hint else R.string.add_ip_probe_port_hint)
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(if (isTtl) R.string.add_custom_ttl else R.string.add_ip_probe))
.setView(input)
.setPositiveButton(R.string.confirm) { dialog, _ ->
when (val host = hostEditText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
R.string.host_is_empty,
Toast.LENGTH_SHORT
).show()
else -> {
when (val intValue = intEditText.text.toString()) {
"" -> Toast.makeText(
this@ListActivity,
if (isTtl) R.string.ttl_is_empty else R.string.port_is_empty,
Toast.LENGTH_SHORT
).show()
else -> {
try {
if (isTtl) {
viewModel.toSaveTtlCache(
host,
intValue.toInt(),
listAdapter
)
} else {
viewModel.toSaveIPProbe(
host,
intValue.toInt(),
listAdapter
)
}
} catch (e: NumberFormatException) {
Toast.makeText(
this@ListActivity,
R.string.ttl_is_not_number,
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
}
}
}
fun createIpTypeRadio(context: Context): RadioButton {
val btn = RadioButton(context)
btn.id = View.generateViewId()
val params = RadioGroup.LayoutParams(RadioGroup.LayoutParams.MATCH_PARENT, RadioGroup.LayoutParams.WRAP_CONTENT)
btn.layoutParams = params
return btn
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
override fun onTagDeleted(position: Int) {
viewModel.onTagDeleted(position)
}
override fun onHostWithFixedIPDeleted(position: Int) {
//鍙兘閲嶅惎鐢熸晥
viewModel.onHostWithFixedIPDeleted(position)
}
override fun onIPRankingItemDeleted(position: Int) {
viewModel.onIPProbeItemDeleted(position)
}
override fun onTtlDeleted(host: String) {
viewModel.onTtlDeleted(host)
}
override fun onPreResolveDeleted(host: String, intValue: Int) {
Log.d("httpdns", "onPreResolveDeleted")
viewModel.onPreResolveDeleted(host, intValue)
}
override fun onHostBlackListDeleted(position: Int) {
viewModel.onHostBlackListDeleted(position)
}
override fun onBatchResolveDeleted(host: String, intValue: Int) {
viewModel.onBatchResolveDeleted(host, intValue)
}
}

View File

@@ -0,0 +1,151 @@
package com.newsdk.ams.emas.demo.ui.info.list
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.InfoListItemBinding;
/**
* @author allen.wy
* @date 2023/6/5
*/
class ListAdapter(private val context: Context,
private val itemList: MutableList<ListItem>,
private val deleteListener: OnDeleteListener) :
RecyclerView.Adapter<ListAdapter.ListViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val binding = InfoListItemBinding.inflate(LayoutInflater.from(context))
return ListViewHolder(context, binding)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
if (itemList.isEmpty()) {
return
}
holder.setItemValue(itemList[position]) {
when (itemList[holder.adapterPosition].type) {
kListItemTag -> deleteListener.onTagDeleted(holder.adapterPosition)
kListItemTypeHostWithFixedIP -> deleteListener.onHostWithFixedIPDeleted(holder.adapterPosition)
kListItemTypeBlackList -> deleteListener.onHostBlackListDeleted(holder.adapterPosition)
kListItemTypeCacheTtl -> deleteListener.onTtlDeleted(itemList[holder.adapterPosition].content)
kListItemPreResolve -> deleteListener.onPreResolveDeleted(itemList[holder.adapterPosition].content, itemList[holder.adapterPosition].intValue)
kListItemBatchResolve -> deleteListener.onBatchResolveDeleted(itemList[holder.adapterPosition].content, itemList[holder.adapterPosition].intValue)
else -> deleteListener.onIPRankingItemDeleted(holder.adapterPosition)
}
itemList.removeAt(holder.adapterPosition)
notifyItemRemoved(holder.adapterPosition)
}
}
override fun getItemCount(): Int {
return itemList.size
}
fun addItemData(item: ListItem) {
itemList.add(item)
notifyItemInserted(itemList.size - 1)
}
fun getPositionByContent(content: String): Int {
for (index in itemList.indices) {
if (content == itemList[index].content) {
return index
}
}
return -1
}
fun updateItemByPosition(content:String, intValue: Int, position: Int) {
itemList[position].content = content
itemList[position].intValue = intValue
notifyItemChanged(position)
}
class ListViewHolder(private val context: Context, private val binding: InfoListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun setItemValue(listItem: ListItem, onDeleteListener: View.OnClickListener) {
when (listItem.type) {
kListItemTag -> {
binding.hostFixedIpContainer.visibility = View.VISIBLE
binding.hostAndPortOrTtlContainer.visibility = View.GONE
binding.preHostOrWithFixedIp.text = listItem.content
}
kListItemTypeIPRanking -> {
binding.hostFixedIpContainer.visibility = View.GONE
binding.hostAndPortOrTtlContainer.visibility = View.VISIBLE
binding.hostValue.text = listItem.content
binding.portOrTtlValue.text = listItem.intValue.toString()
binding.portOrTtlIndicate.text = context.getString(R.string.port)
}
kListItemTypeCacheTtl -> {
binding.hostFixedIpContainer.visibility = View.GONE
binding.hostAndPortOrTtlContainer.visibility = View.VISIBLE
binding.hostValue.text = listItem.content
binding.portOrTtlValue.text = listItem.intValue.toString()
binding.portOrTtlIndicate.text = context.getString(R.string.ttl)
}
kListItemTypeHostWithFixedIP -> {
binding.hostFixedIpContainer.visibility = View.VISIBLE
binding.hostAndPortOrTtlContainer.visibility = View.GONE
binding.preHostOrWithFixedIp.text = listItem.content
}
kListItemTypeBlackList -> {
binding.hostFixedIpContainer.visibility = View.VISIBLE
binding.hostAndPortOrTtlContainer.visibility = View.GONE
binding.preHostOrWithFixedIp.text = listItem.content
}
kListItemPreResolve -> {
binding.hostFixedIpContainer.visibility = View.GONE
binding.hostAndPortOrTtlContainer.visibility = View.VISIBLE
binding.hostValue.text = listItem.content
binding.portOrTtlValue.text = when (listItem.intValue) {
0 -> "IPv4"
1 -> "IPv6"
2 -> "IPv4&IPv6"
else -> "鑷姩鍒ゆ柇IP绫诲瀷"
}
binding.portOrTtlIndicate.text = context.getString(R.string.ip_type)
}
kListItemBatchResolve -> {
binding.hostFixedIpContainer.visibility = View.GONE
binding.hostAndPortOrTtlContainer.visibility = View.VISIBLE
binding.hostValue.text = listItem.content
binding.portOrTtlValue.text = when (listItem.intValue) {
0 -> "IPv4"
1 -> "IPv6"
2 -> "IPv4&IPv6"
else -> "鑷姩鍒ゆ柇IP绫诲瀷"
}
binding.portOrTtlIndicate.text = context.getString(R.string.ip_type)
}
}
binding.slideDeleteMenu.setOnClickListener(onDeleteListener)
binding.slideDeleteMenu2.setOnClickListener(onDeleteListener)
}
}
interface OnDeleteListener {
fun onTagDeleted(position: Int)
fun onHostWithFixedIPDeleted(position: Int)
fun onIPRankingItemDeleted(position: Int)
fun onTtlDeleted(host: String)
fun onPreResolveDeleted(host: String, intValue: Int)
fun onHostBlackListDeleted(position: Int)
fun onBatchResolveDeleted(host: String, intValue: Int)
}
}

View File

@@ -0,0 +1,9 @@
package com.newsdk.ams.emas.demo.ui.info.list
/**
* @author allen.wy
* @date 2023/6/5
*/
data class ListItem(var type: Int, var content: String, var intValue: Int)

View File

@@ -0,0 +1,21 @@
package com.newsdk.ams.emas.demo.ui.info.list
/**
* @author allen.wy
* @date 2023/6/5
*/
const val kListItemTypeIPRanking = 0x01
const val kListItemTypeCacheTtl = 0x02
const val kListItemTypeHostWithFixedIP = 0x03
const val kListItemPreResolve = 0x04
const val kListItemTypeBlackList = 0x05
const val kListItemBatchResolve = 0x06
const val kListItemTag = 0x07

View File

@@ -0,0 +1,407 @@
package com.newsdk.ams.emas.demo.ui.info.list
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.newsdk.ams.emas.demo.*
import com.newsdk.ams.emas.demo.TtlCacheHolder.toJsonString
import com.newsdk.ams.emas.demo.constant.KEY_BATCH_RESOLVE_HOST_LIST
import com.newsdk.ams.emas.demo.constant.KEY_HOST_BLACK_LIST
import com.newsdk.ams.emas.demo.constant.KEY_HOST_WITH_FIXED_IP
import com.newsdk.ams.emas.demo.constant.KEY_IP_RANKING_ITEMS
import com.newsdk.ams.emas.demo.constant.KEY_PRE_RESOLVE_HOST_LIST
import com.newsdk.ams.emas.demo.constant.KEY_TAGS
import com.newsdk.ams.emas.demo.constant.KEY_TTL_CHANGER
import com.newsdk.sdk.android.httpdns.ranking.IPRankingBean
import com.newsdk.ams.httpdns.demo.R
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
/**
* @author allen.wy
* @date 2023/6/6
*/
class ListViewModel(application: Application) : AndroidViewModel(application) {
private var hostFixedIpList: MutableList<String> = mutableListOf()
private var ipRankingList: MutableList<IPRankingBean> = mutableListOf()
private var hostBlackList: MutableList<String> = mutableListOf()
private var tagsList: MutableList<String> = mutableListOf()
private lateinit var preferences: SharedPreferences
fun initData(listType: Int, infoList: MutableList<ListItem>) {
preferences = getAccountPreference(getApplication())
viewModelScope.launch {
when (listType) {
kListItemTag -> {
val tagStr = preferences.getString(KEY_TAGS, null)
val list = tagStr.toTagList()
list?.let {
tagsList.addAll(list)
for (tag in tagsList) {
infoList.add(ListItem(kListItemTag, tag, 0))
}
}
}
kListItemTypeHostWithFixedIP -> {
val hostFixedIpStr = preferences.getString(KEY_HOST_WITH_FIXED_IP, null)
val list = hostFixedIpStr.toHostList()
list?.let {
hostFixedIpList.addAll(list)
for (host in hostFixedIpList) {
infoList.add(ListItem(kListItemTypeHostWithFixedIP, host, 0))
}
}
}
kListItemTypeBlackList -> {
val hostBlackListStr = preferences.getString(KEY_HOST_BLACK_LIST, null)
val list = hostBlackListStr.toBlackList()
list?.let {
hostBlackList.addAll(list)
for (host in hostBlackList) {
infoList.add(ListItem(kListItemTypeBlackList, host, 0))
}
}
}
kListItemTypeCacheTtl -> {
val ttlCacheStr = preferences.getString(KEY_TTL_CHANGER, null)
val map = ttlCacheStr.toTtlCacheMap()
map?.let {
TtlCacheHolder.ttlCache.putAll(map)
for ((host, ttl) in TtlCacheHolder.ttlCache) {
infoList.add(ListItem(kListItemTypeCacheTtl, host, ttl))
}
}
}
kListItemPreResolve -> {
for (host in PreResolveCacheHolder.preResolveV4List) {
infoList.add(ListItem(kListItemPreResolve, host, 0))
}
for (host in PreResolveCacheHolder.preResolveV6List) {
infoList.add(ListItem(kListItemPreResolve, host, 1))
}
for (host in PreResolveCacheHolder.preResolveBothList) {
infoList.add(ListItem(kListItemPreResolve, host, 2))
}
for (host in PreResolveCacheHolder.preResolveAutoList) {
infoList.add(ListItem(kListItemPreResolve, host, 3))
}
}
kListItemBatchResolve -> {
for (host in BatchResolveCacheHolder.batchResolveV4List) {
infoList.add(ListItem(kListItemBatchResolve, host, 0))
}
for (host in BatchResolveCacheHolder.batchResolveV6List) {
infoList.add(ListItem(kListItemBatchResolve, host, 1))
}
for (host in BatchResolveCacheHolder.batchResolveBothList) {
infoList.add(ListItem(kListItemBatchResolve, host, 2))
}
for (host in BatchResolveCacheHolder.batchResolveAutoList) {
infoList.add(ListItem(kListItemBatchResolve, host, 3))
}
}
else -> {
val ipRankingListStr = preferences.getString(KEY_IP_RANKING_ITEMS, null)
val rankingList = ipRankingListStr.toIPRankingList()
rankingList?.let {
ipRankingList.addAll(rankingList)
for (rankingItem in ipRankingList) {
infoList.add(
ListItem(
kListItemTypeIPRanking,
rankingItem.hostName,
rankingItem.port
)
)
}
}
}
}
}
}
fun toAddTag(tag: String, listAdapter: ListAdapter) {
tagsList.add(tag)
saveTags()
listAdapter.addItemData(
ListItem(
kListItemTag,
tag,
0
)
)
}
fun toAddHostWithFixedIP(host: String, listAdapter: ListAdapter) {
if (hostFixedIpList.contains(host)) {
Toast.makeText(
getApplication(),
getString(R.string.host_fixed_ip_duplicate, host),
Toast.LENGTH_SHORT
).show()
} else {
hostFixedIpList.add(host)
saveHostWithFixedIP()
listAdapter.addItemData(
ListItem(
kListItemTypeHostWithFixedIP,
host,
0
)
)
}
}
fun toAddHostInBlackList(host: String, listAdapter: ListAdapter) {
if (hostBlackList.contains(host)) {
Toast.makeText(
getApplication(),
getString(R.string.host_black_list_duplicate, host),
Toast.LENGTH_SHORT
).show()
} else {
hostBlackList.add(host)
saveHostInBlackList()
listAdapter.addItemData(
ListItem(
kListItemTypeBlackList,
host,
0
)
)
}
}
private fun saveTags() {
viewModelScope.launch {
val array = JSONArray()
for (tag in tagsList) {
array.put(tag)
}
val tagStr = array.toString()
val editor = preferences.edit()
editor.putString(KEY_TAGS, tagStr)
editor.apply()
}
}
private fun saveHostWithFixedIP() {
viewModelScope.launch {
val array = JSONArray()
for (host in hostFixedIpList) {
array.put(host)
}
val hostStr = array.toString()
val editor = preferences.edit()
editor.putString(KEY_HOST_WITH_FIXED_IP, hostStr)
editor.apply()
}
}
private fun saveHostInBlackList() {
viewModelScope.launch {
val array = JSONArray()
for (host in hostBlackList) {
array.put(host)
}
preferences.edit()
.putString(KEY_HOST_BLACK_LIST, array.toString())
.apply()
}
}
fun toSaveIPProbe(host: String, port: Int, listAdapter: ListAdapter) {
val ipProbeItem =
IPRankingBean(host, port)
if (ipRankingList.contains(ipProbeItem)) {
Toast.makeText(
getApplication(),
getString(R.string.ip_probe_item_duplicate, host, port.toString()),
Toast.LENGTH_SHORT
).show()
} else {
ipRankingList.add(ipProbeItem)
saveIPProbe()
listAdapter.addItemData(
ListItem(
kListItemTypeIPRanking,
host,
port
)
)
}
}
private fun saveIPProbe() {
viewModelScope.launch {
val jsonObject = JSONObject()
for (item in ipRankingList) {
try {
jsonObject.put(item.hostName, item.port)
} catch (e: JSONException) {
e.printStackTrace()
}
}
val ipProbeStr = jsonObject.toString()
val editor = preferences.edit()
editor.putString(KEY_IP_RANKING_ITEMS, ipProbeStr)
editor.apply()
}
}
fun toSaveTtlCache(host: String, ttl: Int, listAdapter: ListAdapter) {
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_TTL_CHANGER, TtlCacheHolder.ttlCache.toJsonString())
editor.apply()
}
if (TtlCacheHolder.ttlCache.containsKey(host)) {
val position = listAdapter.getPositionByContent(host)
if (position != -1) {
listAdapter.updateItemByPosition(host, ttl, position)
}
} else {
listAdapter.addItemData(
ListItem(kListItemTypeCacheTtl, host, ttl)
)
}
TtlCacheHolder.ttlCache[host] = ttl
}
fun toAddPreResolveHost(host: String, listAdapter: ListAdapter, type: Int) {
val list: MutableList<String> = when (type) {
0 -> PreResolveCacheHolder.preResolveV4List
1 -> PreResolveCacheHolder.preResolveV6List
2 -> PreResolveCacheHolder.preResolveBothList
else -> PreResolveCacheHolder.preResolveAutoList
}
if (list.contains(host)) {
Toast.makeText(
getApplication(),
getString(R.string.pre_resolve_host_duplicate, host),
Toast.LENGTH_SHORT
).show()
} else {
list.add(host)
savePreResolveHost()
listAdapter.addItemData(
ListItem(
kListItemPreResolve,
host,
type
)
)
}
}
fun toAddBatchResolveHost(host: String, listAdapter: ListAdapter, type: Int) {
val list: MutableList<String> = when (type) {
0 -> BatchResolveCacheHolder.batchResolveV4List
1 -> BatchResolveCacheHolder.batchResolveV6List
2 -> BatchResolveCacheHolder.batchResolveBothList
else -> BatchResolveCacheHolder.batchResolveAutoList
}
if (list.contains(host)) {
Toast.makeText(
getApplication(),
getString(R.string.batch_resolve_host_duplicate, host),
Toast.LENGTH_SHORT
).show()
} else {
list.add(host)
saveBatchResolveHost()
listAdapter.addItemData(
ListItem(
kListItemBatchResolve,
host,
type
)
)
}
}
private fun savePreResolveHost() {
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_PRE_RESOLVE_HOST_LIST, PreResolveCacheHolder.convertPreResolveString())
editor.apply()
}
}
private fun saveBatchResolveHost() {
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_BATCH_RESOLVE_HOST_LIST, BatchResolveCacheHolder.convertBatchResolveString())
editor.apply()
}
}
fun onTagDeleted(position: Int) {
tagsList.removeAt(position)
saveTags()
}
fun onHostWithFixedIPDeleted(position: Int) {
//鍙兘閲嶅惎鐢熸晥
val deletedHost = hostFixedIpList.removeAt(position)
saveHostWithFixedIP()
}
fun onIPProbeItemDeleted(position: Int) {
ipRankingList.removeAt(position)
saveIPProbe()
}
fun onTtlDeleted(host: String) {
TtlCacheHolder.ttlCache.remove(host)
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_TTL_CHANGER, TtlCacheHolder.ttlCache.toJsonString())
editor.apply()
}
}
fun onPreResolveDeleted(host: String, intValue: Int) {
val list = when (intValue) {
0 -> PreResolveCacheHolder.preResolveV4List
1 -> PreResolveCacheHolder.preResolveV6List
2 -> PreResolveCacheHolder.preResolveBothList
else -> PreResolveCacheHolder.preResolveAutoList
}
list.remove(host)
savePreResolveHost()
}
fun onBatchResolveDeleted(host: String, intValue: Int) {
val list = when (intValue) {
0 -> BatchResolveCacheHolder.batchResolveV4List
1 -> BatchResolveCacheHolder.batchResolveV6List
2 -> BatchResolveCacheHolder.batchResolveBothList
else -> BatchResolveCacheHolder.batchResolveAutoList
}
list.remove(host)
saveBatchResolveHost()
}
fun onHostBlackListDeleted(position: Int) {
hostBlackList.removeAt(position)
saveHostInBlackList()
}
private fun getString(resId: Int, vararg args: String): String {
return getApplication<HttpDnsApplication>().getString(resId, *args)
}
}

View File

@@ -0,0 +1,68 @@
package com.newsdk.ams.emas.demo.ui.practice
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.FragmentBestPracticeBinding
/**
* @author allen.wy
* @date 2023/6/14
*/
class BestPracticeFragment : Fragment(), IBestPracticeShowDialog {
private var _binding: FragmentBestPracticeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBestPracticeBinding.inflate(inflater, container, false)
val viewModel = ViewModelProvider(this)[BestPracticeViewModel::class.java]
viewModel.showDialog = this
binding.viewModel = viewModel
binding.openHttpdnsWebview.setOnClickListener {
val intent = Intent(activity, HttpDnsWebviewGetActivity::class.java)
startActivity(intent)
}
// binding.openHttpdnsWebviewPost.setOnClickListener {
// val intent = Intent(activity, HttpDnsWVWebViewActivity::class.java)
// startActivity(intent)
// }
return binding.root
}
override fun showResponseDialog(message: String) {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.sni_request)
setMessage(message)
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
builder?.show()
}
override fun showNoNetworkDialog() {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.tips)
setMessage(R.string.network_not_connect)
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
builder?.show()
}
}

View File

@@ -0,0 +1,127 @@
package com.newsdk.ams.emas.demo.ui.practice
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.newsdk.ams.emas.demo.HttpDnsServiceHolder
import com.newsdk.ams.emas.demo.net.TLSSNISocketFactory
import com.newsdk.ams.emas.demo.readStringFrom
import com.newsdk.sdk.android.httpdns.NetType
import com.newsdk.sdk.android.httpdns.RequestIpType
import com.newsdk.sdk.android.httpdns.net.HttpDnsNetworkDetector
import com.alibaba.sdk.android.tool.NetworkUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
/**
* @author allen.wy
* @date 2023/6/15
*/
class BestPracticeViewModel(application: Application) : AndroidViewModel(application) {
var showDialog: IBestPracticeShowDialog? = null
fun sniRequest() {
if (!NetworkUtils.isNetworkConnected(getApplication())) {
showDialog?.showNoNetworkDialog()
return
}
val testUrl = "https://suggest.taobao.com/sug?code=utf-8&q=phone"
viewModelScope.launch(Dispatchers.IO) {
recursiveRequest(testUrl) { message ->
withContext(Dispatchers.Main) {
showDialog?.showResponseDialog(
message
)
}
}
}
}
private suspend fun recursiveRequest(url: String, callback: suspend (message: String) -> Unit) {
val host = URL(url).host
var ipURL: String? = null
val dnsService = HttpDnsServiceHolder.getHttpDnsService(getApplication())
dnsService?.let {
val httpDnsResult = dnsService.getIpsByHostAsync(host, RequestIpType.both)
Log.d("httpdns", "$host 瑙f瀽缁撴灉 $httpDnsResult")
val ipStackType = HttpDnsNetworkDetector.getInstance().getNetType(getApplication())
val isV6 = ipStackType == NetType.v6 || ipStackType == NetType.both
val isV4 = ipStackType == NetType.v4 || ipStackType == NetType.both
if (httpDnsResult.ipv6s != null && httpDnsResult.ipv6s.isNotEmpty() && isV6) {
ipURL = url.replace(host, "[" + httpDnsResult.ipv6s[0] + "]")
} else if (httpDnsResult.ips != null && httpDnsResult.ips.isNotEmpty() && isV4) {
ipURL = url.replace(host, httpDnsResult.ips[0])
}
}
val conn: HttpsURLConnection =
URL(ipURL ?: url).openConnection() as HttpsURLConnection
conn.setRequestProperty("Host", host)
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
//璁剧疆SNI
val sslSocketFactory = TLSSNISocketFactory(conn)
// SNI鍦烘櫙锛屽垱寤篠SLSocket
conn.sslSocketFactory = sslSocketFactory
conn.hostnameVerifier = HostnameVerifier { _, session ->
val requestHost = conn.getRequestProperty("Host") ?: conn.url.host
HttpsURLConnection.getDefaultHostnameVerifier().verify(requestHost, session)
}
val code = conn.responseCode
if (needRedirect(code)) {
//涓存椂閲嶅畾鍚戝拰姘镐箙閲嶅畾鍚憀ocation鐨勫ぇ灏忓啓鏈夊尯鍒?
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (!(location!!.startsWith("http://") || location.startsWith("https://"))) {
//鏌愪簺鏃跺€欎細鐪佺暐host锛屽彧杩斿洖鍚庨潰鐨刾ath锛屾墍浠ラ渶瑕佽ˉ鍏rl
val originalUrl = URL(url)
location = (originalUrl.protocol + "://"
+ originalUrl.host + location)
}
recursiveRequest(location, callback)
} else {
val inputStream: InputStream?
val streamReader: BufferedReader?
if (code != HttpURLConnection.HTTP_OK) {
inputStream = conn.errorStream
var errMsg: String? = null
if (inputStream != null) {
streamReader = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
errMsg = readStringFrom(streamReader).toString()
}
Log.d("httpdns", "SNI request error: $errMsg")
callback("$code - $errMsg")
} else {
inputStream = conn.inputStream
streamReader = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
val body: String = readStringFrom(streamReader).toString()
Log.d("httpdns", "SNI request response: $body")
callback("$code - $body")
}
}
}
private fun needRedirect(code: Int): Boolean {
return code in 300..399
}
}

View File

@@ -0,0 +1,199 @@
package com.newsdk.ams.emas.demo.ui.practice
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.MenuItem
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import com.newsdk.ams.emas.demo.HttpDnsServiceHolder
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.ActivityHttpDnsWebviewBinding
import java.io.IOException
import java.net.*
import javax.net.ssl.*
class HttpDnsWebviewGetActivity : AppCompatActivity() {
private lateinit var binding: ActivityHttpDnsWebviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHttpDnsWebviewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webviewToolbar.title = getString(R.string.httpdns_webview_best_practice)
setSupportActionBar(binding.webviewToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)//娣诲姞榛樿鐨勮繑鍥炲浘鏍?
supportActionBar?.setHomeButtonEnabled(true)
binding.httpdnsWebview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url.toString()
val schema = request?.url?.scheme?.trim()
val method = request?.method
if ("get" != method && "GET" != method) {
return super.shouldInterceptRequest(view, request)
}
schema?.let {
if (!schema.startsWith("https") && !schema.startsWith("http")) {
return super.shouldInterceptRequest(view, request)
}
val headers = request.requestHeaders
try {
val urlConnection = recursiveRequest(url, headers)
?: return super.shouldInterceptRequest(view, request)
val contentType = urlConnection.contentType
val mimeType = contentType?.split(";")?.get(0)
if (TextUtils.isEmpty(mimeType)) {
//鏃爉imeType寰楄姹備笉鎷︽埅
return super.shouldInterceptRequest(view, request)
}
val charset = getCharset(contentType)
val httpURLConnection = urlConnection as HttpURLConnection
val statusCode = httpURLConnection.responseCode
var response = httpURLConnection.responseMessage
val headerFields = httpURLConnection.headerFields
val isBinaryResource =
mimeType!!.startsWith("image") || mimeType.startsWith("audio") || mimeType.startsWith(
"video"
)
if (!TextUtils.isEmpty(charset) || isBinaryResource) {
val resourceResponse = WebResourceResponse(
mimeType,
charset,
httpURLConnection.inputStream
)
if (TextUtils.isEmpty(response)) {
response = "OK"
}
resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response)
val responseHeader: MutableMap<String?, String> = HashMap()
for ((key) in headerFields) {
// HttpUrlConnection鍙兘鍖呭惈key涓簄ull鐨勬姤澶达紝鎸囧悜璇ttp璇锋眰鐘舵€佺爜
responseHeader[key] = httpURLConnection.getHeaderField(key)
}
resourceResponse.responseHeaders = responseHeader
return resourceResponse
} else {
return super.shouldInterceptRequest(view, request)
}
} catch (e: Exception) {
Log.e("httpdns", Log.getStackTraceString(e))
}
}
return super.shouldInterceptRequest(view, request)
}
}
binding.httpdnsWebview.loadUrl("https://demo.cloudxdr.com")
}
private fun getCharset(contentType: String?): String? {
if (contentType == null) {
return null
}
val fields = contentType.split(";")
if (fields.size <= 1) {
return null
}
var charset = fields[1]
if (!charset.contains("=")) {
return null
}
charset = charset.substring(charset.indexOf("=") + 1)
return charset
}
private fun recursiveRequest(path: String, headers: Map<String, String>?): URLConnection? {
try {
val url = URL(path)
val httpdnsService = HttpDnsServiceHolder.getHttpDnsService(this@HttpDnsWebviewGetActivity)
?: return null
val hostIP: String? = httpdnsService.getIpByHostAsync(url.host) ?: return null
val newUrl = if (hostIP == null) path else path.replaceFirst(url.host, hostIP)
val urlConnection: HttpURLConnection = URL(newUrl).openConnection() as HttpURLConnection
if (headers != null) {
for ((key, value) in headers) {
urlConnection.setRequestProperty(key, value)
}
}
urlConnection.setRequestProperty("Host", url.host)
urlConnection.connectTimeout = 30000
urlConnection.readTimeout = 30000
urlConnection.instanceFollowRedirects = false
if (urlConnection is HttpsURLConnection) {
val sniFactory = SNISocketFactory(urlConnection)
urlConnection.sslSocketFactory = sniFactory
urlConnection.hostnameVerifier = HostnameVerifier { _, session ->
var host: String? = urlConnection.getRequestProperty("Host")
if (null == host) {
host = urlConnection.getURL().host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
}
val responseCode = urlConnection.responseCode
if (responseCode in 300..399) {
if (containCookie(headers)) {
return null
}
var location: String? = urlConnection.getHeaderField("Location")
if (location == null) {
location = urlConnection.getHeaderField("location")
}
return if (location != null) {
if (!(location.startsWith("http://") || location.startsWith("https://"))) {
//鏌愪簺鏃跺€欎細鐪佺暐host锛屽彧杩斿洖鍚庨潰鐨刾ath锛屾墍浠ラ渶瑕佽ˉ鍏rl
val originalUrl = URL(path)
location = (originalUrl.protocol + "://" + originalUrl.host + location)
}
recursiveRequest(location, headers)
} else {
null
}
} else {
return urlConnection
}
} catch (e: MalformedURLException) {
Log.e("httpdns", Log.getStackTraceString(e))
} catch (e: IOException) {
Log.e("httpdns", Log.getStackTraceString(e))
}
return null
}
private fun containCookie(headers: Map<String, String>?): Boolean {
if (headers == null) {
return false
}
for ((key) in headers) {
if (key.contains("Cookie")) {
return true
}
}
return false
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -0,0 +1,12 @@
package com.newsdk.ams.emas.demo.ui.practice
/**
* @author allen.wy
* @date 2023/6/15
*/
interface IBestPracticeShowDialog {
fun showResponseDialog( message: String)
fun showNoNetworkDialog()
}

View File

@@ -0,0 +1,82 @@
package com.newsdk.ams.emas.demo.ui.practice
import android.net.SSLCertificateSocketFactory
import java.net.InetAddress
import java.net.Socket
import javax.net.ssl.*
/**
* @author allen.wy
* @date 2023/6/14
*/
class SNISocketFactory(private val conn: HttpsURLConnection) : SSLSocketFactory() {
private val hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
override fun createSocket(
plainSocket: Socket?,
host: String?,
port: Int,
autoClose: Boolean
): Socket {
var peerHost: String? = conn.getRequestProperty("Host")
if (peerHost == null) {
peerHost = host
}
val address = plainSocket?.inetAddress
if (autoClose) {
plainSocket?.close()
}
val sslSocketFactory: SSLCertificateSocketFactory =
SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
val ssl: SSLSocket =
sslSocketFactory.createSocket(address, port) as SSLSocket
ssl.enabledProtocols = ssl.supportedProtocols
// set up SNI before the handshake
sslSocketFactory.setHostname(ssl, peerHost)
// verify hostname and certificate
val session: SSLSession = ssl.session
if (!hostnameVerifier.verify(peerHost, session)
) throw SSLPeerUnverifiedException("Cannot verify hostname: $peerHost")
return ssl
}
override fun createSocket(host: String?, port: Int): Socket? {
return null
}
override fun createSocket(
host: String?,
port: Int,
localHost: InetAddress?,
localPort: Int
): Socket? {
return null
}
override fun createSocket(host: InetAddress?, port: Int): Socket? {
return null
}
override fun createSocket(
address: InetAddress?,
port: Int,
localAddress: InetAddress?,
localPort: Int
): Socket? {
return null
}
override fun getDefaultCipherSuites(): Array<String> {
return arrayOf()
}
override fun getSupportedCipherSuites(): Array<String> {
return arrayOf()
}
}

View File

@@ -0,0 +1,18 @@
package com.newsdk.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/5/26
*/
interface IResolveShowDialog {
fun showSelectResolveIpTypeDialog()
fun showRequestResultDialog(response: Response)
fun showRequestFailedDialog(e: Throwable)
fun showResolveMethodDialog()
fun showRequestNumberDialog()
}

View File

@@ -0,0 +1,12 @@
package com.newsdk.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/5/26
*/
enum class NetRequestType {
OKHTTP,
HTTP_URL_CONNECTION
}

View File

@@ -0,0 +1,233 @@
package com.newsdk.ams.emas.demo.ui.resolve
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.newsdk.ams.emas.demo.constant.KEY_RESOLVE_IP_TYPE
import com.newsdk.ams.emas.demo.constant.KEY_RESOLVE_METHOD
import com.newsdk.ams.emas.demo.getAccountPreference
import com.newsdk.ams.httpdns.demo.R
import com.newsdk.ams.httpdns.demo.databinding.FragmentResolveBinding
import org.json.JSONException
import org.json.JSONObject
class ResolveFragment : Fragment(), IResolveShowDialog {
private var _binding: FragmentResolveBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: ResolveViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[ResolveViewModel::class.java]
viewModel.showDialog = this
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentResolveBinding.inflate(inflater, container, false)
viewModel.initData()
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.sdnsParamsInputLayout.visibility = if (viewModel.isSdns.value!!) {
View.VISIBLE
} else {
View.GONE
}
binding.sdnsCacheKeyInputLayout.visibility = if (viewModel.isSdns.value!!) {
View.VISIBLE
} else {
View.GONE
}
binding.enableSdnsResolve.setOnCheckedChangeListener{_, isChecked ->
viewModel.toggleSdns(isChecked)
binding.sdnsParamsInputLayout.visibility = if (viewModel.isSdns.value!!) {
View.VISIBLE
} else {
View.GONE
}
binding.sdnsCacheKeyInputLayout.visibility = if (viewModel.isSdns.value!!) {
View.VISIBLE
} else {
View.GONE
}
}
binding.startResolve.setOnClickListener {
binding.resolveHostInputLayout.error = ""
//1. 鏍¢獙鍩熷悕鏄惁濉啓
val host = binding.resolveHostInputLayout.editText?.text.toString()
if (TextUtils.isEmpty(host)) {
binding.resolveHostInputLayout.error = getString(R.string.resolve_host_empty)
return@setOnClickListener
}
var sdnsParams: MutableMap<String, String>? = null
//2. 鏍¢獙sdns鍙傛暟
if (viewModel.isSdns.value!!) {
val sdnsParamsStr = binding.sdnsParamsInputLayout.editText?.text.toString()
if (!TextUtils.isEmpty(sdnsParamsStr)) {
try {
val sdnsJson = JSONObject(sdnsParamsStr)
val keys = sdnsJson.keys()
sdnsParams = HashMap()
while (keys.hasNext()) {
val key = keys.next()
sdnsParams[key] = sdnsJson.getString(key)
}
} catch (e: JSONException) {
binding.sdnsParamsInputLayout.error = getString(R.string.input_the_sdns_params_error)
}
}
}
var api = binding.requestApiInputLayout.editText?.text.toString()
val cacheKey = binding.sdnsCacheKeyInputLayout.editText?.text.toString()
if (!api.startsWith("/")) {
api = "/$api"
}
var index: Int = 0
do {
viewModel.startToResolve(host, api, sdnsParams, cacheKey)
++index
} while (index < viewModel.requestNum.value!!)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun showSelectResolveIpTypeDialog() {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.select_resolve_ip_type)
val items = arrayOf("IPv4", "IPv6", "IPv4&IPv6", getString(R.string.auto_get_ip_type))
val preferences = activity?.let { getAccountPreference(it) }
val index = when (preferences?.getString(KEY_RESOLVE_IP_TYPE, "IPv4")) {
"IPv4" -> 0
"IPv6" -> 1
"IPv4&IPv6" -> 2
else -> 3
}
var resolvedIpType = "IPv4"
setSingleChoiceItems(items, index) { _, which ->
resolvedIpType = when (which) {
0 -> "IPv4"
1 -> "IPv6"
2 -> "IPv4&IPv6"
else -> "Auto"
}
}
setPositiveButton(getString(R.string.confirm)) { dialog, _ ->
viewModel.saveResolveIpType(resolvedIpType)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}
builder?.show()
}
override fun showRequestResultDialog(response: Response) {
val code = response.code
val body = response.body
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.response_title)
val message = if (code == 200 && !TextUtils.isEmpty(body)) {
if (body!!.length <= 100) "$code - $body" else "$code - ${getString(R.string.body_large_see_log)}"
} else {
code.toString()
}
setMessage(message)
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
builder?.show()
}
override fun showRequestFailedDialog(e: Throwable) {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.response_title)
setMessage(getString(R.string.request_exception, e.message))
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
builder?.show()
}
override fun showResolveMethodDialog() {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.select_resolve_method)
val items = arrayOf("Sync", "Async", "Sync NonBlocking")
val preferences = activity?.let { getAccountPreference(it) }
var resolvedMethod = preferences?.getString(KEY_RESOLVE_METHOD, "getHttpDnsResultForHostSync(String host, RequestIpType type)").toString()
val index = when (resolvedMethod) {
"getHttpDnsResultForHostSync(String host, RequestIpType type)" -> 0
"getHttpDnsResultForHostAsync(String host, RequestIpType type, HttpDnsCallback callback)" -> 1
"getHttpDnsResultForHostSyncNonBlocking(String host, RequestIpType type)" -> 2
else -> 3
}
setSingleChoiceItems(items, index) { _, which ->
resolvedMethod = when (which) {
0 -> "getHttpDnsResultForHostSync(String host, RequestIpType type)"
1 -> "getHttpDnsResultForHostAsync(String host, RequestIpType type, HttpDnsCallback callback)"
2 -> "getHttpDnsResultForHostSyncNonBlocking(String host, RequestIpType type)"
else -> "getHttpDnsResultForHostSync(String host, RequestIpType type)"
}
}
setPositiveButton(getString(R.string.confirm)) { dialog, _ ->
viewModel.saveResolveMethod(resolvedMethod)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}
builder?.show()
}
override fun showRequestNumberDialog() {
val builder = activity?.let { act -> AlertDialog.Builder(act) }
builder?.apply {
setTitle(R.string.select_request_num)
val items = arrayOf("1", "2", "3", "4", "5")
val index = viewModel.requestNum.value!! - 1
var num = viewModel.requestNum.value
setSingleChoiceItems(items, index) { _, which ->
num = which + 1
}
setPositiveButton(getString(R.string.confirm)) { dialog, _ ->
viewModel.saveRequestNumber(num!!)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}
builder?.show()
}
}

View File

@@ -0,0 +1,153 @@
package com.newsdk.ams.emas.demo.ui.resolve
import android.app.Application
import android.util.Log
import android.widget.RadioGroup
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.newsdk.ams.emas.demo.HttpDnsApplication
import com.newsdk.ams.emas.demo.SingleLiveData
import com.newsdk.ams.emas.demo.constant.KEY_RESOLVE_IP_TYPE
import com.newsdk.ams.emas.demo.constant.KEY_RESOLVE_METHOD
import com.newsdk.ams.emas.demo.constant.KEY_SDNS_RESOLVE
import com.newsdk.ams.emas.demo.getAccountPreference
import com.newsdk.ams.emas.demo.net.HttpURLConnectionRequest
import com.newsdk.ams.emas.demo.net.OkHttpRequest
import com.newsdk.sdk.android.httpdns.RequestIpType
import com.newsdk.ams.httpdns.demo.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ResolveViewModel(application: Application) : AndroidViewModel(application) {
private val preferences = getAccountPreference(getApplication())
val currentIpType = SingleLiveData<String>().apply {
value = "IPv4"
}
val requestNum = SingleLiveData<Int>().apply {
value = 1
}
val currentResolveMethod = SingleLiveData<String>().apply {
value = "getHttpDnsResultForHostSync(String host, RequestIpType type)"
}
val isSdns = SingleLiveData<Boolean>().apply {
value = false
}
var showDialog:IResolveShowDialog? = null
private var requestType: NetRequestType = NetRequestType.OKHTTP
private var schemaType: SchemaType = SchemaType.HTTPS
fun initData() {
isSdns.value = preferences.getBoolean(KEY_SDNS_RESOLVE, false)
val ipType = preferences.getString(KEY_RESOLVE_IP_TYPE, "IPv4")
currentIpType.value = when(ipType) {
"Auto" -> getApplication<HttpDnsApplication>().getString(R.string.auto_get_ip_type)
else -> ipType
}
currentResolveMethod.value = preferences.getString(KEY_RESOLVE_METHOD, "getHttpDnsResultForHostSync(String host, RequestIpType type)")
requestNum.value = 1
}
fun onNetRequestTypeChanged(radioGroup: RadioGroup, id: Int) {
requestType = when(id) {
R.id.http_url_connection -> NetRequestType.HTTP_URL_CONNECTION
else -> NetRequestType.OKHTTP
}
}
fun toggleSdns(checked: Boolean) {
isSdns.value = checked
viewModelScope.launch {
val editor = preferences.edit()
editor.putBoolean(KEY_SDNS_RESOLVE, checked)
editor.apply()
}
}
fun onSchemaTypeChanged(radioGroup: RadioGroup, id: Int) {
schemaType = when(id) {
R.id.schema_http -> SchemaType.HTTP
else -> SchemaType.HTTPS
}
}
fun setResolveIpType() {
showDialog?.showSelectResolveIpTypeDialog()
}
fun setResolveMethod() {
showDialog?.showResolveMethodDialog()
}
fun setRequestNumber() {
showDialog?.showRequestNumberDialog()
}
fun saveResolveIpType(ipType: String) {
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_RESOLVE_IP_TYPE, ipType)
editor.apply()
}
currentIpType.value = when (ipType) {
"Auto" -> getApplication<HttpDnsApplication>().getString(R.string.auto_get_ip_type)
else -> ipType
}
}
fun saveResolveMethod(resolveMethod: String) {
viewModelScope.launch {
val editor = preferences.edit()
editor.putString(KEY_RESOLVE_METHOD, resolveMethod)
editor.apply()
}
currentResolveMethod.value = resolveMethod
}
fun saveRequestNumber(num: Int) {
requestNum.value = num
}
fun startToResolve(host: String, api: String, sdnsParams: Map<String, String>?, cacheKey: String) {
val requestUrl = if (schemaType == SchemaType.HTTPS) "https://$host$api" else "http://$host$api"
val requestIpType = when (currentIpType.value) {
"IPv4" -> RequestIpType.v4
"IPv6" -> RequestIpType.v6
"IPv4&IPv6" -> RequestIpType.both
else -> RequestIpType.auto
}
Log.d("httpdns", "api: ${currentResolveMethod.value}, " + "requestIp: $requestIpType")
val requestClient = if (requestType == NetRequestType.OKHTTP) OkHttpRequest(getApplication(), requestIpType,
currentResolveMethod.value!!, isSdns.value!!, sdnsParams, cacheKey
) else HttpURLConnectionRequest(getApplication(), requestIpType, currentResolveMethod.value!!, isSdns.value!!, sdnsParams, cacheKey)
viewModelScope.launch(Dispatchers.IO) {
try {
Log.d("httpdns", "before request $requestUrl");
val response = requestClient.get(requestUrl)
withContext(Dispatchers.Main) {
showDialog?.showRequestResultDialog(response)
}
} catch (e: Exception) {
Log.e("httpdns", Log.getStackTraceString(e))
withContext(Dispatchers.Main) {
showDialog?.showRequestFailedDialog(e)
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
package com.newsdk.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/6/14
*/
data class Response(val code: Int, val body: String?)

View File

@@ -0,0 +1,12 @@
package com.newsdk.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/6/7
*/
enum class SchemaType {
HTTPS,
HTTP
}

View File

@@ -0,0 +1,364 @@
package com.newsdk.ams.emas.demo.widget
import android.content.Context
import android.graphics.PointF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Scroller
import com.newsdk.ams.httpdns.demo.R
import java.lang.ref.WeakReference
import kotlin.math.abs
/**
* @author allen.wy
* @date 2023/6/5
*/
class SwipeLayout(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
ViewGroup(context, attrs, defStyleAttr) {
private val mMatchParentChildren = mutableListOf<View>()
private var menuViewResId = 0
private var contentViewResId = 0
private var menuView: View? = null
private var contentView: View? = null
private var contentViewLayoutParam: MarginLayoutParams? = null
private var isSwiping = false
private var lastP: PointF? = null
private var firstP: PointF? = null
private var fraction = 0.2f
private var scaledTouchSlop = 0
private var scroller: Scroller? = null
private var finalDistanceX = 0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
/**
* 鍒濆鍖栨柟娉?
*
* @param context
* @param attrs
* @param defStyleAttr
*/
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
//鍒涘缓杈呭姪瀵硅薄
val viewConfiguration = ViewConfiguration.get(context)
scaledTouchSlop = viewConfiguration.scaledTouchSlop
scroller = Scroller(context)
//1銆佽幏鍙栭厤缃殑灞炴€у€?
val typedArray = context.theme
.obtainStyledAttributes(attrs, R.styleable.SwipeLayout, defStyleAttr, 0)
try {
val indexCount: Int = typedArray.indexCount
for (i in 0 until indexCount) {
when (typedArray.getIndex(i)) {
R.styleable.SwipeLayout_menuView -> {
menuViewResId =
typedArray.getResourceId(R.styleable.SwipeLayout_menuView, -1)
}
R.styleable.SwipeLayout_contentView -> {
contentViewResId =
typedArray.getResourceId(R.styleable.SwipeLayout_contentView, -1)
}
R.styleable.SwipeLayout_fraction -> {
fraction = typedArray.getFloat(R.styleable.SwipeLayout_fraction, 0.5f)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
typedArray.recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//鑾峰彇childView鐨勪釜鏁?
isClickable = true
var count = childCount
val measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY
mMatchParentChildren.clear()
var maxHeight = 0
var maxWidth = 0
var childState = 0
for (i in 0 until count) {
val child: View = getChildAt(i)
if (child.visibility != View.GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
val lp = child.layoutParams as MarginLayoutParams
maxWidth =
maxWidth.coerceAtLeast(child.measuredWidth + lp.leftMargin + lp.rightMargin)
maxHeight =
maxHeight.coerceAtLeast(child.measuredHeight + lp.topMargin + lp.bottomMargin)
childState = combineMeasuredStates(childState, child.measuredState)
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT
) {
mMatchParentChildren.add(child)
}
}
}
}
// Check against our minimum height and width
maxHeight = maxHeight.coerceAtLeast(suggestedMinimumHeight)
maxWidth = maxWidth.coerceAtLeast(suggestedMinimumWidth)
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(
maxHeight, heightMeasureSpec,
childState shl MEASURED_HEIGHT_STATE_SHIFT
)
)
count = mMatchParentChildren.size
if (count < 1) {
return
}
for (i in 0 until count) {
val child: View = mMatchParentChildren[i]
val lp = child.layoutParams as MarginLayoutParams
val childWidthMeasureSpec = if (lp.width == LayoutParams.MATCH_PARENT) {
val width = 0.coerceAtLeast(
measuredWidth - lp.leftMargin - lp.rightMargin
)
MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY
)
} else {
getChildMeasureSpec(
widthMeasureSpec,
lp.leftMargin + lp.rightMargin,
lp.width
)
}
val childHeightMeasureSpec = if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
val height = 0.coerceAtLeast(
measuredHeight - lp.topMargin - lp.bottomMargin
)
MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY
)
} else {
getChildMeasureSpec(
heightMeasureSpec,
lp.topMargin + lp.bottomMargin,
lp.height
)
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val count = childCount
val left = 0 + paddingLeft
val top = 0 + paddingTop
for (i in 0 until count) {
val child: View = getChildAt(i)
if (menuView == null && child.id == menuViewResId) {
menuView = child
menuView!!.isClickable = true
} else if (contentView == null && child.id == contentViewResId) {
contentView = child
contentView!!.isClickable = true
}
}
//甯冨眬contentView
val cRight: Int
if (contentView != null) {
contentViewLayoutParam = contentView!!.layoutParams as MarginLayoutParams?
val cTop = top + contentViewLayoutParam!!.topMargin
val cLeft = left + contentViewLayoutParam!!.leftMargin
cRight = left + contentViewLayoutParam!!.leftMargin + contentView!!.measuredWidth
val cBottom: Int = cTop + contentView!!.measuredHeight
contentView!!.layout(cLeft, cTop, cRight, cBottom)
}
if (menuView != null) {
val rightViewLp = menuView!!.layoutParams as MarginLayoutParams
val lTop = top + rightViewLp.topMargin
val lLeft =
contentView!!.right + contentViewLayoutParam!!.rightMargin + rightViewLp.leftMargin
val lRight: Int = lLeft + menuView!!.measuredWidth
val lBottom: Int = lTop + menuView!!.measuredHeight
menuView!!.layout(lLeft, lTop, lRight, lBottom)
}
}
private var result: State? = null
init {
init(context, attrs, defStyleAttr)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
isSwiping = false
if (lastP == null) {
lastP = PointF()
}
lastP!!.set(ev.rawX, ev.rawY)
if (firstP == null) {
firstP = PointF()
}
firstP!!.set(ev.rawX, ev.rawY)
if (viewCache != null) {
if (viewCache!!.get() != this) {
viewCache!!.get()!!.handlerSwipeMenu(State.CLOSE)
}
parent.requestDisallowInterceptTouchEvent(true)
}
}
MotionEvent.ACTION_MOVE -> run {
val distanceX: Float = lastP!!.x - ev.rawX
val distanceY: Float = lastP!!.y - ev.rawY
if (abs(distanceY) > scaledTouchSlop && abs(distanceY) > abs(distanceX)) {
return@run
}
scrollBy(distanceX.toInt(), 0)
//瓒婄晫淇
if (scrollX < 0) {
scrollTo(0, 0)
} else if (scrollX > 0) {
if (scrollX > menuView!!.right - contentView!!.right - contentViewLayoutParam!!.rightMargin) {
scrollTo(
menuView!!.right - contentView!!.right - contentViewLayoutParam!!.rightMargin,
0
)
}
}
//褰撳浜庢按骞虫粦鍔ㄦ椂锛岀姝㈢埗绫绘嫤鎴?
if (abs(distanceX) > scaledTouchSlop) {
parent.requestDisallowInterceptTouchEvent(true)
}
lastP!!.set(ev.rawX, ev.rawY)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
finalDistanceX = firstP!!.x - ev.rawX
if (abs(finalDistanceX) > scaledTouchSlop) {
isSwiping = true
}
result = isShouldOpen()
handlerSwipeMenu(result)
}
else -> {}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {
//婊戝姩鏃舵嫤鎴偣鍑绘椂闂?
if (abs(finalDistanceX) > scaledTouchSlop) {
return true
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
//婊戝姩鍚庝笉瑙﹀彂contentView鐨勭偣鍑讳簨浠?
if (isSwiping) {
isSwiping = false
finalDistanceX = 0f
return true
}
}
}
return super.onInterceptTouchEvent(event)
}
/**
* 鑷姩璁剧疆鐘舵€?
*
* @param result
*/
private fun handlerSwipeMenu(result: State?) {
if (result === State.RIGHT_OPEN) {
viewCache = WeakReference(this)
scroller!!.startScroll(
scrollX,
0,
menuView!!.right - contentView!!.right - contentViewLayoutParam!!.rightMargin - scrollX,
0
)
mStateCache = result
} else {
scroller!!.startScroll(scrollX, 0, -scrollX, 0)
viewCache = null
mStateCache = null
}
invalidate()
}
override fun computeScroll() {
//鍒ゆ柇Scroller鏄惁鎵ц瀹屾瘯锛?
if (scroller!!.computeScrollOffset()) {
scrollTo(scroller!!.currX, scroller!!.currY)
invalidate()
}
}
/**
* 鏍规嵁褰撳墠鐨剆crollX鐨勫€煎垽鏂澗寮€鎵嬪悗搴斿浜庝綍绉嶇姸鎬?
*
* @param
* @param scrollX
* @return
*/
private fun isShouldOpen(): State? {
if (scaledTouchSlop >= abs(finalDistanceX)) {
return mStateCache
}
if (finalDistanceX < 0) {
//鍏抽棴鍙宠竟
if (scrollX > 0 && menuView != null) {
return State.CLOSE
}
} else if (finalDistanceX > 0) {
//寮€鍚彸杈?
if (scrollX > 0 && menuView != null) {
if (abs(menuView!!.width * fraction) < abs(scrollX)) {
return State.RIGHT_OPEN
}
}
}
return State.CLOSE
}
override fun onDetachedFromWindow() {
if (this == viewCache?.get()) {
viewCache!!.get()!!.handlerSwipeMenu(State.CLOSE)
}
super.onDetachedFromWindow()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (this == viewCache?.get()) {
viewCache!!.get()!!.handlerSwipeMenu(mStateCache)
}
}
companion object {
var viewCache: WeakReference<SwipeLayout>? = null
private set
private var mStateCache: State? = null
}
enum class State {
RIGHT_OPEN, CLOSE
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M548.6,170.7v304.8H853.3v73.1H548.5L548.6,853.3h-73.1l-0,-304.8H170.7v-73.1h304.8V170.7h73.1z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M384,512L731.7,202.7c17.1,-14.9 19.2,-42.7 4.3,-59.7 -14.9,-17.1 -42.7,-19.2 -59.7,-4.3l-384,341.3c-10.7,8.5 -14.9,19.2 -14.9,32s4.3,23.5 14.9,32l384,341.3c8.5,6.4 19.2,10.7 27.7,10.7 12.8,0 23.5,-4.3 32,-14.9 14.9,-17.1 14.9,-44.8 -4.3,-59.7L384,512z"
android:fillColor="#666666"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M731.7,480l-384,-341.3c-17.1,-14.9 -44.8,-14.9 -59.7,4.3 -14.9,17.1 -14.9,44.8 4.3,59.7L640,512 292.3,821.3c-17.1,14.9 -19.2,42.7 -4.3,59.7 8.5,8.5 19.2,14.9 32,14.9 10.7,0 19.2,-4.3 27.7,-10.7l384,-341.3c8.5,-8.5 14.9,-19.2 14.9,-32s-4.3,-23.5 -14.9,-32z"
android:fillColor="#666666"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M672,896c-8.5,0 -17.1,-2.1 -21.3,-8.5l-362.7,-352c-6.4,-6.4 -10.7,-14.9 -10.7,-23.5 0,-8.5 4.3,-17.1 10.7,-23.5L652.8,136.5c12.8,-12.8 32,-12.8 44.8,0s12.8,32 0,44.8L356.3,512l339.2,328.5c12.8,12.8 12.8,32 0,44.8 -6.4,8.5 -14.9,10.7 -23.5,10.7z"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M853.3,554.7l-682.7,0c-23.5,0 -42.7,19.2 -42.7,42.7l0,256c0,23.5 19.2,42.7 42.7,42.7l682.7,0c23.5,0 42.7,-19.2 42.7,-42.7l0,-256c0,-23.5 -19.2,-42.7 -42.7,-42.7zM298.7,810.7c-47.1,0 -85.3,-38.2 -85.3,-85.3s38.2,-85.3 85.3,-85.3 85.3,38.2 85.3,85.3 -38.2,85.3 -85.3,85.3zM853.3,128l-682.7,0c-23.5,0 -42.7,19.2 -42.7,42.7l0,256c0,23.5 19.2,42.7 42.7,42.7l682.7,0c23.5,0 42.7,-19.2 42.7,-42.7l0,-256c0,-23.5 -19.2,-42.7 -42.7,-42.7zM298.7,384c-47.1,0 -85.3,-38.2 -85.3,-85.3s38.2,-85.3 85.3,-85.3 85.3,38.2 85.3,85.3 -38.2,85.3 -85.3,85.3z"
android:fillColor="#FF000000"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More