first commit
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
tls
|
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
10
.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
19
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintUnsafeImplicitIntentLaunch" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
10
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
17
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
66
app/build.gradle.kts
Normal file
@@ -0,0 +1,66 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.tls"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.tls"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
|
||||
// 网络和TLS相关依赖
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@@ -0,0 +1,24 @@
|
||||
package com.example.tls
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.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.example.tls", appContext.packageName)
|
||||
}
|
||||
}
|
30
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Tls">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Tls">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
58
app/src/main/assets/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 证书文件说明
|
||||
|
||||
请将您的证书文件放在此目录下:
|
||||
|
||||
- 客户端证书: `client.crt` (必需,用于客户端证书认证)
|
||||
- 客户端私钥: `client.key` (必需,用于客户端证书认证)
|
||||
- CA证书: `ca.crt` (可选,用于服务器证书验证)
|
||||
|
||||
## 证书格式要求
|
||||
|
||||
### 客户端证书 (client.crt)
|
||||
- 格式: PEM格式的X.509证书
|
||||
- 内容示例:
|
||||
```
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC...证书内容...
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
### 客户端私钥 (client.key)
|
||||
- 格式: PEM格式的PKCS#8私钥(未加密)
|
||||
- 内容示例:
|
||||
```
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**重要**: 私钥必须是PKCS#8格式,不能是PKCS#1格式(RSA PRIVATE KEY)。
|
||||
|
||||
## 服务器错误解决
|
||||
|
||||
如果服务器报错 "peer did not return a certificate",说明服务器要求客户端证书认证。
|
||||
|
||||
### 解决方案:
|
||||
1. 将客户端证书文件 `client.crt` 和 `client.key` 放在此目录下
|
||||
2. 确保证书文件格式正确
|
||||
3. 重新运行应用进行测试
|
||||
|
||||
### 证书生成示例(使用OpenSSL):
|
||||
```bash
|
||||
# 生成私钥(PKCS#8格式)
|
||||
openssl genpkey -algorithm RSA -out client.key -pkcs8 -outform PEM
|
||||
|
||||
# 生成证书请求
|
||||
openssl req -new -key client.key -out client.csr
|
||||
|
||||
# 生成证书(自签名)
|
||||
openssl x509 -req -in client.csr -signkey client.key -out client.crt -days 365
|
||||
```
|
||||
|
||||
### 私钥格式转换(如果已有PKCS#1格式私钥):
|
||||
```bash
|
||||
# 将PKCS#1格式转换为PKCS#8格式
|
||||
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in old_key.key -out client.key
|
||||
```
|
||||
|
||||
如果没有证书文件,应用将尝试不使用客户端证书进行连接,但可能被服务器拒绝。
|
22
app/src/main/assets/ca.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDozCCAougAwIBAgIUPJ2ri4qd4BpnUcb/6vCZ//2uYd8wDQYJKoZIhvcNAQEL
|
||||
BQAwYTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
|
||||
aWppbmcxDjAMBgNVBAoMBU15T3JnMQswCQYDVQQLDAJJVDERMA8GA1UEAwwITXlS
|
||||
b290Q0EwHhcNMjUxMDA3MDYwMDI3WhcNMzUxMDA1MDYwMDI3WjBhMQswCQYDVQQG
|
||||
EwJDTjEQMA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UE
|
||||
CgwFTXlPcmcxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhNeVJvb3RDQTCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMf0LkX1LF71yhsnqM4laww8B6RRo3nF
|
||||
UD8pzGr4J8iFhtPyUr8T4Q3B1Et98sV+7zx4qxgYaMZ1IMX6QDti/T1qurAONhy9
|
||||
/HDGDrvMYE0AOQhvRvcqicpayqNqjLBJgdzh92Uei5s8F30tqLVm8tN715UaVDuc
|
||||
enjh6s9e1GpR6CzvbKLEmsUESC9YFeV1kdlbn+b5cFgDElBan2nEpS2sAUamc5pt
|
||||
+h8cjwojd58o3s+Cfc69DXnbSzRVo/7DSLO0itBLNvoo1YBSmJkuGDzsu0dcUD3m
|
||||
84scYvxzvOZiUqESEaqXRMPw7OcOAQSbjloUgWfiX35ntmdeK5xyk90CAwEAAaNT
|
||||
MFEwHQYDVR0OBBYEFH2YOXkKFAfeuB+IXTTB5NjXFsBqMB8GA1UdIwQYMBaAFH2Y
|
||||
OXkKFAfeuB+IXTTB5NjXFsBqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBAA7+FwMG6yJi9vDHdaFRWA/RFSTBCo5bcFJ7DO7DwvsEOfHnP5ygulDY
|
||||
astft8RsGelVSlXJtWHaCdByST9V282q+P2Q2jjwfmVB5ATq2MAgobYsU3nwLg3r
|
||||
c6zd34GeTJ6wjyGF5g3rDG8NqcHlARRwL8GbWX5lbcq9ooYLl9KHgFY+gz6u5njx
|
||||
WyJdnCMLRUgTt5tsT/3k5PU2V9C5kwZh/eAocEsfhuXDplFHeqdvZTZaXD/oX+So
|
||||
jPnLL6fsAoCskIhVVM0xGAXtWmhk+1LVtrqCfTQrqBhtXi5qCl3NLmBf+d0OClwh
|
||||
HgkcqJYr+WaiEHLUyIdfZji/ZEml7aA=
|
||||
-----END CERTIFICATE-----
|
28
app/src/main/assets/ca.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDH9C5F9Sxe9cob
|
||||
J6jOJWsMPAekUaN5xVA/Kcxq+CfIhYbT8lK/E+ENwdRLffLFfu88eKsYGGjGdSDF
|
||||
+kA7Yv09arqwDjYcvfxwxg67zGBNADkIb0b3KonKWsqjaoywSYHc4fdlHoubPBd9
|
||||
Lai1ZvLTe9eVGlQ7nHp44erPXtRqUegs72yixJrFBEgvWBXldZHZW5/m+XBYAxJQ
|
||||
Wp9pxKUtrAFGpnOabfofHI8KI3efKN7Pgn3OvQ1520s0VaP+w0iztIrQSzb6KNWA
|
||||
UpiZLhg87LtHXFA95vOLHGL8c7zmYlKhEhGql0TD8OznDgEEm45aFIFn4l9+Z7Zn
|
||||
XiuccpPdAgMBAAECggEAIXon7y2LxsBXHLHIqO8J26wHSYMjoCUheNnKMFSo8IEu
|
||||
oDivku9EnFWJ8jO9nERS0KiRWMDpdeSxXoQ2EdtSc+B1LjnK5IgIhmcam2Wt7+Zs
|
||||
JhXfZ013cWo/CBo0QOWluPIaRhNVo2Ftu1cUKn74g+D1qLCWTr61oJyOgDar0Lrn
|
||||
Cf6DvZl5pGJVV3lQUiYNjZR5qEywpaMvVcVjRUaUElNplld9VLm9xE8lhwjq0GER
|
||||
LQKt63p6b34kUKM8dS4cWr7+EutQPEwMLPtM/0HBUpid0qRIj5iZtROo86g8ksS1
|
||||
vzGQJ5asfgiRdZX7tkrj31RDg4latXQ8saHfZeOLzwKBgQDu0jI25NKHpxbWOveS
|
||||
OBtZK12aiO4n/nria3YsOTGCWYqtFUsZqcfj7/C30KH0vKODNoy9933+5roEdOvc
|
||||
FIo6PDUADiZNGN3Oy69EhBVuGr4OkK2LjjCSSI2jvWONTDN4lQ17nAiiJ0LMT/TX
|
||||
CBIef0i/QpAOYeuUh8FT1Ph8DwKBgQDWVkIfGLXFGeRORjUBuVv0ceQMR1K8Nz0/
|
||||
ldR0nuwoxMReSgoP2OVlSXqiLWAC6zPuXxE8KBo7GkvxEc4+AcwcgPZoAkK0KGBO
|
||||
BCyS6V45abp56nanAxgGZJ/BNd3MEma9rbraFjC/yF29heGeE0s+gR6uPMW32qJe
|
||||
U8nBJFj1UwKBgCvxm3HEWwTA9w/GW+WY01duBlQ4G/JZ/gyJj34FrBl7FmxQvbfk
|
||||
KLbFYLrB9fsNdtze/bi6wIFVvSayyO9/DAw5JdtzvxJyn+W8TuzBjRvsacpOTtCe
|
||||
Akv4c6+MWrQWMGZgrtFu3ZvQs5bao4epoYPhEea3fcBXvjxfWnBtgKd7AoGAeTT8
|
||||
XWN230heEFmpfhkZRCnnwX3P7rn6O+v54h1BBWkIdx29hOquBtI/tFiek+f4TROb
|
||||
xn4TH1smmOPt0qjniTLwpS6qFAFFPLklj8rCywrcNjd988JPIsZihTt1+wJo8Vi+
|
||||
crfbx4iCYjvEs8TLZ0RTWkrpsKfF7DvLuxpX6BsCgYBygdo3EGCuKlymw1V8xrWU
|
||||
rumFLCqtTxUfTu4hzWpPM6wDBh6BNMOeXw8Z7bE69Ha8rWxGpu0qEgRM2RegNJza
|
||||
f2j+hZUZbdJsGoClpq3ROBwBJHZ3ZUs+4po3JWQWc0DblaMOZftVIAh7EbMhmVPe
|
||||
OwMin2eLv4aEzSEtSYK8aQ==
|
||||
-----END PRIVATE KEY-----
|
20
app/src/main/assets/client.crt
Normal file
@@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDTDCCAjQCFDcqUklN76USvOskALvfIUttDamKMA0GCSqGSIb3DQEBCwUAMGEx
|
||||
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
|
||||
MQ4wDAYDVQQKDAVNeU9yZzELMAkGA1UECwwCSVQxETAPBgNVBAMMCE15Um9vdENB
|
||||
MB4XDTI1MTAwNzA2MDE1NFoXDTI2MTAwNzA2MDE1NFowZDELMAkGA1UEBhMCQ04x
|
||||
EDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15
|
||||
T3JnMQ8wDQYDVQQLDAZDbGllbnQxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwT
|
||||
FyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miubkb6hbP7695GERB/wVDTj
|
||||
nMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9N
|
||||
ukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx
|
||||
+gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4
|
||||
xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iDBlt6dNafAgMBAAEwDQYJ
|
||||
KoZIhvcNAQELBQADggEBAHV2eD8V79IF/u74mXjQxEalAJ0nA/ooMASbDIx08/wy
|
||||
S4mwZl08yMFRHwV4nMp6kbRh81cWYtTe8+LW8w1mHUKd/7MzStKT93IADPULFjCi
|
||||
4ee0wJmQePnWDxOcUUz6zEfh72W2Fy1CrZ99R0ZMF5ifUGUPNfmj0NsfqGgEReOO
|
||||
ai3yNTEg0zg35+2SnHzodFerKWDeis5gP5EOURfHcKYKE3oEatxwn9//gb/sICoR
|
||||
99rxHTO4BZB06DIArdxomZ84eanBkHGsJGuPjncR9fyLQcbP1SP0WUcR2p73xAd3
|
||||
EMBByvrT1m68g7L0SVqN05EshJoYWONb4gTHd2BCO0A=
|
||||
-----END CERTIFICATE-----
|
17
app/src/main/assets/client.csr
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICqTCCAZECAQAwZDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO
|
||||
BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZDbGllbnQx
|
||||
EDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF
|
||||
9khaJZn6wN//miubkb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5v
|
||||
fP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV
|
||||
4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncH
|
||||
ARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2Y
|
||||
mRUoe5bAnZiMk3iDBlt6dNafAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAjnuL
|
||||
FPH9DnQFg8yxNrD9XDDXGxrbRQEJqQeMJotLpafDk6Y8JROUrG9+v8p0KfdXn++6
|
||||
uZHAqsYMhTej6lK7ECI4RYt1vnmZ6V8Cwj/RaY+wqUVhx3UhDrcrbhjvf4YzZIQZ
|
||||
ObcFsGc6qHfjKCENQvwPP7aKxyK9/mMGbXBDhT/8wACsYRh4HL/6Rr+Uad9n7q+j
|
||||
y19s6h+SkkGST9feE4X4dWCx87w5us9QTRIywD4/L8IlzJXGEhZ285QxOokw/R7t
|
||||
pQCkKksycajW7cu7kIZOsTus9wW2vyBhsfJnzTfZq8Qrqyt53C1d431Z8uD9yLMa
|
||||
SfxqqgmsSUSLjfw3nA==
|
||||
-----END CERTIFICATE REQUEST-----
|
28
app/src/main/assets/client.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaJLDgelGrpFYg
|
||||
FKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miub
|
||||
kb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2Vlj
|
||||
BbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNq
|
||||
I7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CH
|
||||
PtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iD
|
||||
Blt6dNafAgMBAAECggEAHzak1qAOX7qEcjSdH1ugPbkSeoL3h6iNK7R4UiJ62UOk
|
||||
N/fnTap289OgyIXTq3Emz5JjruJVubWndPY7awbeT0XIoscEzK7QkvLRdqQCuoc0
|
||||
+5MSHIHUKs2eZEErQNpH5mOumo04Dr+5mRKzoH3pskJa74BAuK/BaHT7ilnlqmJp
|
||||
xoVDilQC5jiGys4MpCtx2Kt8l2CAlsE84vZff+PGV4O0kDsqqky6uhuJtrTaZs7j
|
||||
bFJoWcG9L9s/WZtwb32X11epHOmQv6mrzgFyminzkBoCmO7cBNB44eyQJnnNYece
|
||||
MnFdklKCPuSYc0lgWSepuUkrz8hLKC8+QzQO668UsQKBgQDYfmM9ES087q+H9ooc
|
||||
l/GZsEdV/iN+Kcm1JkyyypzRQrmVUTyAp9SFRaLiQP3ea6Jy/CUeCC1SWwPLJM9E
|
||||
EA0jlCYIPFOhsT7I1EM5Qa7DWf1z8bAVUMs1cqZrzOegIbEiKSu+Ym4MMVtvKvON
|
||||
OqaTtwwNUvyxVHmsuZBUZzzXOQKBgQC2RZLvI4p/Qzbpznsmq4beCSFEACRJTa/F
|
||||
e58/Xbmfdwo42WU17548k7sN/Jgyg7PVu4+bY5ZAYMibXFtEHW+sBz/4Gf90BZxE
|
||||
egiKD7gtEw4kxu8BfygJgWyszK4guzTYKx+DJRl2xtvGCNX10SrV1rEfHvYNHXj8
|
||||
BekIht0ElwKBgQCb/FSch2fE42Vt3WEdwQy+45hCiV4hZRKEhxf0KrBaxmzY/TNO
|
||||
r54ceFQoGRPR0lO17Z8AyHt/Pzy4fckpDTeqTvAoNu87LW5DXU0iUAUPlCNeCuII
|
||||
ObJwzC7EtVqesifiqS9veZQ5DMcIjjX1qDClddolL4oKawdQQFORvODFYQKBgB/l
|
||||
FM0b3wRd8qH/K7WclkEMP/HyRGc/XN6lvzwLXov0/KjuAbPqdjoLb9QGu2s7eKCR
|
||||
7ZM3Xfdt+CyXgLDupbfonN0BT54xzSJ+aDgggA4DI5pz5SbR5WOkbivetSmtGJYr
|
||||
FZyRRV9vdM22hho5u9EnfF8Bv/STj7QqJJkFYG+JAoGBAKx9evvrTS1tgcduIZXm
|
||||
fFQJ/Y4r2AtUqPs5TE69wdQg+WL35Njd0r3gguK4isur4NV3Yb97JUN95L9E2cY2
|
||||
asRkLNGiWwgFtSp6xTA6ltfLbdW+smZgeFkMTu8O6WBUiep6TEkRiR60UnKxDfwO
|
||||
eNr+BzEVtLNmY43pf3uRjnfm
|
||||
-----END PRIVATE KEY-----
|
203
app/src/main/java/com/example/tls/CertificateManager.kt
Normal file
@@ -0,0 +1,203 @@
|
||||
package com.example.tls
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.InputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.net.ssl.X509KeyManager
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLSession
|
||||
import java.security.KeyFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.RSAPrivateCrtKeySpec
|
||||
import java.util.Base64
|
||||
import java.io.ByteArrayInputStream
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||
import org.bouncycastle.openssl.PEMParser
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||||
import java.io.StringReader
|
||||
|
||||
class CertificateManager(private val context: Context) {
|
||||
|
||||
fun loadCertificateFromAssets(fileName: String): X509Certificate? {
|
||||
return try {
|
||||
val inputStream: InputStream = context.assets.open(fileName)
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
certificateFactory.generateCertificate(inputStream) as X509Certificate
|
||||
} catch (e: Exception) {
|
||||
Log.e("CertificateManager", "加载证书失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createTrustManagerWithCustomCert(certificate: X509Certificate): X509TrustManager {
|
||||
return object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
// 客户端证书验证
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
// 服务器证书验证 - 这里可以添加自定义验证逻辑
|
||||
for (cert in chain) {
|
||||
if (cert == certificate) {
|
||||
return // 信任我们的自定义证书
|
||||
}
|
||||
}
|
||||
// 如果不匹配,可以选择抛出异常或使用默认验证
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return arrayOf(certificate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createDefaultTrustManager(): X509TrustManager {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(null as KeyStore?)
|
||||
return trustManagerFactory.trustManagers[0] as X509TrustManager
|
||||
}
|
||||
|
||||
fun createCustomTrustManager(): X509TrustManager {
|
||||
return object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
// 接受所有客户端证书
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
// 接受所有服务器证书 - 仅用于测试
|
||||
Log.d("CertificateManager", "接受服务器证书: ${chain[0].subjectDN}")
|
||||
Log.d("CertificateManager", "证书颁发者: ${chain[0].issuerDN}")
|
||||
Log.d("CertificateManager", "证书有效期: ${chain[0].notBefore} 到 ${chain[0].notAfter}")
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return arrayOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPrivateKeyFromAssets(fileName: String): PrivateKey? {
|
||||
return try {
|
||||
val inputStream: InputStream = context.assets.open(fileName)
|
||||
val keyContent = inputStream.readBytes().toString(Charsets.UTF_8)
|
||||
|
||||
// 首先尝试简单的Base64解码(适用于PKCS#8格式)
|
||||
if (keyContent.contains("-----BEGIN PRIVATE KEY-----")) {
|
||||
Log.d("CertificateManager", "检测到PKCS#8格式私钥,使用标准解析")
|
||||
val keyBytes = extractKeyBytes(keyContent, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
|
||||
val keySpec = PKCS8EncodedKeySpec(keyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
keyFactory.generatePrivate(keySpec)
|
||||
} else if (keyContent.contains("-----BEGIN RSA PRIVATE KEY-----")) {
|
||||
Log.d("CertificateManager", "检测到PKCS#1格式私钥,建议转换为PKCS#8格式")
|
||||
Log.e("CertificateManager", "PKCS#1格式私钥需要转换,请运行转换脚本")
|
||||
Log.e("CertificateManager", "Linux/Mac: ./convert_private_key.sh")
|
||||
Log.e("CertificateManager", "Windows: convert_private_key.bat")
|
||||
null
|
||||
} else {
|
||||
Log.e("CertificateManager", "无法识别私钥格式")
|
||||
Log.e("CertificateManager", "请确保私钥是PEM格式(PKCS#8或PKCS#1)")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("CertificateManager", "加载私钥失败: ${e.message}")
|
||||
Log.e("CertificateManager", "错误类型: ${e.javaClass.simpleName}")
|
||||
|
||||
// 提供更详细的错误信息
|
||||
when {
|
||||
e.message?.contains("parsing") == true -> {
|
||||
Log.e("CertificateManager", "私钥格式错误,请检查PEM格式是否正确")
|
||||
}
|
||||
e.message?.contains("encrypted") == true -> {
|
||||
Log.e("CertificateManager", "私钥已加密,请使用未加密的私钥")
|
||||
}
|
||||
e.message?.contains("PKCS8") == true -> {
|
||||
Log.e("CertificateManager", "请使用PKCS#8格式的私钥")
|
||||
}
|
||||
else -> {
|
||||
Log.e("CertificateManager", "请确保私钥是PEM格式且未加密")
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractKeyBytes(keyContent: String, beginMarker: String, endMarker: String): ByteArray {
|
||||
val lines = keyContent.split("\n")
|
||||
val keyLines = lines.filter {
|
||||
!it.startsWith("-----") && it.trim().isNotEmpty()
|
||||
}
|
||||
val keyString = keyLines.joinToString("")
|
||||
return Base64.getDecoder().decode(keyString)
|
||||
}
|
||||
|
||||
fun createKeyManager(certificate: X509Certificate, privateKey: PrivateKey): X509KeyManager {
|
||||
return object : X509KeyManager {
|
||||
override fun chooseClientAlias(keyType: Array<String>, issuers: Array<java.security.Principal>, socket: java.net.Socket): String {
|
||||
return "client"
|
||||
}
|
||||
|
||||
override fun chooseServerAlias(keyType: String, issuers: Array<java.security.Principal>, socket: java.net.Socket): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getCertificateChain(alias: String): Array<X509Certificate> {
|
||||
return arrayOf(certificate)
|
||||
}
|
||||
|
||||
override fun getClientAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
|
||||
return arrayOf("client")
|
||||
}
|
||||
|
||||
override fun getServerAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
|
||||
return arrayOf()
|
||||
}
|
||||
|
||||
override fun getPrivateKey(alias: String): PrivateKey {
|
||||
return privateKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createKeyManagerFactory(): KeyManagerFactory? {
|
||||
return try {
|
||||
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(null, null)
|
||||
keyManagerFactory
|
||||
} catch (e: Exception) {
|
||||
Log.e("CertificateManager", "创建KeyManagerFactory失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createCustomHostnameVerifier(): HostnameVerifier {
|
||||
return HostnameVerifier { hostname, session ->
|
||||
Log.d("CertificateManager", "验证主机名: $hostname")
|
||||
Log.d("CertificateManager", "SSL会话: ${session.protocol}")
|
||||
|
||||
// 对于IP地址,跳过主机名验证
|
||||
if (hostname.matches(Regex("^\\d+\\.\\d+\\.\\d+\\.\\d+$"))) {
|
||||
Log.d("CertificateManager", "IP地址主机名验证通过: $hostname")
|
||||
return@HostnameVerifier true
|
||||
}
|
||||
|
||||
// 对于域名,进行标准验证
|
||||
try {
|
||||
val result = javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)
|
||||
Log.d("CertificateManager", "域名验证结果: $result")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.w("CertificateManager", "主机名验证异常: ${e.message}")
|
||||
// 对于测试环境,允许所有主机名
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
338
app/src/main/java/com/example/tls/MainActivity.kt
Normal file
@@ -0,0 +1,338 @@
|
||||
package com.example.tls
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.tls.ui.theme.TlsTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var tlsTestService: TlsTestService
|
||||
// 可配置的测试服务器列表
|
||||
private val testServers = listOf(
|
||||
// "https://httpbin.org/post",
|
||||
// "https://www.google.com",
|
||||
// "https://www.cloudflare.com",
|
||||
// "https://www.github.com",
|
||||
"https://43.162.113.116:7271", // 示例IP+端口
|
||||
// "https://10.0.0.1:443", // 示例内网IP
|
||||
// "https://localhost:8080" // 示例本地服务器
|
||||
)
|
||||
private var currentServerIndex = 0
|
||||
private var customServerUrl by mutableStateOf("")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
tlsTestService = TlsTestService(this)
|
||||
setContent {
|
||||
TlsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
TlsTestScreen(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
tlsTestService = tlsTestService,
|
||||
serverUrl = if (customServerUrl.isNotEmpty()) customServerUrl else testServers[currentServerIndex],
|
||||
testServers = testServers,
|
||||
customServerUrl = customServerUrl,
|
||||
onServerChange = { index ->
|
||||
currentServerIndex = index
|
||||
customServerUrl = ""
|
||||
},
|
||||
onCustomServerChange = { url -> customServerUrl = url }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TlsTestScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
tlsTestService: TlsTestService,
|
||||
serverUrl: String,
|
||||
testServers: List<String> = emptyList(),
|
||||
customServerUrl: String = "",
|
||||
onServerChange: (Int) -> Unit = {},
|
||||
onCustomServerChange: (String) -> Unit = {}
|
||||
) {
|
||||
var logs by remember { mutableStateOf(listOf<String>()) }
|
||||
var isTestRunning by remember { mutableStateOf(false) }
|
||||
var tls12Result by remember { mutableStateOf<TestResult?>(null) }
|
||||
var tls13Result by remember { mutableStateOf<TestResult?>(null) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 自动滚动到最新日志
|
||||
LaunchedEffect(logs.size) {
|
||||
if (logs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(logs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 标题
|
||||
Text(
|
||||
text = "TLS 1.2/1.3 循环测试",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// 服务器选择
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F0F0))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "测试服务器",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
// 自定义服务器输入
|
||||
OutlinedTextField(
|
||||
value = customServerUrl,
|
||||
onValueChange = onCustomServerChange,
|
||||
label = { Text("自定义服务器 (IP:端口)") },
|
||||
placeholder = { Text("例如: https://192.168.1.100:8443") },
|
||||
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
|
||||
keyboardType = KeyboardType.Uri
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// 预设服务器列表
|
||||
if (testServers.isNotEmpty()) {
|
||||
Text(
|
||||
text = "预设服务器:",
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
|
||||
testServers.forEachIndexed { index, server ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = customServerUrl.isEmpty() && server == serverUrl,
|
||||
onClick = { onServerChange(index) }
|
||||
)
|
||||
Text(
|
||||
text = server,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当前选择的服务器显示
|
||||
if (serverUrl.isNotEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFE3F2FD))
|
||||
) {
|
||||
Text(
|
||||
text = "当前服务器: $serverUrl",
|
||||
modifier = Modifier.padding(12.dp),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF1976D2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 控制按钮
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!isTestRunning) {
|
||||
isTestRunning = true
|
||||
logs = listOf("开始TLS测试...")
|
||||
tls12Result = null
|
||||
tls13Result = null
|
||||
|
||||
scope.launch {
|
||||
tlsTestService.startContinuousTest(
|
||||
serverUrl = serverUrl,
|
||||
onLog = { log ->
|
||||
logs = logs + log
|
||||
},
|
||||
onTestComplete = { tls12, tls13 ->
|
||||
tls12Result = tls12
|
||||
tls13Result = tls13
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isTestRunning,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("开始测试")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
isTestRunning = false
|
||||
logs = logs + "测试已停止"
|
||||
},
|
||||
enabled = isTestRunning,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("停止测试")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 测试结果显示
|
||||
if (tls12Result != null || tls13Result != null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "最新测试结果",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
tls12Result?.let { result ->
|
||||
TestResultItem("TLS 1.2", result)
|
||||
}
|
||||
|
||||
tls13Result?.let { result ->
|
||||
TestResultItem("TLS 1.3", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 日志显示区域
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Black)
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "测试日志",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(logs) { log ->
|
||||
Text(
|
||||
text = log,
|
||||
color = Color.Green,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestResultItem(protocol: String, result: TestResult) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = protocol,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is TestResult.Success -> {
|
||||
Text(
|
||||
text = "成功 - ${result.duration}ms",
|
||||
color = Color.Green,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
is TestResult.Failure -> {
|
||||
Text(
|
||||
text = "失败 - ${result.error}",
|
||||
color = Color.Red,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun TlsTestScreenPreview() {
|
||||
TlsTheme {
|
||||
// Preview中不显示实际功能
|
||||
Text("TLS测试应用预览")
|
||||
}
|
||||
}
|
279
app/src/main/java/com/example/tls/TlsTestService.kt
Normal file
@@ -0,0 +1,279 @@
|
||||
package com.example.tls
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TlsTestService(private val context: Context) {
|
||||
private val certificateManager = CertificateManager(context)
|
||||
|
||||
// 尝试加载客户端证书
|
||||
private val clientCertificate = certificateManager.loadCertificateFromAssets("client.crt")
|
||||
private val clientPrivateKey = certificateManager.loadPrivateKeyFromAssets("client.key")
|
||||
private val hasClientCert = clientCertificate != null && clientPrivateKey != null
|
||||
|
||||
init {
|
||||
// 在初始化时记录证书加载状态
|
||||
if (hasClientCert) {
|
||||
Log.d("TlsTestService", "客户端证书加载成功")
|
||||
} else {
|
||||
Log.w("TlsTestService", "客户端证书加载失败或未找到")
|
||||
if (clientCertificate == null) Log.w("TlsTestService", "证书文件 client.crt 未找到或格式错误")
|
||||
if (clientPrivateKey == null) Log.w("TlsTestService", "私钥文件 client.key 未找到或格式错误")
|
||||
}
|
||||
}
|
||||
|
||||
// 为每次测试创建新的客户端,避免SSL会话重用问题
|
||||
private fun createTls12Client(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.sslSocketFactory(
|
||||
createTls12SocketFactory(),
|
||||
certificateManager.createCustomTrustManager()
|
||||
)
|
||||
.hostnameVerifier(certificateManager.createCustomHostnameVerifier())
|
||||
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createTls13Client(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.sslSocketFactory(
|
||||
createTls13SocketFactory(),
|
||||
certificateManager.createCustomTrustManager()
|
||||
)
|
||||
.hostnameVerifier(certificateManager.createCustomHostnameVerifier())
|
||||
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createTls12SocketFactory(): SSLSocketFactory {
|
||||
val sslContext = SSLContext.getInstance("TLSv1.2")
|
||||
val keyManagers = if (hasClientCert) {
|
||||
arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null)
|
||||
return sslContext.socketFactory
|
||||
}
|
||||
|
||||
private fun createTls13SocketFactory(): SSLSocketFactory {
|
||||
val sslContext = SSLContext.getInstance("TLSv1.3")
|
||||
val keyManagers = if (hasClientCert) {
|
||||
arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null)
|
||||
return sslContext.socketFactory
|
||||
}
|
||||
|
||||
suspend fun testTls12(serverUrl: String, onLog: (String) -> Unit): TestResult {
|
||||
return withContext(Dispatchers.IO) {
|
||||
var response: okhttp3.Response? = null
|
||||
try {
|
||||
onLog("开始TLS 1.2测试...")
|
||||
onLog("目标服务器: $serverUrl")
|
||||
onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}")
|
||||
|
||||
val testData = createTestData()
|
||||
onLog("准备发送数据: ${testData.length} 字节")
|
||||
logTestDataInfo(testData, onLog)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(serverUrl)
|
||||
.post(testData.toRequestBody("text/plain".toMediaType()))
|
||||
.addHeader("Content-Type", "text/plain")
|
||||
.addHeader("User-Agent", "TLS-Test-Client/1.0")
|
||||
.build()
|
||||
|
||||
onLog("正在建立TLS 1.2连接...")
|
||||
onLog("目标主机: ${java.net.URL(serverUrl).host}")
|
||||
val tls12Client = createTls12Client() // 创建新的客户端实例
|
||||
response = tls12Client.newCall(request).execute()
|
||||
val endTime = System.currentTimeMillis()
|
||||
val duration = endTime - startTime
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
onLog("TLS 1.2连接成功 - 耗时: ${duration}ms")
|
||||
onLog("已发送数据: ${testData.length} 字节")
|
||||
onLog("服务器响应长度: ${responseBody.length} 字节")
|
||||
onLog("TLS版本: ${response.protocol}")
|
||||
onLog("HTTP状态: ${response.code}")
|
||||
logResponseInfo(responseBody, onLog)
|
||||
TestResult.Success(duration, responseBody.length)
|
||||
} else {
|
||||
onLog("TLS 1.2测试失败 - HTTP ${response.code}")
|
||||
onLog("响应头: ${response.headers}")
|
||||
TestResult.Failure("HTTP ${response.code}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onLog("TLS 1.2测试异常: ${e.message}")
|
||||
onLog("异常类型: ${e.javaClass.simpleName}")
|
||||
TestResult.Failure(e.message ?: "未知错误")
|
||||
} finally {
|
||||
// 确保连接被正确关闭
|
||||
try {
|
||||
response?.close()
|
||||
} catch (e: Exception) {
|
||||
Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testTls13(serverUrl: String, onLog: (String) -> Unit): TestResult {
|
||||
return withContext(Dispatchers.IO) {
|
||||
var response: okhttp3.Response? = null
|
||||
try {
|
||||
onLog("开始TLS 1.3测试...")
|
||||
onLog("目标服务器: $serverUrl")
|
||||
onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}")
|
||||
|
||||
val testData = createTestData()
|
||||
onLog("准备发送数据: ${testData.length} 字节")
|
||||
logTestDataInfo(testData, onLog)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(serverUrl)
|
||||
.post(testData.toRequestBody("text/plain".toMediaType()))
|
||||
.addHeader("Content-Type", "text/plain")
|
||||
.addHeader("User-Agent", "TLS-Test-Client/1.0")
|
||||
.build()
|
||||
|
||||
onLog("正在建立TLS 1.3连接...")
|
||||
onLog("目标主机: ${java.net.URL(serverUrl).host}")
|
||||
val tls13Client = createTls13Client() // 创建新的客户端实例
|
||||
response = tls13Client.newCall(request).execute()
|
||||
val endTime = System.currentTimeMillis()
|
||||
val duration = endTime - startTime
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
onLog("TLS 1.3连接成功 - 耗时: ${duration}ms")
|
||||
onLog("已发送数据: ${testData.length} 字节")
|
||||
onLog("服务器响应长度: ${responseBody.length} 字节")
|
||||
onLog("TLS版本: ${response.protocol}")
|
||||
onLog("HTTP状态: ${response.code}")
|
||||
logResponseInfo(responseBody, onLog)
|
||||
TestResult.Success(duration, responseBody.length)
|
||||
} else {
|
||||
onLog("TLS 1.3测试失败 - HTTP ${response.code}")
|
||||
onLog("响应头: ${response.headers}")
|
||||
TestResult.Failure("HTTP ${response.code}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onLog("TLS 1.3测试异常: ${e.message}")
|
||||
onLog("异常类型: ${e.javaClass.simpleName}")
|
||||
TestResult.Failure(e.message ?: "未知错误")
|
||||
} finally {
|
||||
// 确保连接被正确关闭
|
||||
try {
|
||||
response?.close()
|
||||
} catch (e: Exception) {
|
||||
Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun create1KDataRequestBody(): RequestBody {
|
||||
val data = "A".repeat(1024) // 创建1KB的数据
|
||||
return data.toRequestBody("text/plain".toMediaType())
|
||||
}
|
||||
|
||||
private fun createTestData(): String {
|
||||
// 创建包含时间戳的测试数据,确保每次发送的数据都不同
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val testData = "TLS_TEST_DATA_${timestamp}_"
|
||||
val remainingSize = 1024 - testData.length
|
||||
val padding = "A".repeat(remainingSize.coerceAtLeast(0))
|
||||
return testData + padding
|
||||
}
|
||||
|
||||
private fun logTestDataInfo(testData: String, onLog: (String) -> Unit) {
|
||||
onLog("测试数据详情:")
|
||||
onLog(" - 数据长度: ${testData.length} 字节")
|
||||
onLog(" - 数据前缀: ${testData.take(50)}...")
|
||||
onLog(" - 数据后缀: ...${testData.takeLast(20)}")
|
||||
}
|
||||
|
||||
private fun logResponseInfo(responseBody: String, onLog: (String) -> Unit) {
|
||||
onLog("服务器响应详情:")
|
||||
onLog(" - 响应长度: ${responseBody.length} 字节")
|
||||
if (responseBody.isNotEmpty()) {
|
||||
onLog(" - 响应前缀: ${responseBody.take(100)}...")
|
||||
if (responseBody.length > 100) {
|
||||
onLog(" - 响应后缀: ...${responseBody.takeLast(50)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSSLSession() {
|
||||
try {
|
||||
// 清理SSL会话缓存
|
||||
val sslContext = SSLContext.getDefault()
|
||||
val sessionContext = sslContext.clientSessionContext
|
||||
sessionContext.setSessionCacheSize(0)
|
||||
sessionContext.setSessionTimeout(0)
|
||||
Log.d("TlsTestService", "SSL会话缓存已清理")
|
||||
} catch (e: Exception) {
|
||||
Log.d("TlsTestService", "清理SSL会话时发生异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun startContinuousTest(
|
||||
serverUrl: String,
|
||||
onLog: (String) -> Unit,
|
||||
onTestComplete: (TestResult, TestResult) -> Unit
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
while (true) {
|
||||
try {
|
||||
onLog("=== 开始新一轮测试 ===")
|
||||
|
||||
// 测试TLS 1.2
|
||||
val tls12Result = testTls12(serverUrl, onLog)
|
||||
|
||||
// 测试TLS 1.3
|
||||
val tls13Result = testTls13(serverUrl, onLog)
|
||||
|
||||
onTestComplete(tls12Result, tls13Result)
|
||||
|
||||
// 清理SSL会话缓存,避免会话重用问题
|
||||
clearSSLSession()
|
||||
|
||||
onLog("等待3秒后进行下一轮测试...")
|
||||
delay(3000)
|
||||
|
||||
} catch (e: Exception) {
|
||||
onLog("测试过程中发生异常: ${e.message}")
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TestResult {
|
||||
data class Success(val duration: Long, val responseSize: Int) : TestResult()
|
||||
data class Failure(val error: String) : TestResult()
|
||||
}
|
11
app/src/main/java/com/example/tls/ui/theme/Color.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.example.tls.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
58
app/src/main/java/com/example/tls/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.example.tls.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TlsTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
34
app/src/main/java/com/example/tls/ui/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.example.tls.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 7.6 KiB |
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
</resources>
|
3
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">tls</string>
|
||||
</resources>
|
5
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Tls" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
17
app/src/test/java/com/example/tls/ExampleUnitTest.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.tls
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
6
build.gradle.kts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
23
gradle.properties
Normal file
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
32
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[versions]
|
||||
agp = "8.12.0"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
composeBom = "2024.09.00"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Thu Oct 09 21:54:23 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
185
gradlew
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
89
gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
23
settings.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "tls"
|
||||
include(":app")
|