阿里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 @@
/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>

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="M288,918.4l544,-352c19.2,-12.8 28.8,-32 28.8,-54.4 0,0 0,0 0,0 -0,-22.4 -9.6,-41.6 -28.8,-54.4l-275.2,-179.2L288,108.8c-19.2,-12.8 -44.8,-12.8 -64,-3.2 -19.2,9.6 -32,32 -32,54.4l0,704c0,22.4 12.8,44.8 32,54.4C243.2,931.2 268.8,931.2 288,918.4z" />
</vector>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.AlicloudHttpDnsDemo.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/webview_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.AlicloudHttpDnsDemo.PopupOverlay"
app:titleTextColor="@color/white"
app:navigationIcon="@drawable/ic_back"/>
</com.google.android.material.appbar.AppBarLayout>
<WebView
android:id="@+id/httpdns_webview"
android:layout_marginTop="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.alibaba.ams.emas.demo.ui.info.list.ListActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.AlicloudHttpDnsDemo.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/info_list_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.AlicloudHttpDnsDemo.PopupOverlay"
app:titleTextColor="@color/white"
app:navigationIcon="@drawable/ic_back"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/info_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
android:backgroundTint="@color/cloud_blue"
app:srcCompat="@drawable/ic_add"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.AlicloudHttpDnsDemo.AppBarOverlay"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.AlicloudHttpDnsDemo.PopupOverlay"
app:titleTextColor="@color/white"
app:navigationIcon="@drawable/ic_back"/>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sdns_params_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/app_bar_layout"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/input_the_sdns_params"
app:boxBackgroundColor="@android:color/transparent"
app:errorEnabled="true"
app:helperText="@string/input_the_sdns_params_help_text"
tools:ignore="MissingConstraints">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,18 @@
<?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">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/add_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
>
</androidx.appcompat.widget.AppCompatEditText>
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?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:orientation="vertical">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/input_content_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
android:textSize="14dp"
/>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/input_content_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:textSize="14dp"
/>
</LinearLayout>

View File

@@ -0,0 +1,37 @@
<?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:orientation="vertical">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/add_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
>
</androidx.appcompat.widget.AppCompatEditText>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@string/ip_type"/>
<RadioGroup android:id="@+id/ip_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="18dp"
android:layout_marginEnd="18dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" />
</LinearLayout>

View File

@@ -0,0 +1,268 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.aliyun.ams.httpdns.demo.BuildConfig" />
<variable
name="viewModel"
type="com.alibaba.ams.emas.demo.ui.basic.BasicSettingViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.info.InfoFragment"
android:layout_marginBottom="60dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/basic_scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/content_max_width_percent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/secret_key_config"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.secretKeySetByConfig}"
android:onCheckedChanged="@{viewModel::toggleSecretKeySet}"
android:text="@string/secret_key_location"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_auth_mode"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableAuthMode}"
android:onCheckedChanged="@{viewModel::toggleAuthMode}"
android:text="@string/enable_auth_mode"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_encrypt_mode"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableEncryptMode}"
android:onCheckedChanged="@{viewModel::toggleEncryptMode}"
android:text="@string/enable_encrypt_mode"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_expired_ip"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableExpiredIP}"
android:onCheckedChanged="@{viewModel::toggleEnableExpiredIp}"
android:text="@string/enable_expired_ip"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_cache_ip"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableCacheIP}"
android:onCheckedChanged="@{viewModel::toggleEnableCacheIp}"
android:text="@string/enable_local_cache"
android:textSize="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cache_expire_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/cache_expire_time"
app:boxBackgroundColor="@android:color/transparent"
app:helperText="@string/cache_expire_time_unit"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.cacheExpireTime}"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_https"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableHttps}"
android:onCheckedChanged="@{viewModel::toggleEnableHttps}"
android:text="@string/enable_https"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_downgrade"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableDegrade}"
android:onCheckedChanged="@{viewModel::toggleEnableDegrade}"
android:text="@string/enable_downgrade"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_network_change_pre"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableAutoRefresh}"
android:onCheckedChanged="@{viewModel::toggleEnableAutoRefresh}"
android:text="@string/enable_network_changed_pre_resolve"
android:textSize="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_log"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.enableLog}"
android:onCheckedChanged="@{viewModel::toggleEnableLog}"
android:text="@string/enable_httpdns_log"
android:textSize="16dp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> viewModel.setRegion()}"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="Region"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/info_region"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.currentRegion)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="12345" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> viewModel.setTimeout()}"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/timeout"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/info_timeout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.currentTimeout)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="12345" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:onClick="@{() -> viewModel.showClearCacheDialog()}"
android:padding="18dp"
android:text="@string/clear_host_cache"
android:textColor="@color/black"
android:background="?selectableItemBackground"
android:textSize="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:onClick="@{() -> viewModel.batchResolveHosts()}"
android:padding="18dp"
android:text="@string/batch_resolve"
android:textColor="@color/black"
android:background="?selectableItemBackground"
android:textSize="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:onClick="@{() -> viewModel.showAddPreResolveDialog()}"
android:padding="18dp"
android:text="@string/add_pre_resolve"
android:textColor="@color/black"
android:background="?selectableItemBackground"
android:visibility="gone"
android:textSize="16dp" />
<TextView android:id="@+id/jump_to_add_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:padding="18dp"
android:text="@string/add_tag"
android:textColor="@color/black"
android:background="?selectableItemBackground"
android:textSize="16dp" />
<Button android:id="@+id/init_httpdns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/init_httpdns"
android:onClick="@{() -> viewModel.initHttpDns()}"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.alibaba.ams.emas.demo.ui.practice.BestPracticeViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<TextView
android:id="@+id/open_httpdns_webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableEnd="@drawable/ic_arrow_right"
android:padding="18dp"
android:text="@string/httpdns_webview_best_practice"
android:textColor="@color/black"
android:textSize="16dp" />
<!-- <TextView-->
<!-- android:id="@+id/open_httpdns_webview_post"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:background="?selectableItemBackground"-->
<!-- android:drawableEnd="@drawable/ic_arrow_right"-->
<!-- android:padding="18dp"-->
<!-- android:text="@string/httpdns_webview_post_best_practice"-->
<!-- android:textColor="@color/black"-->
<!-- android:textSize="16dp" />-->
<TextView
android:id="@+id/open_httpdns_sni"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableEnd="@drawable/ic_arrow_right"
android:padding="18dp"
android:text="@string/httpdns_sni"
android:textColor="@color/black"
android:textSize="16dp"
android:onClick="@{() -> viewModel.sniRequest()}"
/>
</LinearLayout>
</layout>

View File

@@ -0,0 +1,378 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.aliyun.ams.httpdns.demo.BuildConfig" />
<variable
name="viewModel"
type="com.alibaba.ams.emas.demo.ui.info.InfoViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.info.InfoFragment"
android:paddingBottom="60dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/basic_scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/content_max_width_percent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="15dp"
android:paddingTop="10dp"
android:paddingEnd="15dp"
android:paddingBottom="10dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="20dp"
android:orientation="vertical">
<TextView
android:id="@+id/info_app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="16dp"
android:textStyle="bold" />
<TextView
android:id="@+id/info_pkg_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="15dp"
tools:text="com.aliyun.ams.httpdns.demo" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:background="@color/light_grey" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="AccoutId"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/info_account_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.accountId)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="12345" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/info_secret_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="SecretKey"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/info_secret_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.secretKey)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="12345" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_pre_resolve"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/pre_resolve_list"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_ip_ranking"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/ip_probe_list"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_ttl_cache"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/ttl_cache_list"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_host_fiex_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/host_fixed_ip_list"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_host_black_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/host_black_list"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_sdns_global_params"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/sdns_global_params"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/jump_to_batch_resolve"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/batch_resolve"
android:textColor="@color/black"
android:textSize="16dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:onClick="@{() -> viewModel.clearDnsCache()}"
android:padding="18dp"
android:text="@string/clear_dns_cache"
android:textColor="@color/black"
android:textSize="16dp"
android:background="?selectableItemBackground"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/httpdns_sdk_version"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{BuildConfig.HTTPDNS_VERSION}"
android:textColor="@color/black"
android:textSize="14dp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="@string/ip_stack"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.currentIpStackType)}"
android:textColor="@color/black"
android:textSize="14dp" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_arrow_right"
android:onClick="@{() -> viewModel.clearAllCache()}"
android:padding="18dp"
android:text="@string/clear_all_cache"
android:textColor="@color/black"
android:textSize="16dp"
android:background="?selectableItemBackground"
/>
FragmentBestPracticeBinding
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,336 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.alibaba.ams.emas.demo.ui.resolve.ResolveViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.resolve.ResolveFragment"
android:paddingBottom="60dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/basic_scroll_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="@dimen/content_max_width_percent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="5dp"
android:text="@string/network_request_type"
android:textColor="@color/black"
android:textSize="16dp" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:checkedButton="@id/okhttp"
android:onCheckedChanged="@{viewModel::onNetRequestTypeChanged}"
android:orientation="horizontal">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/okhttp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OkHttp"
android:textSize="14dp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/http_url_connection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="HttpURLConnection"
android:textSize="14dp" />
</RadioGroup>
</RelativeLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_sdns_resolve"
style="@style/Widget.HttpDnsDemo.Settings.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{viewModel.isSdns}"
android:text="@string/sdns_resolve"
android:textSize="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sdns_params_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/input_the_sdns_params"
app:boxBackgroundColor="@android:color/transparent"
app:errorEnabled="true"
app:helperText="@string/input_the_sdns_params_help_text">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sdns_cache_key_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/input_the_sdns_cache_key"
app:boxBackgroundColor="@android:color/transparent"
app:errorEnabled="true"
app:helperText="@string/input_the_sdns_cache_key_help_text">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:padding="12dp"
android:onClick="@{() -> viewModel.setResolveMethod()}"
android:orientation="vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="5dp"
android:text="@string/resolve_method"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/resolve_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.currentResolveMethod)}"
android:textColor="@color/black"
android:textSize="12dp" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> viewModel.setResolveIpType()}"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="5dp"
android:text="@string/ip_type"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/info_region"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.currentIpType)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="V4" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{() -> viewModel.setRequestNumber()}"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="5dp"
android:text="@string/request_num"
android:textColor="@color/black"
android:textSize="16dp" />
<TextView
android:id="@+id/request_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:padding="5dp"
android:text="@{String.valueOf(viewModel.requestNum)}"
android:textColor="@color/black"
android:textSize="14dp"
tools:text="1" />
</RelativeLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/resolve_host_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/input_the_resolve_host"
app:boxBackgroundColor="@android:color/transparent"
app:errorEnabled="true"
app:helperText="@string/input_the_resolve_host_help_text">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/request_api_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:backgroundTint="@color/white"
android:hint="@string/input_the_request_api"
app:boxBackgroundColor="@android:color/transparent"
app:errorEnabled="true"
app:helperText="@string/input_the_request_api_help_text">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="2dp" />
</com.google.android.material.textfield.TextInputLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:padding="5dp"
android:text="@string/schema_type"
android:textColor="@color/black"
android:textSize="16dp" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:checkedButton="@id/schema_https"
android:onCheckedChanged="@{viewModel::onSchemaTypeChanged}"
android:orientation="horizontal">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/schema_https"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Https"
android:textSize="14dp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/schema_http"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="Http"
android:textSize="14dp" />
</RadioGroup>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/start_resolve"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/resolve_and_request"
app:cornerRadius="20dp"
app:icon="@drawable/ic_httpdns"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp"
app:iconTint="@color/white"
app:rippleColor="@color/cloud_blue"
app:strokeColor="@color/white"
app:strokeWidth="2dp" />
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.alibaba.ams.emas.demo.widget.SwipeLayout
android:id="@+id/host_and_port_or_ttl_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentView="@id/host_port_ttl_item"
app:menuView="@id/slide_delete_menu">
<LinearLayout
android:id="@+id/host_port_ttl_item"
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"
android:paddingTop="8dp"
android:paddingBottom="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:padding="5dp"
android:text="@string/host"
android:textColor="@color/black"
android:textSize="15dp" />
<TextView
android:id="@+id/host_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:padding="5dp"
android:textColor="@color/black"
android:textSize="15dp"
android:maxLines="2"
android:ellipsize="marquee"
tools:text="www.aliyun.com" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/port_or_ttl_indicate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:padding="5dp"
android:textColor="@color/black"
android:textSize="15dp"
tools:text="@string/port" />
<TextView
android:id="@+id/port_or_ttl_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:padding="5dp"
android:textColor="@color/black"
android:textSize="15dp"
android:maxLines="2"
android:ellipsize="marquee"
tools:text="8888" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1.6dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:background="#30BDBDBD" />
</LinearLayout>
<RelativeLayout
android:id="@+id/slide_delete_menu"
android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_marginBottom="6dp"
android:background="#D32F2F"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="5dp"
android:text="@string/delete"
android:textColor="@color/white"
android:textSize="17dp" />
</RelativeLayout>
</com.alibaba.ams.emas.demo.widget.SwipeLayout>
<com.alibaba.ams.emas.demo.widget.SwipeLayout
android:id="@+id/host_fixed_ip_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentView="@id/host_fixed_ip_item"
app:menuView="@id/slide_delete_menu2">
<LinearLayout
android:id="@+id/host_fixed_ip_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/pre_host_or_with_fixed_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:padding="5dp"
android:textColor="@color/black"
android:textSize="17dp"
tools:text="www.aliyun.com" />
<View
android:layout_width="match_parent"
android:layout_height="1.6dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:background="#30BDBDBD" />
</LinearLayout>
<RelativeLayout
android:id="@+id/slide_delete_menu2"
android:layout_width="100dp"
android:layout_height="match_parent"
android:layout_marginBottom="6dp"
android:background="#D32F2F"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="5dp"
android:text="@string/delete"
android:textColor="@color/white"
android:textSize="17dp" />
</RelativeLayout>
</com.alibaba.ams.emas.demo.widget.SwipeLayout>
</LinearLayout>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_basic"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_basic" />
<item
android:id="@+id/navigation_resolve"
android:icon="@drawable/ic_dns"
android:title="@string/title_resolve" />
<item
android:id="@+id/navigation_best_practice"
android:icon="@drawable/ic_practice"
android:title="@string/title_best_practice" />
<item
android:id="@+id/navigation_information"
android:icon="@drawable/ic_information"
android:title="@string/title_info" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_basic">
<fragment
android:id="@+id/navigation_basic"
android:name="com.alibaba.ams.emas.demo.ui.basic.BasicSettingFragment"
android:label="@string/title_basic"
tools:layout="@layout/fragment_basic_setting" />
<fragment
android:id="@+id/navigation_resolve"
android:name="com.alibaba.ams.emas.demo.ui.resolve.ResolveFragment"
android:label="@string/title_resolve"
tools:layout="@layout/fragment_resolve" />
<fragment
android:id="@+id/navigation_best_practice"
android:name="com.alibaba.ams.emas.demo.ui.practice.BestPracticeFragment"
android:label="@string/title_best_practice"
tools:layout="@layout/fragment_best_practice" />
<fragment
android:id="@+id/navigation_information"
android:name="com.alibaba.ams.emas.demo.ui.info.InfoFragment"
android:label="@string/title_info"
tools:layout="@layout/fragment_info" />
</navigation>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -0,0 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Alicloudandroidsdkhttpdns_for_open_source" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryDark">@color/purple_700</item>
<item name="colorAccent">@color/teal_200</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">阿里云HttpDNS官方Demo</string>
<string name="title_basic">基础设置</string>
<string name="title_resolve">HttpDNS解析</string>
<string name="title_best_practice">最佳实践</string>
<string name="title_info">信息</string>
<string name="enable_auth_mode">开启鉴权模式</string>
<string name="secret_key_location">SecretKey通过config配置</string>
<string name="enable_encrypt_mode">开启加密模式</string>
<string name="enable_expired_ip">允许过期IP</string>
<string name="enable_local_cache">开启持久化缓存IP</string>
<string name="cache_expire_time">缓存过期时间</string>
<string name="cache_expire_time_unit">缓存过期时间的单位是天.</string>
<string name="enable_https">开启HTTPS</string>
<string name="enable_downgrade">允许降级</string>
<string name="enable_network_changed_pre_resolve">网络切换自动刷新</string>
<string name="enable_httpdns_log">允许SDK打印日志</string>
<string name="timeout">超时时间</string>
<string name="init_tip">请先初始化HttpDns</string>
<string name="inited_httpdns">已经初始化</string>
<string name="china">中国</string>
<string name="china_hk">中国香港</string>
<string name="singapore">新加坡</string>
<string name="germany">德国</string>
<string name="america">美国</string>
<string name="pre">预发</string>
<string name="select_region">选择Region</string>
<string name="confirm">确认</string>
<string name="cancel">取消</string>
<string name="timeout_hint">请输入超时时间,毫秒为单位</string>
<string name="set_timeout">设置超时</string>
<string name="timeout_empty">超时时间为空</string>
<string name="ip_probe_list">探测IP列表</string>
<string name="ttl_cache_list">自定义TTL缓存列表</string>
<string name="host_fixed_ip_list">主站域名列表</string>
<string name="host_black_list">域名黑名单列表</string>
<string name="sdns_global_params">自定义解析全局参数</string>
<string name="batch_resolve">批量解析域名</string>
<string name="httpdns_sdk_version">HttpDNS版本号</string>
<string name="unknown">未知</string>
<string name="ip_stack">当前所连接网络的网络栈类型</string>
<string name="clear_host_cache">清空指定域名缓存</string>
<string name="clear_cache_hint">请输入要清空缓存的域名</string>
<string name="network_request_type">网络请求类型</string>
<string name="async_resolve">异步解析获取IP</string>
<string name="sdns_resolve">自定义域名解析</string>
<string name="ip_type">要解析的IP类型</string>
<string name="resolve_method">使用的解析方法</string>
<string name="request_num">并发请求次数</string>
<string name="select_resolve_ip_type">选择IP类型</string>
<string name="select_resolve_method">选择解析方法</string>
<string name="select_request_num">选择并发请求次数</string>
<string name="auto_get_ip_type">自动判断IP类型</string>
<string name="add_pre_resolve">添加预解析域名</string>
<string name="add_pre_resolve_hint">请输入要预解析的域名</string>
<string name="pre_resolve_list">预解析域名列表</string>
<string name="add_tag">添加Tag</string>
<string name="pre_resolve_host_duplicate">%s域名已经被添加至预解析列表请勿重复添加</string>
<string name="init_httpdns">初始化HttpDns</string>
<string name="batch_resolve_list">批量解析域名列表</string>
<string name="add_batch_resolve_hint">请输入要批量解析的域名</string>
<string name="add_batch_resolve">请输入要批量解析的域名</string>
<string name="batch_resolve_host_duplicate">%s域名已经被添加至批量解析列表请勿重复添加</string>
<string name="host">域名:</string>
<string name="port">端口:</string>
<string name="ttl">TTL时长: </string>
<string name="add_host_fixed_ip_hint">请输入主站域名</string>
<string name="add_host_fixed_ip">添加主站域名</string>
<string name="host_fixed_ip_empty">主站域名为空</string>
<string name="host_fixed_ip_duplicate">%s主站域名已经被添加请勿重复添加</string>
<string name="add_tag_hint">请输入标签</string>
<string name="add_host_to_black_list_hint">请输入不使用HttpDns解析的域名</string>
<string name="add_host_to_black_list">添加不使用HttpDns的域名</string>
<string name="host_to_black_list_empty">域名为空</string>
<string name="host_black_list_duplicate">%s域名已经在黑名单中请勿重复添加</string>
<string name="add_ip_probe_host_hint">请输入探测IP的域名</string>
<string name="add_ip_probe_port_hint">请输入探测IP的端口</string>
<string name="add_ip_probe">添加IP探测</string>
<string name="port_is_empty">端口号为空</string>
<string name="ip_probe_item_duplicate">%s:%s已经被添加至IP探测列表请勿重复添加</string>
<string name="host_is_empty">域名为空</string>
<string name="pre_resolve_host_is_empty">预解析的域名为空</string>
<string name="batch_resolve_host_is_empty">批量解析的域名为空</string>
<string name="add_ttl_host_hint">请输入缓存的域名</string>
<string name="add_ttl_ttl_hint">请输入缓存的ttl时间单位</string>
<string name="add_custom_ttl">添加自定义TTL</string>
<string name="ttl_is_empty">TTL时间为空</string>
<string name="ttl_is_not_number">请输入正确格式的TTL时长</string>
<string name="delete">删除</string>
<string name="clear_all_cache">清除所有HttpDNS配置缓存</string>
<string name="all_cache_cleared">所有HttpDNS配置缓存都已清除</string>
<string name="clear_dns_cache">清空Dns缓存500次</string>
<string name="input_the_sdns_params">自定义解析的参数</string>
<string name="input_the_sdns_params_help_text">json对象格式</string>
<string name="input_the_sdns_params_error">自定义解析的参数必须是json对象</string>
<string name="input_the_sdns_cache_key">自定义解析的cache key</string>
<string name="input_the_sdns_cache_key_help_text">Cache key用于唯一标识缓存中的解析结果</string>
<string name="input_the_resolve_host">要解析的域名</string>
<string name="input_the_resolve_host_help_text">例如help.aliyun.com</string>
<string name="input_the_request_api">要请求的接口</string>
<string name="input_the_request_api_help_text">例如: /document_detail/434554.html</string>
<string name="resolve_and_request">解析并请求</string>
<string name="resolve_host_empty">域名不能为空</string>
<string name="host_is_ip">域名不能是IP地址</string>
<string name="host_illegal">请输入正确格式的域名</string>
<string name="schema_type">Schema类型</string>
<string name="httpdns_webview_best_practice">HttpDNS WebView 拦截GET请求</string>
<string name="httpdns_webview_post_best_practice">HttpDNS WebView POST请求通过Native发送</string>
<string name="httpdns_sni">HttpDNS IP直连方案</string>
<string name="ok">好的</string>
<string name="response_title">请求结果</string>
<string name="body_large_see_log">Body请通过日志查看关键字为httpdns</string>
<string name="sni_request">IP直连方案</string>
<string name="tips">提示</string>
<string name="network_not_connect">网络未连接,请检查网络</string>
<string name="request_exception">请求异常: %s</string>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SwipeLayout">
<attr name="menuView" format="reference" />
<attr name="contentView" format="reference" />
<attr name="fraction" format="float" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="light_grey">#e6e6e6</color>
<color name="cloud_blue">#1B58F4</color>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<!-- Maximum width of the content area (in percent) to use in single pane. -->
<dimen name="content_max_width_percent">1.0</dimen>
<dimen name="spacing_normal">8dp</dimen>
<dimen name="margin_small">8dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,140 @@
<resources>
<string name="app_name">Alibaba Cloud HttpDNS Demo</string>
<string name="title_basic">Basic Settings</string>
<string name="title_resolve">HttpDNS Resolve</string>
<string name="title_best_practice">Best Practice</string>
<string name="title_info">Information</string>
<string name="init_tip">Please init HttpDns first</string>
<string name="enable_auth_mode">Enable Auth Mode</string>
<string name="secret_key_location">Secret key is in config</string>
<string name="enable_encrypt_mode">Enable Encrypt Mode</string>
<string name="enable_expired_ip">Enable Expired IP</string>
<string name="enable_local_cache">Enable Cache IP</string>
<string name="cache_expire_time">cache expire time</string>
<string name="cache_expire_time_unit">the unit of cache expire time is day.</string>
<string name="enable_https">Enable HTTPS</string>
<string name="enable_downgrade">Enable Degrade</string>
<string name="enable_network_changed_pre_resolve">Enable Automatic Refresh</string>
<string name="enable_httpdns_log">Enable Log</string>
<string name="timeout">Timeout</string>
<string name="china">China</string>
<string name="china_hk">China-HongKong</string>
<string name="singapore">Singapore</string>
<string name="germany">Germany</string>
<string name="america">America</string>
<string name="pre">Pre</string>
<string name="select_region">Choose Region</string>
<string name="confirm">Confirm</string>
<string name="cancel">Cancel</string>
<string name="timeout_hint">Please input timeout, ms unit</string>
<string name="set_timeout">Set Timeout</string>
<string name="timeout_empty">Timeout is empty</string>
<string name="ip_probe_list">Probe IP List</string>
<string name="ttl_cache_list">TTL Cache List</string>
<string name="host_fixed_ip_list">Host with fixed IP List</string>
<string name="host_black_list">Host black list</string>
<string name="sdns_global_params">Custom DNS global params</string>
<string name="batch_resolve"> Batch resolve hosts</string>
<string name="httpdns_sdk_version">HttpDNS Version</string>
<string name="unknown">Unknown</string>
<string name="ip_stack">IP stack type of current network</string>
<string name="clear_host_cache">Clear the cached DNS record</string>
<string name="clear_cache_hint">Please input the domain name which you want to clear the cache</string>
<string name="network_request_type">Network request type</string>
<string name="async_resolve">Asynchronously resolve host</string>
<string name="sdns_resolve">Custom resolve host</string>
<string name="ip_type">Resolve IP Type</string>
<string name="resolve_method">Resolve Method</string>
<string name="request_num">Number of concurrent requests</string>
<string name="select_resolve_ip_type">Select IP Type</string>
<string name="select_resolve_method">Select resolve Method</string>
<string name="select_request_num">Select number of concurrent requests</string>
<string name="auto_get_ip_type">Get IP automatically</string>
<string name="add_pre_resolve">Add Pre-Resolve Domain</string>
<string name="add_pre_resolve_hint">Please input the domain you want to pre-resolve</string>
<string name="pre_resolve_list">Pre-Resolve Domain List</string>
<string name="add_tag">Add Tag</string>
<string name="pre_resolve_host_duplicate">The host %s is already added to pre-resolve List</string>
<string name="init_httpdns">Init HttpDns</string>
<string name="inited_httpdns">HttpDns Inited</string>
<string name="batch_resolve_list">Batch resolve domain list</string>
<string name="add_batch_resolve_hint">Please input the domain you want to batch resolve</string>
<string name="add_batch_resolve">Add batch resolve domain</string>
<string name="batch_resolve_host_duplicate">The host %s is already added to batch resolve List</string>
<string name="host">Host: </string>
<string name="port">Port: </string>
<string name="ttl">Ttl: </string>
<string name="add_host_fixed_ip_hint">Please input the host with fixed IP</string>
<string name="add_host_fixed_ip">Add Host with fixed IP</string>
<string name="host_fixed_ip_empty">Host with fixed IP is empty</string>
<string name="host_fixed_ip_duplicate">The host %s is already added to fixed IP List</string>
<string name="add_tag_hint">Please input the tag</string>
<string name="add_host_to_black_list_hint">Please input the host not use HttpDns</string>
<string name="add_host_to_black_list">Add Host not use HttpDns</string>
<string name="host_to_black_list_empty">Host not use HttpDns is empty</string>
<string name="host_black_list_duplicate">The host %s is already added to Black List</string>
<string name="add_ip_probe_host_hint">Please input the host of IP Probe</string>
<string name="add_ip_probe_port_hint">Please input the port of IP Probe</string>
<string name="add_ip_probe">Add IP probe</string>
<string name="port_is_empty">Port is empty</string>
<string name="ip_probe_item_duplicate">The ip probe item - %s:%s is already added</string>
<string name="host_is_empty">Host is empty</string>
<string name="pre_resolve_host_is_empty">Pre-Resolve Host is empty</string>
<string name="batch_resolve_host_is_empty">Batch resolve host is empty</string>
<string name="add_ttl_host_hint">Please input the host of custom TTL</string>
<string name="add_ttl_ttl_hint">Please input the ttl, unit is second</string>
<string name="add_custom_ttl">Add custom ttl</string>
<string name="ttl_is_empty">TTL is empty</string>
<string name="ttl_is_not_number">Please input correct ttl value</string>
<string name="delete">Delete</string>
<string name="clear_all_cache">Clear all HttpDNS config cache</string>
<string name="all_cache_cleared">All HttpDNDS config cache have been cleared</string>
<string name="clear_dns_cache">Clear Dns Cache (500)</string>
<string name="input_the_sdns_params">The sdns params</string>
<string name="input_the_sdns_params_help_text">json object format</string>
<string name="input_the_sdns_params_error">The sdns params must be json object</string>
<string name="input_the_sdns_cache_key">The sdns cache key</string>
<string name="input_the_sdns_cache_key_help_text">The cache key is the uniquely identifies of the cache dns result</string>
<string name="input_the_resolve_host">The Host will be resolved</string>
<string name="input_the_resolve_host_help_text">eg: help.aliyun.com</string>
<string name="input_the_request_api">The API to be requested</string>
<string name="input_the_request_api_help_text">eg: /document_detail/434554.html</string>
<string name="resolve_and_request">Resolve and request</string>
<string name="resolve_host_empty">The host can not be empty</string>
<string name="host_is_ip">The input host can not be an IP</string>
<string name="host_illegal">The input a valid host</string>
<string name="schema_type">Schema Type</string>
<string name="httpdns_webview_best_practice">HttpDNS WebView intercept GET request</string>
<string name="httpdns_webview_post_best_practice">HttpDNS WebView POST request forward to native</string>
<string name="httpdns_sni">HttpDNS Request By IP Address</string>
<string name="ok">OK</string>
<string name="response_title">Response</string>
<string name="body_large_see_log">Check the logs to get the response body with the keyword httpdns</string>
<string name="sni_request">Request By IP Address</string>
<string name="tips">Tips</string>
<string name="network_not_connect">Network has issues, please check the network</string>
<string name="request_exception">Request exception: %s</string>
</resources>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.HttpDnsDemo" parent="@android:style/Widget.Material" />
<style name="Widget.HttpDnsDemo.Settings.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:background">@null</item>
<item name="android:foreground">?selectableItemBackground</item>
<item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item>
<item name="android:paddingBottom">@dimen/spacing_normal</item>
<item name="android:paddingEnd">?listPreferredItemPaddingEnd</item>
<item name="android:paddingStart">?listPreferredItemPaddingStart</item>
<item name="android:paddingTop">@dimen/spacing_normal</item>
<item name="android:textAlignment">viewStart</item>
<item name="switchPadding">@dimen/margin_small</item>
<item name="switchTextAppearance">?attr/textAppearanceBody2</item>
</style>
</resources>

View File

@@ -0,0 +1,20 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AlicloudHttpDnsDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/cloud_blue</item>
<item name="colorPrimaryDark">@color/cloud_blue</item>
<item name="colorAccent">@color/cloud_blue</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Customize your theme here. -->
</style>
<style name="Theme.AlicloudHttpDnsDemo.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.AlicloudHttpDnsDemo.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.AlicloudHttpDnsDemo.PopupOverlay" parent="ThemeOverlay.AppCompat.Dark" />
</resources>

View File

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

View File

@@ -0,0 +1,17 @@
package com.alibaba.ams.emas.demo
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}