阿里sdk

This commit is contained in:
Robin
2026-02-20 17:56:24 +08:00
parent 39524692e5
commit f3af234308
524 changed files with 58345 additions and 0 deletions

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.

26
EdgeHttpDNS/sdk/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
.DS_Store
*.swp
# Kiro IDE
.kiro/
# VSCode
.vscode/
# Gradle
build
.gradle/
target/
# Intellij project files
*.iml
*.ipr
*.iws
.idea/
# sonar
.sonar/
# Android
local.properties
nohup.out

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,148 @@
# Alicloud HTTPDNS Android SDK
面向 Android 的 HTTP/HTTPS DNS 解析 SDK提供鉴权与可选 AES 加密、IPv4/IPv6 双栈解析、缓存与调度、预解析等能力。最低支持 Android API 19Android 4.4)。
## 功能特性
- 鉴权请求与可选 AES 传输加密
- IPv4/IPv6 双栈解析,支持自动/同时解析
- 内存 + 持久化缓存与 TTL 控制,可选择复用过期 IP
- 预解析、区域路由、网络切换自动刷新
- 可定制日志回调与会话追踪 `sessionId`
## 安装Gradle
在项目的 `build.gradle` 中添加:
```groovy
dependencies {
implementation 'com.aliyun.ams:alicloud-android-httpdns:2.6.7'
}
```
请访问 [Android SDK发布说明](https://help.aliyun.com/document_detail/435251.html) 查看最新版本号。
## 快速开始
### Java
```java
import com.alibaba.sdk.android.httpdns.HttpDns;
import com.alibaba.sdk.android.httpdns.HttpDnsService;
import com.alibaba.sdk.android.httpdns.InitConfig;
import com.alibaba.sdk.android.httpdns.RequestIpType;
// 初始化配置
new InitConfig.Builder()
.setContext(context)
.setSecretKey("YOUR_SECRET_KEY")
.setEnableExpiredIp(true) // 允许返回过期 IP
.buildFor("YOUR_ACCOUNT_ID");
// 获取实例
HttpDnsService httpDns = HttpDns.getService("YOUR_ACCOUNT_ID");
// 预解析热点域名
httpDns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com")));
// 解析域名
HTTPDNSResult result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto);
String[] ips = result.getIps();
```
### Kotlin
```kotlin
import com.alibaba.sdk.android.httpdns.HttpDns
import com.alibaba.sdk.android.httpdns.InitConfig
import com.alibaba.sdk.android.httpdns.RequestIpType
// 初始化配置
InitConfig.Builder()
.setContext(context)
.setSecretKey("YOUR_SECRET_KEY")
.setEnableExpiredIp(true) // 允许返回过期 IP
.buildFor("YOUR_ACCOUNT_ID")
// 获取实例
val httpDns = HttpDns.getService("YOUR_ACCOUNT_ID")
// 预解析热点域名
httpDns.setPreResolveHosts(arrayListOf("www.aliyun.com"))
// 解析域名
val result = httpDns.getHttpDnsResultForHostSyncNonBlocking("www.aliyun.com", RequestIpType.auto)
val ips = result.ips
```
### 提示
- 启动时通过 `setPreResolveHosts()` 预热热点域名
- 如需在刷新期间容忍 TTL 过期,可开启 `setEnableExpiredIp(true)`
- 使用 `getSessionId()` 并与选用 IP 一同记录,便于排障
## 源码构建
```bash
./gradlew clean :httpdns-sdk:assembleRelease
```
构建产物位于 `httpdns-sdk/build/outputs/aar/` 目录。
### 版本说明
项目使用 `productFlavors` 区分不同版本:
- `normal`:中国大陆版本
- `intl`:国际版本
- `end2end`:用于单元测试
## 测试
### 运行单元测试
```bash
./gradlew clean :httpdns-sdk:testEnd2endForTestUnitTest
```
### Demo 应用
SDK 提供了两个 Demo
#### 1. app module旧版 Demo
`MyApp.java` 中配置测试账号:
```java
private HttpDnsHolder holderA = new HttpDnsHolder("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret");
private HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null);
```
> 两个实例用于测试实例间互不影响,体验时只需配置一个
#### 2. demo module推荐
使用 Kotlin + MVVM 开发,功能更丰富。在 `demo/build.gradle` 中配置测试账号:
```groovy
buildConfigField "String", "ACCOUNT_ID", "\"请替换为测试用实例的accountId\""
buildConfigField "String", "SECRET_KEY", "\"请替换为测试用实例的secret\""
buildConfigField "String", "AES_SECRET_KEY", "\"请替换为测试用实例的aes\""
```
## 依赖与要求
- Android API 19+Android 4.4+
- 需要权限:`INTERNET``ACCESS_NETWORK_STATE`
## 安全说明
- 切勿提交真实的 AccountID/SecretKey请通过本地安全配置或 CI 注入
- 若担心设备时间偏差影响鉴权,可使用 `setAuthCurrentTime()` 校正时间
## 文档
官方文档:[Android SDK手册](https://help.aliyun.com/document_detail/435250.html)
## 感谢
本项目中 Inet64Util 工具类由 [Shinelw](https://github.com/Shinelw) 贡献支持,感谢。

View File

@@ -0,0 +1,2 @@
/build
/libs/alicloud-android-httpdns-*.aar

View File

@@ -0,0 +1,84 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.aliyun.ams.httpdns.demo'
compileSdkVersion 34
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.aliyun.ams.httpdns.demo2"
minSdkVersion 19
targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
forTest {
// 注意这里的配置并不是需要编译forTest的app而是避免httpdns-sdk在AndroidStudio改为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 {
// 注意这里的配置并不是需要编译end2end的app而是避免httpdns-sdk在AndroidStudio改为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.alibaba.sdk.android.httpdns.test.**
-dontwarn com.alibaba.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,356 @@
package com.aliyun.ams.httpdns.demo;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import com.alibaba.sdk.android.httpdns.DegradationFilter;
import com.alibaba.sdk.android.httpdns.NetType;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector;
import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean;
import com.aliyun.ams.httpdns.demo.base.BaseActivity;
import com.aliyun.ams.httpdns.demo.http.HttpUrlConnectionRequest;
import com.aliyun.ams.httpdns.demo.okhttp.OkHttpRequest;
import com.aliyun.ams.httpdns.demo.utils.ThreadUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zonglin.nzl
* @date 8/30/22
*/
public class HttpDnsActivity extends BaseActivity {
public static final String SCHEMA_HTTPS = "https://";
public static final String SCHEMA_HTTP = "http://";
public static final String TAOBAO_URL = "www.taobao.com";
public static final String ALIYUN_URL = "www.aliyun.com";
public static final String[] hosts = new String[]{
TAOBAO_URL, ALIYUN_URL
};
public static String getUrl(String schema, String host) {
return schema + host;
}
/**
* 要请求的schema
*/
private String schema = SCHEMA_HTTPS;
/**
* 要请求的域名
*/
private String host = ALIYUN_URL;
/**
* 要解析的ip类型
*/
private RequestIpType requestIpType = RequestIpType.v4;
private HttpUrlConnectionRequest httpUrlConnectionRequest;
private OkHttpRequest okHttpRequest;
private NetworkRequest networkRequest = httpUrlConnectionRequest;
private ExecutorService worker = Executors.newSingleThreadExecutor();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
httpUrlConnectionRequest = new HttpUrlConnectionRequest(this);
okHttpRequest = new OkHttpRequest(this);
networkRequest = httpUrlConnectionRequest;
addFourButton("切换实例", v -> {
MyApp.getInstance().changeHolder();
sendLog("httpdns实例已切换");
}, "获取配置", v -> {
sendLog(MyApp.getInstance().getCurrentHolder().getCurrentConfig());
sendLog("要解析的域名是" + host);
sendLog("要解析的ip类型是" + requestIpType.name());
sendLog("要模拟请求的url是" + getUrl(schema, host));
sendLog("模拟请求的网络框架是" + (networkRequest == httpUrlConnectionRequest ? " HttpUrlConnection" : "OkHttp"));
}, "清除配置缓存", v -> {
MyApp.getInstance().getCurrentHolder().cleanSp();
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "配置缓存清除, 重启生效");
}, "清除日志", v -> cleanLog());
addTwoButton("开启https", v -> {
MyApp.getInstance().getCurrentHolder().setEnableHttps(true);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "开启https");
}, "关闭https", v -> {
MyApp.getInstance().getCurrentHolder().setEnableHttps(false);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "关闭https");
});
addTwoButton("允许过期IP", v -> {
MyApp.getInstance().getCurrentHolder().setEnableExpiredIp(true);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "允许过期IP");
}, "不允许过期IP", v -> {
MyApp.getInstance().getCurrentHolder().setEnableExpiredIp(false);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "不允许过期IP");
});
addTwoButton("允许持久化缓存", v -> {
MyApp.getInstance().getCurrentHolder().setEnableCacheIp(true);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "允许持久化缓存");
}, "不允许持久化缓存", v -> {
MyApp.getInstance().getCurrentHolder().setEnableCacheIp(false);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "不允许持久化缓存");
});
addThreeButton("设置中国大陆", v -> {
MyApp.getInstance().getCurrentHolder().setRegion(null);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到中国大陆");
}, "设置中国香港", v -> {
MyApp.getInstance().getCurrentHolder().setRegion("hk");
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到中国香港");
}, "设置新加坡", v -> {
MyApp.getInstance().getCurrentHolder().setRegion("sg");
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "切换到新加坡");
});
addEditTextButton("超时时长 ms", "设置超时ms", view -> {
EditText et = (EditText) view;
int timeout = Integer.parseInt(et.getEditableText().toString());
MyApp.getInstance().getCurrentHolder().setTimeout(timeout);
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "设置超时 " + timeout);
});
addTwoButton("开启降级", v -> {
// 注意降级过滤器现在需要通过InitConfig设置重启应用生效
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "降级功能需要通过InitConfig配置");
}, "关闭降级", v -> {
// 注意降级过滤器现在需要通过InitConfig设置重启应用生效
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "降级功能需要通过InitConfig配置");
});
addTwoButton("开启网络变化后预解析", v -> {
// 注意此功能现在需要通过InitConfig设置重启应用生效
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "网络变化后预解析需要通过InitConfig配置");
}, "关闭网络变化后预解析", v -> {
// 注意此功能现在需要通过InitConfig设置重启应用生效
sendLog(MyApp.getInstance().getCurrentHolder().getAccountId() + "网络变化后预解析需要通过InitConfig配置");
});
addView(R.layout.item_autocomplete_edittext_button, view -> {
final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne);
final EditText etOne = view.findViewById(R.id.etOne);
Button btnOne = view.findViewById(R.id.btnOne);
actvOne.setHint("请输入域名");
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getApplicationContext(),
android.R.layout.simple_dropdown_item_1line, hosts);
actvOne.setAdapter(adapter);
etOne.setHint("请输入自定义ttl");
btnOne.setText("指定域名ttl s");
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String host = actvOne.getEditableText().toString();
int ttl = Integer.parseInt(etOne.getEditableText().toString());
MyApp.getInstance().getCurrentHolder().setHostTtl(host, ttl);
sendLog("指定域名" + host + "的ttl为" + ttl + "");
}
});
});
addView(R.layout.item_autocomplete_edittext_button, view -> {
final AutoCompleteTextView actvOne = view.findViewById(R.id.actvOne);
final EditText etOne = view.findViewById(R.id.etOne);
Button btnOne = view.findViewById(R.id.btnOne);
actvOne.setHint("域名");
ArrayAdapter<String> adapter = new ArrayAdapter<>(getApplicationContext(),
android.R.layout.simple_dropdown_item_1line, hosts);
actvOne.setAdapter(adapter);
etOne.setHint("请输入端口");
btnOne.setText("添加ipRanking配置");
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String host = actvOne.getEditableText().toString();
int port = Integer.parseInt(etOne.getEditableText().toString());
MyApp.getInstance().getCurrentHolder().addIpProbeItem(new IPRankingBean(host, port));
sendLog("添加域名" + host + " 探测");
}
});
});
addAutoCompleteTextViewButton(hosts, "主站域名", "添加主站域名", view -> {
AutoCompleteTextView actvOne = (AutoCompleteTextView) view;
String host = actvOne.getEditableText().toString();
MyApp.getInstance().getCurrentHolder().addHostWithFixedIp(host);
sendLog("添加主站域名" + host);
});
addAutoCompleteTextViewButton(hosts, "域名", "删除指定域名的缓存", view -> {
AutoCompleteTextView actvOne = (AutoCompleteTextView) view;
String host = actvOne.getEditableText().toString();
ArrayList<String> list = new ArrayList<>();
list.add(host);
MyApp.getInstance().getService().cleanHostCache(list);
sendLog("清除指定host的缓存" + host);
});
addOneButton("清除所有缓存", v -> {
MyApp.getInstance().getService().cleanHostCache(null);
sendLog("清除所有缓存");
});
addFourButton("获取当前网络状态", v -> {
NetType type = HttpDnsNetworkDetector.getInstance().getNetType(getApplicationContext());
sendLog("获取网络状态 " + type.name());
}, "禁用网络状态缓存", v -> {
HttpDnsNetworkDetector.getInstance().disableCache(true);
sendLog("网络状态 禁用缓存 ");
}, "开启网络状态缓存", v -> {
HttpDnsNetworkDetector.getInstance().disableCache(false);
sendLog("网络状态 开启缓存 ");
}, "清除网络状态缓存", v -> {
HttpDnsNetworkDetector.getInstance().cleanCache(false);
sendLog("网络状态清除缓存 ");
});
addTwoButton("禁止读取IP", v -> {
HttpDnsNetworkDetector.getInstance().setCheckInterface(false);
sendLog("查询网络状态时 禁止读取IP");
}, "允许读取IP", v -> {
HttpDnsNetworkDetector.getInstance().setCheckInterface(true);
sendLog("查询网络状态时 允许读取IP");
});
addAutoCompleteTextViewButton(hosts, "域名", "设置检测网络使用的域名", view -> {
AutoCompleteTextView actvOne = (AutoCompleteTextView) view;
String host = actvOne.getEditableText().toString();
HttpDnsNetworkDetector.getInstance().setHostToCheckNetType(host);
sendLog("设置检测网络状态使用的域名为" + host);
});
addTwoButton("模拟请求使用https请求", v -> {
schema = SCHEMA_HTTPS;
sendLog("测试url使用https");
}, "模拟请求使用http请求", v -> {
schema = SCHEMA_HTTP;
sendLog("测试url使用http");
});
addTwoButton("HttpUrlConnection", v -> {
networkRequest = httpUrlConnectionRequest;
sendLog("指定网络实现方式为HttpUrlConnection");
}, "Okhttp", v -> {
networkRequest = okHttpRequest;
sendLog("指定网络实现方式为okhttp");
});
addFourButton("指定v4", v -> {
requestIpType = RequestIpType.v4;
sendLog("要解析的IP类型指定为ipv4");
}, "指定v6", v -> {
requestIpType = RequestIpType.v6;
sendLog("要解析的IP类型指定为ipv6");
}, "都解析", v -> {
requestIpType = RequestIpType.both;
sendLog("要解析的IP类型指定为ipv4和ipv6");
}, "自动判断", v -> {
requestIpType = RequestIpType.auto;
sendLog("要解析的IP类型根据网络情况自动判断");
});
addAutoCompleteTextViewButton(hosts, "域名", "指定要解析的域名", new OnButtonClick() {
@Override
public void onBtnClick(View view) {
AutoCompleteTextView actvOne = (AutoCompleteTextView) view;
host = actvOne.getEditableText().toString();
sendLog("要解析的域名" + host);
}
});
addTwoButton("异步解析", v -> worker.execute(new Runnable() {
@Override
public void run() {
sendLog("开始发起网络请求");
sendLog("网络实现方式为" + (networkRequest == httpUrlConnectionRequest ? "HttpUrlConnection" : "okhttp"));
String url = getUrl(schema, host);
sendLog("url is " + url);
sendLog("httpdns 使用 异步解析api");
sendLog("指定解析ip类型为" + requestIpType.name());
networkRequest.updateHttpDnsConfig(true, requestIpType);
try {
String response = networkRequest.httpGet(url);
if (response != null && response.length() > 30) {
response = response.trim();
if (response.length() > 30) {
response = response.substring(0, 30) + "...";
}
}
sendLog("请求结束 response is " + response + " 完整记录请看logcat日志");
} catch (Exception e) {
e.printStackTrace();
sendLog("请求结束 发生异常 " + e.getClass().getName() + e.getMessage() + " 完整记录请看logcat日志");
}
}
}), "同步解析", v -> worker.execute(() -> {
sendLog("开始发起网络请求");
sendLog("网络实现方式为" + (networkRequest == httpUrlConnectionRequest ? "HttpUrlConnection" : "okhttp"));
String url = getUrl(schema, host);
sendLog("url is " + url);
sendLog("httpdns 使用 同步解析api");
sendLog("指定解析ip类型为" + requestIpType.name());
networkRequest.updateHttpDnsConfig(false, requestIpType);
try {
String response = networkRequest.httpGet(url);
if (response != null && response.length() > 30) {
response = response.substring(0, 30) + "...";
}
sendLog("请求结束 response is " + response + " 完整记录请看logcat日志");
} catch (Exception e) {
e.printStackTrace();
sendLog("请求结束 发生异常 " + e.getClass().getName() + e.getMessage() + " 完整记录请看logcat日志");
}
}));
addThreeButton("发起预解析", v -> worker.execute(() -> {
ArrayList<String> tmp = new ArrayList<>();
Collections.addAll(tmp, hosts);
MyApp.getInstance().getService().setPreResolveHosts(tmp, requestIpType);
sendLog("已发起预解析请求");
}), "跳转SDNS测试界面",
v -> startActivity(new Intent(HttpDnsActivity.this, SDNSActivity.class)), "跳转Webview测试界面", new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(HttpDnsActivity.this, WebViewActivity.class));
}
});
final String[] validHosts = new String[]{
"www.aliyun.com",
"www.taobao.com"
};
addTwoButton("并发异步请求", v -> {
ThreadUtil.multiThreadTest(validHosts, 100, 20, 10 * 60 * 1000, true, requestIpType);
sendLog("异步api并发测试开始大约耗时10分钟请最后查看logcat日志确认结果建议关闭httpdns日志避免日志量过大");
}, "并发同步请求", v -> {
ThreadUtil.multiThreadTest(validHosts, 100, 20, 10 * 60 * 1000, false, requestIpType);
sendLog("同步api并发测试开始大约耗时10分钟请最后查看logcat日志确认结果建议关闭httpdns日志避免日志量过大");
});
}
}

View File

@@ -0,0 +1,342 @@
package com.aliyun.ams.httpdns.demo;
import android.content.Context;
import android.content.SharedPreferences;
import com.alibaba.sdk.android.httpdns.CacheTtlChanger;
import com.alibaba.sdk.android.httpdns.HttpDns;
import com.alibaba.sdk.android.httpdns.HttpDnsService;
import com.alibaba.sdk.android.httpdns.InitConfig;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean;
import com.aliyun.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 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;
}
/**
* 初始化httpdns的配置
*
* @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));
}
});
// 初始化httpdns 的配置此步骤需要在第一次获取HttpDnsService实例之前
new InitConfig.Builder()
.setEnableExpiredIp(enableExpiredIp)
.setEnableCacheIp(enableCacheIp)
.setTimeout(timeout)
.setEnableHttps(enableHttps)
.setIPRankingList(ipRankingList)
.setRegion(region)
.configCacheTtlChanger(cacheTtlChanger)
.configHostWithFixedIp(hostListWithFixedIp)
.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("是否开启HTTPS : ").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,92 @@
package com.aliyun.ams.httpdns.demo;
import android.app.Application;
import android.content.SharedPreferences;
import android.util.Log;
import com.alibaba.sdk.android.httpdns.HttpDnsService;
import com.alibaba.sdk.android.httpdns.ILogger;
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog;
import com.aliyun.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("请替换为测试用A实例的accountId", "请替换为测试用A实例的secret");
private final HttpDnsHolder holderB = new HttpDnsHolder("请替换为测试用B实例的accountId", null);
private HttpDnsHolder current = holderA;
@Override
public void onCreate() {
super.onCreate();
instance = this;
// 开启logcat 日志 默认关闭, 开发测试过程中可以开启
HttpDnsLog.enable(true);
// 注入日志接口接受httpdns的日志开发测试过程中可以开启, 基础日志需要先enable才生效一些错误日志不需要
HttpDnsLog.setLogger(new ILogger() {
@Override
public void log(String msg) {
Log.d("HttpDnsLogger", msg);
}
});
// 初始化httpdns的配置
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,20 @@
package com.aliyun.ams.httpdns.demo;
import com.alibaba.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,132 @@
package com.aliyun.ams.httpdns.demo;
import android.os.Bundle;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.aliyun.ams.httpdns.demo.base.BaseActivity;
import java.util.HashMap;
import static com.aliyun.ams.httpdns.demo.HttpDnsActivity.ALIYUN_URL;
public class SDNSActivity extends BaseActivity {
private final HashMap<String, String> globalParams = new HashMap<>();
/**
* 要请求的域名
*/
private String host = HttpDnsActivity.ALIYUN_URL;
/**
* 要解析的ip类型
*/
private RequestIpType requestIpType = RequestIpType.v4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addEditTextEditTextButton("key", "value", "添加全局配置", new OnButtonClickMoreView() {
@Override
public void onBtnClick(View[] views) {
EditText etOne = (EditText) views[0];
EditText etTwo = (EditText) views[1];
String key = etOne.getEditableText().toString();
String value = etTwo.getEditableText().toString();
globalParams.put(key, value);
// 注意SDNS全局参数现在需要通过InitConfig设置重启应用生效
sendLog("添加全局参数 " + key + " : " + value);
}
});
addOneButton("清除全局配置", new View.OnClickListener() {
@Override
public void onClick(View v) {
globalParams.clear();
// 注意SDNS全局参数现在需要通过InitConfig设置重启应用生效
sendLog("清除全局参数");
}
});
addFourButton("指定v4", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.v4;
sendLog("要解析的IP类型指定为ipv4");
}
}, "指定v6", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.v6;
sendLog("要解析的IP类型指定为ipv6");
}
}, "都解析", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.both;
sendLog("要解析的IP类型指定为ipv4和ipv6");
}
}, "自动判断", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestIpType = RequestIpType.auto;
sendLog("要解析的IP类型根据网络情况自动判断");
}
});
addAutoCompleteTextViewButton(HttpDnsActivity.hosts, "域名", "指定要解析的域名", new OnButtonClick() {
@Override
public void onBtnClick(View view) {
AutoCompleteTextView actvOne = (AutoCompleteTextView) view;
host = actvOne.getEditableText().toString();
sendLog("要解析的域名" + host);
}
});
addEditTextEditTextButton("key", "value", "发起请求", new OnButtonClickMoreView() {
@Override
public void onBtnClick(View[] views) {
EditText etOne = (EditText) views[0];
EditText etTwo = (EditText) views[1];
String key = etOne.getEditableText().toString();
String value = etTwo.getEditableText().toString();
HashMap<String, String> map = new HashMap<>();
map.put(key, value);
sendLog("发起SDNS请求 requestIpType is " + requestIpType.name());
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试SDNS");
sendLog("结果 " + result);
}
});
addTwoButton("scale1参数请求", new View.OnClickListener() {
@Override
public void onClick(View v) {
HashMap<String, String> map = new HashMap<>();
map.put("scale", "scale1");
sendLog("发起SDNS请求 requestIpType is " + requestIpType.name() + " scale : scale1");
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试1");
sendLog("结果 " + result);
}
}, "scale2参数请求", new View.OnClickListener() {
@Override
public void onClick(View v) {
HashMap<String, String> map = new HashMap<>();
map.put("scale", "scale2");
sendLog("发起SDNS请求 requestIpType is " + requestIpType.name() + " scale : scale2");
HTTPDNSResult result = MyApp.getInstance().getService().getIpsByHostAsync(host, requestIpType, map, "测试2");
sendLog("结果 " + result);
}
});
}
}

View File

@@ -0,0 +1,402 @@
package com.aliyun.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);//退出Activity
}
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拦截方案只能正常处理不带body的请求
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);
}
// 注*对于POST请求的Body数据WebResourceRequest接口中并没有提供这里无法处理
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);
// 无mime类型的请求不拦截
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为null的报头指向该http请求状态码
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);
}
/**
* 从contentType中获取MIME类型
*
* @param contentType
* @return
*/
private String getMime(String contentType) {
if (contentType == null) {
return null;
}
return contentType.split(";")[0];
}
/**
* 从contentType中获取编码信息
*
* @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成功进行URL替换和HOST头设置
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请求头Host域
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场景创建SSLScocket
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)) {
// 原有报头中含有cookie放弃拦截
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只返回后面的path所以需要补全url
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中是否含有cookie
*/
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,263 @@
package com.aliyun.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.aliyun.ams.httpdns.demo.MyApp;
import com.aliyun.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,257 @@
package com.aliyun.ams.httpdns.demo.http;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.util.Log;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import com.alibaba.sdk.android.httpdns.NetType;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.alibaba.sdk.android.httpdns.SyncService;
import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector;
import com.aliyun.ams.httpdns.demo.MyApp;
import com.aliyun.ams.httpdns.demo.NetworkRequest;
import com.aliyun.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;
/* 切换为新版标准api */
if (async) {
result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(host, type);
} else {
result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(host, type);
}
Log.d(TAG, "httpdns 解析 " + 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 未返回解析结果走localdns");
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场景创建SSLSocket
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)) {
//临时重定向和永久重定向location的大小写有区分
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
//某些时候会省略host只返回后面的path所以需要补全url
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,97 @@
package com.aliyun.ams.httpdns.demo.okhttp;
import android.content.Context;
import android.util.Log;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import com.alibaba.sdk.android.httpdns.NetType;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.alibaba.sdk.android.httpdns.SyncService;
import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector;
import com.aliyun.ams.httpdns.demo.MyApp;
import com.aliyun.ams.httpdns.demo.NetworkRequest;
import com.aliyun.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()
// 这里配置连接池是为了方便测试httpdns能力正式代码请不要配置
.connectionPool(new ConnectionPool(0, 10 * 1000, TimeUnit.MICROSECONDS))
.dns(new Dns() {
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
HTTPDNSResult result;
/* 切换为新版标准api */
if (async) {
result = MyApp.getInstance().getService().getHttpDnsResultForHostAsync(hostname, type);
} else {
result = MyApp.getInstance().getService().getHttpDnsResultForHostSync(hostname, type);
}
Log.d(TAG, "httpdns 解析 " + 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 未返回IP走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,27 @@
package com.aliyun.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,127 @@
package com.aliyun.ams.httpdns.demo.utils;
import android.util.Log;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import com.alibaba.sdk.android.httpdns.RequestIpType;
import com.alibaba.sdk.android.httpdns.SyncService;
import com.aliyun.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) {
int validCount = validHosts.length;
if (validCount > hostCount) {
validCount = hostCount;
}
final int tmpValidCount = validCount;
Log.d(MyApp.TAG,
threadCount + "线程并发,执行" + executeTime + ", 总域名" + hostCount + "个,有效"
+ validCount + "");
new Thread(new Runnable() {
@Override
public void run() {
final ArrayList<String> hosts = new ArrayList<>(hostCount);
for (int i = 0; i < hostCount - tmpValidCount; i++) {
hosts.add("test" + i + ".aliyun.com");
}
hosts.addAll(Arrays.asList(validHosts).subList(0, tmpValidCount));
final CountDownLatch testLatch = new CountDownLatch(threadCount);
ExecutorService service = Executors.newFixedThreadPool(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
service.execute(new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Random random = new Random(Thread.currentThread().getId());
int allRequestCount = 0;
int slowRequestCount = 0;
int emptyResponseCount = 0;
long maxSlowRequestTime = 0;
long subSlowRequestTime = 0;
int taobaoCount = 0;
int taobaoSuccessCount = 0;
long firstTaobaoTime = 0;
long firstTaobaoSuccessTime = 0;
long begin = System.currentTimeMillis();
while (System.currentTimeMillis() - begin < executeTime) {
String host = hosts.get(random.nextInt(hostCount));
long startRequestTime = System.currentTimeMillis();
HTTPDNSResult ips = null;
if (async) {
ips = MyApp.getInstance().getService().getIpsByHostAsync(host,
type);
} else {
ips =
((SyncService)MyApp.getInstance().getService()).getByHost(
host, type);
}
long endRequestTime = System.currentTimeMillis();
if (host.equals("www.taobao.com")) {
if (taobaoCount == 0) {
firstTaobaoTime = System.currentTimeMillis();
}
taobaoCount++;
if (ips.getIps() != null && ips.getIps().length > 0) {
if (taobaoSuccessCount == 0) {
firstTaobaoSuccessTime = System.currentTimeMillis();
}
taobaoSuccessCount++;
}
}
if (endRequestTime - startRequestTime > 100) {
slowRequestCount++;
subSlowRequestTime += endRequestTime - startRequestTime;
if (maxSlowRequestTime < endRequestTime - startRequestTime) {
maxSlowRequestTime = endRequestTime - startRequestTime;
}
}
if (ips == null || ips.getIps() == null
|| ips.getIps().length == 0) {
emptyResponseCount++;
}
allRequestCount++;
}
String msg = Thread.currentThread().getId() + " allRequestCount: "
+ allRequestCount + ", slowRequestCount: " + slowRequestCount
+ ", emptyResponseCount: " + emptyResponseCount
+ ", maxSlowRequestTime : " + maxSlowRequestTime
+ ", avgSlowRequestTime: " + (slowRequestCount == 0 ? 0
: subSlowRequestTime / slowRequestCount)
+ ", taoRequestCount: " + taobaoCount + ", "
+ "taoSuccessRequestCount: "
+ taobaoSuccessCount + ", firstTaoRequestTime: " + (firstTaobaoTime
- begin) + ", firstSuccessTaoRequestTime: " + (
firstTaobaoSuccessTime - begin);
Log.w(MyApp.TAG, "asyncMulti " + msg);
testLatch.countDown();
}
});
}
try {
testLatch.await();
} catch (InterruptedException e) {
}
}
}).start();
}
}

View File

@@ -0,0 +1,24 @@
package com.aliyun.ams.httpdns.demo.utils;
import com.alibaba.sdk.android.httpdns.HTTPDNSResult;
import java.lang.reflect.Field;
public class Util {
/**
* 获取ttl
* 此方法是用于测试自定义ttl是否生效
*/
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/aliyun_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】Demo程序</string>
<string name="action_settings">Settings</string>
<string name="normal_parse">普通解析</string>
<string name="request_taobao">解析淘宝域名</string>
<string name="request_apple">解析apple域名</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 Aliyun.com</string>
<string name="layout_aboutus_app_version">1.1.3</string>
<!--帮助中心页面-->
<string name="layout_helpus_content">Q : 什么是用户体验Demo\nA : 用户体验Demo就是阿里云平台为您自动创建的、用来体验HttpDns服务和反馈建议用的一个小Demo让您体验便捷、高效的HttpDns服务。\n\nQ : 如何联系我们?\nA : App Demo相关问题请搜索钉钉群号11777313</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">
<trust-anchors>
<certificates src="system" />
</trust-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 @@
/build

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.aliyun.ams.httpdns.demo'
compileSdk 34
defaultConfig {
applicationId "com.aliyun.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", "\"请替换为测试用实例的accountId\""
buildConfigField "String", "SECRET_KEY", "\"请替换为测试用实例的secret\""
buildConfigField "String", "AES_SECRET_KEY", "\"请替换为测试用实例的aes\""
}
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 {
// 注意这里的配置并不是需要编译forTest的app而是避免httpdns-sdk在AndroidStudio改为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
}
}
}
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 {
// 注意这里的配置并不是需要编译end2end的app而是避免httpdns-sdk在AndroidStudio改为end2end运行测试时 BuildVariants报错
}
}
}
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.aliyun.ams:alicloud-android-httpdns:2.6.6'
implementation('com.alibaba: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.aliyun.ams:alicloud-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.alibaba.sdk.android.httpdns.**{*;}

View File

@@ -0,0 +1,24 @@
package com.alibaba.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.alibaba.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.alibaba.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.AlicloudHttpDnsDemo"
android:usesCleartextTraffic="true">
<activity
android:name="com.alibaba.ams.emas.demo.ui.practice.HttpDnsWebviewGetActivity"
android:exported="false"
android:theme="@style/Theme.AlicloudHttpDnsDemo.NoActionBar">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name="com.alibaba.ams.emas.demo.ui.info.list.ListActivity"
android:exported="false"
android:theme="@style/Theme.AlicloudHttpDnsDemo.NoActionBar">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name="com.alibaba.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.alibaba.ams.emas.demo.ui.info.SdnsGlobalSettingActivity"
android:exported="false"
android:theme="@style/Theme.AlicloudHttpDnsDemo.NoActionBar" >
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,91 @@
package com.alibaba.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("www.aliyun.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,16 @@
package com.alibaba.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,37 @@
package com.alibaba.ams.emas.demo
import android.content.Context
import android.text.TextUtils
import com.alibaba.ams.emas.demo.constant.KEY_ENABLE_AUTH_MODE
import com.alibaba.ams.emas.demo.constant.KEY_SECRET_KEY_SET_BY_CONFIG
import com.alibaba.sdk.android.httpdns.HttpDns
import com.alibaba.sdk.android.httpdns.HttpDnsService
import com.aliyun.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,51 @@
package com.alibaba.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.aliyun.ams.httpdns.demo.R
import com.aliyun.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,92 @@
package com.alibaba.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,47 @@
package com.alibaba.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,50 @@
package com.alibaba.ams.emas.demo
import com.alibaba.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,140 @@
package com.alibaba.ams.emas.demo
import android.content.Context
import android.content.SharedPreferences
import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean
import com.aliyun.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(
"aliyun_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,54 @@
package com.alibaba.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,138 @@
package com.alibaba.ams.emas.demo.net
import android.content.Context
import android.util.Log
import com.alibaba.ams.emas.demo.HttpDnsServiceHolder
import com.alibaba.ams.emas.demo.readStringFrom
import com.alibaba.ams.emas.demo.ui.resolve.Response
import com.alibaba.sdk.android.httpdns.HTTPDNSResult
import com.alibaba.sdk.android.httpdns.HttpDnsCallback
import com.alibaba.sdk.android.httpdns.NetType
import com.alibaba.sdk.android.httpdns.RequestIpType
import com.alibaba.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 解析结果 $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场景创建SSLSocket
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)) {
//临时重定向和永久重定向location的大小写有区分
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (!(location!!.startsWith("http://") || location.startsWith("https://"))) {
//某些时候会省略host只返回后面的path所以需要补全url
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,11 @@
package com.alibaba.ams.emas.demo.net
import com.alibaba.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,149 @@
package com.alibaba.ams.emas.demo.net
import android.content.Context
import android.util.Log
import com.alibaba.ams.emas.demo.HttpDnsServiceHolder
import com.alibaba.sdk.android.httpdns.HTTPDNSResult
import com.alibaba.sdk.android.httpdns.HttpDnsCallback
import com.alibaba.sdk.android.httpdns.NetType
import com.alibaba.sdk.android.httpdns.RequestIpType
import com.alibaba.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()!!)
//修改为最新的通俗易懂的api
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 解析结果 $httpDnsResult")
httpDnsResult?.let { processDnsResult(it, inetAddresses) }
if (inetAddresses.isEmpty()) {
Log.d(tag, "httpdns 未返回IP走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,10 @@
package com.alibaba.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,26 @@
package com.alibaba.ams.emas.demo.net
import android.content.Context
import com.alibaba.ams.emas.demo.ui.resolve.Response
import com.alibaba.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,83 @@
package com.alibaba.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,178 @@
package com.alibaba.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.alibaba.ams.emas.demo.constant.KEY_REGION
import com.alibaba.ams.emas.demo.getAccountPreference
import com.alibaba.ams.emas.demo.ui.info.list.ListActivity
import com.alibaba.ams.emas.demo.ui.info.list.kListItemTag
import com.aliyun.ams.httpdns.demo.R
import com.aliyun.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,361 @@
package com.alibaba.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.alibaba.ams.emas.demo.*
import com.alibaba.ams.emas.demo.constant.*
import com.alibaba.sdk.android.httpdns.HttpDns
import com.alibaba.sdk.android.httpdns.HttpDnsService
import com.alibaba.sdk.android.httpdns.InitConfig
import com.alibaba.sdk.android.httpdns.NotUseHttpDnsFilter
import com.alibaba.sdk.android.httpdns.RequestIpType
import com.alibaba.sdk.android.httpdns.log.HttpDnsLog
import com.aliyun.ams.httpdns.demo.BuildConfig
import com.aliyun.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 timeout = preferences.getInt(KEY_TIMEOUT, 2000)
val region = preferences.getString(KEY_REGION, "cn")
val enableDegradationLocalDns = preferences.getBoolean(KEY_ENABLE_DEGRADE, false);
//自定义ttl
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)
//批量解析
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()
.setEnableHttps(enableHttpDns)
.setEnableCacheIp(enableCacheIp, cacheExpireTimeTemp * DAY_MILLS)
.setEnableExpiredIp(enableExpiredIp)
.setRegion(region)
.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 (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,17 @@
package com.alibaba.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,94 @@
package com.alibaba.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.alibaba.ams.emas.demo.ui.info.list.*
import com.aliyun.ams.httpdns.demo.BuildConfig
import com.aliyun.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,62 @@
package com.alibaba.ams.emas.demo.ui.info
import android.app.Application
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import com.alibaba.ams.emas.demo.HttpDnsApplication
import com.alibaba.ams.emas.demo.HttpDnsServiceHolder
import com.alibaba.ams.emas.demo.SingleLiveData
import com.alibaba.ams.emas.demo.getAccountPreference
import com.alibaba.sdk.android.httpdns.NetType
import com.alibaba.sdk.android.httpdns.net.HttpDnsNetworkDetector
import com.aliyun.ams.httpdns.demo.BuildConfig
import com.aliyun.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,45 @@
package com.alibaba.ams.emas.demo.ui.info
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AppCompatActivity
import com.alibaba.ams.emas.demo.constant.KEY_SDNS_GLOBAL_PARAMS
import com.alibaba.ams.emas.demo.getAccountPreference
import com.aliyun.ams.httpdns.demo.R
import com.aliyun.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,352 @@
package com.alibaba.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.aliyun.ams.httpdns.demo.R
import com.aliyun.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,150 @@
package com.alibaba.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.aliyun.ams.httpdns.demo.R
import com.aliyun.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,8 @@
package com.alibaba.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,20 @@
package com.alibaba.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,406 @@
package com.alibaba.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.alibaba.ams.emas.demo.*
import com.alibaba.ams.emas.demo.TtlCacheHolder.toJsonString
import com.alibaba.ams.emas.demo.constant.KEY_BATCH_RESOLVE_HOST_LIST
import com.alibaba.ams.emas.demo.constant.KEY_HOST_BLACK_LIST
import com.alibaba.ams.emas.demo.constant.KEY_HOST_WITH_FIXED_IP
import com.alibaba.ams.emas.demo.constant.KEY_IP_RANKING_ITEMS
import com.alibaba.ams.emas.demo.constant.KEY_PRE_RESOLVE_HOST_LIST
import com.alibaba.ams.emas.demo.constant.KEY_TAGS
import com.alibaba.ams.emas.demo.constant.KEY_TTL_CHANGER
import com.alibaba.sdk.android.httpdns.ranking.IPRankingBean
import com.aliyun.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,67 @@
package com.alibaba.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.aliyun.ams.httpdns.demo.R
import com.aliyun.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,126 @@
package com.alibaba.ams.emas.demo.ui.practice
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.alibaba.ams.emas.demo.HttpDnsServiceHolder
import com.alibaba.ams.emas.demo.net.TLSSNISocketFactory
import com.alibaba.ams.emas.demo.readStringFrom
import com.alibaba.sdk.android.httpdns.NetType
import com.alibaba.sdk.android.httpdns.RequestIpType
import com.alibaba.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 解析结果 $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场景创建SSLSocket
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)) {
//临时重定向和永久重定向location的大小写有区分
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (!(location!!.startsWith("http://") || location.startsWith("https://"))) {
//某些时候会省略host只返回后面的path所以需要补全url
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,198 @@
package com.alibaba.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.alibaba.ams.emas.demo.HttpDnsServiceHolder
import com.aliyun.ams.httpdns.demo.R
import com.aliyun.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)) {
//无mimeType得请求不拦截
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为null的报头指向该http请求状态码
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://www.aliyun.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只返回后面的path所以需要补全url
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,11 @@
package com.alibaba.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,81 @@
package com.alibaba.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,17 @@
package com.alibaba.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,11 @@
package com.alibaba.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/5/26
*/
enum class NetRequestType {
OKHTTP,
HTTP_URL_CONNECTION
}

View File

@@ -0,0 +1,232 @@
package com.alibaba.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.alibaba.ams.emas.demo.constant.KEY_RESOLVE_IP_TYPE
import com.alibaba.ams.emas.demo.constant.KEY_RESOLVE_METHOD
import com.alibaba.ams.emas.demo.getAccountPreference
import com.aliyun.ams.httpdns.demo.R
import com.aliyun.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("同步方法", "异步方法", "同步非阻塞方法")
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,152 @@
package com.alibaba.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.alibaba.ams.emas.demo.HttpDnsApplication
import com.alibaba.ams.emas.demo.SingleLiveData
import com.alibaba.ams.emas.demo.constant.KEY_RESOLVE_IP_TYPE
import com.alibaba.ams.emas.demo.constant.KEY_RESOLVE_METHOD
import com.alibaba.ams.emas.demo.constant.KEY_SDNS_RESOLVE
import com.alibaba.ams.emas.demo.getAccountPreference
import com.alibaba.ams.emas.demo.net.HttpURLConnectionRequest
import com.alibaba.ams.emas.demo.net.OkHttpRequest
import com.alibaba.sdk.android.httpdns.RequestIpType
import com.aliyun.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,7 @@
package com.alibaba.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,11 @@
package com.alibaba.ams.emas.demo.ui.resolve
/**
* @author allen.wy
* @date 2023/6/7
*/
enum class SchemaType {
HTTPS,
HTTP
}

View File

@@ -0,0 +1,363 @@
package com.alibaba.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.aliyun.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()
}
}
/**
* 根据当前的scrollX的值判断松开手后应处于何种状态
*
* @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>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
</vector>

View File

@@ -0,0 +1,12 @@
<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="M615.3,515.8a19.7,19.7 0,0 0,-9.4 -8.5,35.1 35.1,0 0,0 -13.2,-2.3h-12.4v110.3h12.4c5.1,0 9.6,-0.9 13.4,-2.5a20.9,20.9 0,0 0,9.4 -8.9c2.5,-4.2 4.4,-9.9 5.6,-16.9 1.2,-7.1 1.9,-15.9 1.9,-26.5 0,-11.3 -0.7,-20.5 -1.9,-27.7 -1.3,-7.1 -3.2,-12.8 -5.7,-16.9M506.4,509.5a14.3,14.3 0,0 0,-6.9 -5.1,30.8 30.8,0 0,0 -10.3,-1.5h-10.4V563.2h10.4c4.2,0 7.6,-0.5 10.4,-1.4a13.5,13.5 0,0 0,6.8 -4.9c1.8,-2.4 3,-5.5 3.8,-9.2 0.7,-3.8 1.1,-8.6 1.1,-14.5 0,-5.6 -0.4,-10.3 -1.1,-14.2a22.7,22.7 0,0 0,-3.8 -9.3"
android:fillColor="#ffffff"/>
<path
android:pathData="M161.4,369.7h701.2L862.6,339.4L161.4,339.4v30.3zM852.7,637.1c-6.6,9 -17.5,13.5 -32.7,13.5 -6.5,0 -12.7,-0.7 -18.5,-2a78.7,78.7 0,0 1,-13.8 -4.3v-33.6c1.7,1 3.8,1.9 6.2,2.8 2.4,0.9 5.1,1.6 7.8,2.2 2.8,0.6 5.6,1.1 8.5,1.5 2.9,0.4 5.9,0.6 8.7,0.6 3.6,0 6.7,-0.3 9.2,-1a12.4,12.4 0,0 0,6.1 -3.5,14.5 14.5,0 0,0 3.3,-6.6c0.6,-2.7 1,-6 1,-9.9 0,-3.4 -0.3,-6.2 -0.8,-8.6a15.9,15.9 0,0 0,-2.9 -6.3,20.3 20.3,0 0,0 -6.1,-5.1 77,77 0,0 0,-10.3 -4.8,70 70,0 0,1 -14.8,-7.2 31.3,31.3 0,0 1,-9.4 -9.6,39.8 39.8,0 0,1 -4.9,-13.8 117.8,117.8 0,0 1,-1.4 -20c0,-18.6 3.5,-31.7 10.6,-39.6 7.1,-7.8 17.1,-11.7 30,-11.7 6.3,0 11.8,0.6 16.7,1.9 4.8,1.3 8.8,2.7 11.7,4.4v31.6a74.7,74.7 0,0 0,-20.1 -5.8,63.7 63.7,0 0,0 -7.4,-0.5c-6.3,0 -10.8,1.3 -13.7,3.8 -2.8,2.5 -4.2,7.5 -4.2,14.9 0,2.9 0.2,5.3 0.6,7.3 0.4,2 1.3,3.9 2.6,5.5a19.1,19.1 0,0 0,5.7 4.4c2.5,1.3 5.8,2.8 9.8,4.3 6.6,2.5 12,5.3 16.3,8.5 4.2,3.1 7.5,6.8 9.9,11.1 2.4,4.3 4,9.4 4.9,15.2 0.9,5.8 1.3,12.8 1.3,21.1 0,17.2 -3.3,30.3 -9.9,39.3zM766.5,648.2h-33.4l-38.4,-133.5h-0.9v133.5h-24v-175.5h37.1l34.9,125.4h0.9v-125.4h23.8v175.5zM644.1,602.8c-2.6,11.2 -6.2,20.2 -10.9,26.9 -4.8,6.7 -10.5,11.5 -17.1,14.3 -6.7,2.8 -14,4.2 -22.1,4.2h-38.1v-175.5h38.1c8.1,0 15.5,1.1 22.1,3.4 6.7,2.3 12.4,6.6 17.1,13 4.8,6.4 8.4,15.3 11,26.8 2.5,11.5 3.8,26.3 3.8,44.5 0,17 -1.3,31.2 -3.8,42.4zM533.7,561.3c-1.6,7.8 -4.2,14.2 -7.7,19.1 -3.5,4.9 -8,8.4 -13.6,10.5 -5.5,2.1 -12.2,3.2 -20,3.2h-13.6v54.1h-24.3v-175.5h37.9c8.3,0 15.3,1.1 20.8,3.4 5.6,2.3 10.1,5.8 13.4,10.6 3.3,4.8 5.8,11 7.2,18.5 1.5,7.5 2.2,16.6 2.2,27.2 0,11.5 -0.8,21.1 -2.4,28.9zM439.4,506.3L410.2,506.3v141.9h-24.1v-141.9h-29.1v-33.6h82.3v33.6zM351.3,506.3h-29.1v141.9L298.1,648.1v-141.9L269,506.3v-33.6h82.3v33.6zM253.6,648.1h-24.1v-77.6L185.7,570.5v77.6h-24.3v-175.5h24.3v65.7h43.8v-65.7h24.1v175.5zM179.9,252.7a20.1,20.1 0,1 1,-0 40.3,20.1 20.1,0 0,1 0,-40.3zM242.8,252.7a20.1,20.1 0,1 1,0 40.3,20.1 20.1,0 0,1 0,-40.3zM305.7,252.7a20.1,20.1 0,1 1,0 40.3,20.1 20.1,0 0,1 0,-40.3zM832.7,192L191.3,192A106,106 0,0 0,85.3 297.9v408.7a106,106 0,0 0,105.9 105.9h641.5A106,106 0,0 0,938.7 706.7L938.7,297.9A106,106 0,0 0,832.7 192z"
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:fillColor="#FF000000"
android:pathData="M512,1024A512,512 0,1 1,512 0a512,512 0,0 1,0 1024zM448,448v384h128L576,448L448,448zM448,192v128h128L576,192L448,192z" />
</vector>

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