add kotlin project

This commit is contained in:
asus
2026-04-22 19:52:54 +03:30
parent fb01a229fc
commit ab932abab4
15 changed files with 884 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "ir.masteralidns.androidproxy"
compileSdk = 34
defaultConfig {
applicationId = "ir.masteralidns.androidproxy"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
+1
View File
@@ -0,0 +1 @@
# Intentionally minimal for the first Android-native slice.
@@ -0,0 +1,30 @@
<?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.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.MasterAliDnsAndroid">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".ProxyService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
@@ -0,0 +1,393 @@
package ir.masteralidns.androidproxy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.InputStream
import java.io.OutputStream
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.net.URI
import java.nio.charset.StandardCharsets
import java.util.Locale
class LocalProxyEngine(
private val config: ProxyConfig,
private val log: (String) -> Unit,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val listeners = mutableListOf<ServerSocket>()
fun start() {
if (config.listenPort == config.socks5Port && config.socks5Enabled) {
throw IllegalArgumentException("HTTP and SOCKS5 ports must be different.")
}
listeners += bindListener(config.listenHost, config.listenPort)
scope.launch { acceptLoop(listeners[0], "HTTP", ::handleHttpClient) }
if (config.socks5Enabled) {
val socksListener = bindListener(config.listenHost, config.socks5Port)
listeners += socksListener
scope.launch { acceptLoop(socksListener, "SOCKS5", ::handleSocks5Client) }
}
}
fun stop() {
listeners.forEach(::closeQuietly)
listeners.clear()
scope.cancel()
}
private fun bindListener(host: String, port: Int): ServerSocket {
val server = ServerSocket()
server.reuseAddress = true
server.bind(InetSocketAddress(InetAddress.getByName(host), port))
return server
}
private suspend fun acceptLoop(
server: ServerSocket,
label: String,
handler: suspend (Socket) -> Unit,
) {
log("$label listener on ${server.inetAddress.hostAddress}:${server.localPort}")
while (scope.isActive && !server.isClosed) {
try {
val client = server.accept()
client.tcpNoDelay = true
scope.launch { handler(client) }
} catch (_: Exception) {
if (!server.isClosed) {
log("$label listener stopped unexpectedly")
}
return
}
}
}
private suspend fun handleHttpClient(client: Socket) {
client.use { socket ->
socket.soTimeout = 30_000
val input = BufferedInputStream(socket.getInputStream())
val output = BufferedOutputStream(socket.getOutputStream())
val headerBytes = readHeaderBlock(input) ?: return
val requestText = headerBytes.toString(StandardCharsets.ISO_8859_1)
val lines = requestText.split("\r\n")
val requestLine = lines.firstOrNull()?.trim().orEmpty()
if (requestLine.isEmpty()) {
return
}
val parts = requestLine.split(" ", limit = 3)
if (parts.size < 2) {
return
}
val method = parts[0].uppercase(Locale.US)
if (method == "CONNECT") {
val (host, port) = splitHostPort(parts[1], 443)
handleConnectTunnel(host, port, socket, input, output, "HTTP CONNECT")
return
}
val target = resolveHttpTarget(parts[1], lines)
if (target == null) {
sendSimpleResponse(output, 400, "Bad Request", "No target host")
return
}
log("HTTP ${target.host}:${target.port} ${target.path}")
Socket().use { remote ->
remote.connect(InetSocketAddress(target.host, target.port), 10_000)
remote.tcpNoDelay = true
val remoteOut = BufferedOutputStream(remote.getOutputStream())
val rebuilt = rebuildHttpRequest(parts, lines, target.path)
remoteOut.write(rebuilt.toByteArray(StandardCharsets.ISO_8859_1))
remoteOut.flush()
tunnel(socket, input, output, remote)
}
}
}
private suspend fun handleSocks5Client(client: Socket) {
client.use { socket ->
socket.soTimeout = 30_000
val input = BufferedInputStream(socket.getInputStream())
val output = BufferedOutputStream(socket.getOutputStream())
val version = input.read()
val methodCount = input.read()
if (version != 0x05 || methodCount <= 0) {
return
}
val methods = ByteArray(methodCount)
input.readFully(methods)
if (methods.none { it.toInt() == 0x00 }) {
output.write(byteArrayOf(0x05, 0xFF.toByte()))
output.flush()
return
}
output.write(byteArrayOf(0x05, 0x00))
output.flush()
val reqHead = ByteArray(4)
input.readFully(reqHead)
if (reqHead[0].toInt() != 0x05) {
return
}
val cmd = reqHead[1].toInt() and 0xFF
val atyp = reqHead[3].toInt() and 0xFF
if (cmd != 0x01) {
sendSocksReply(output, 0x07)
return
}
val host = readSocksAddress(input, atyp) ?: run {
sendSocksReply(output, 0x08)
return
}
val port = ((input.read() and 0xFF) shl 8) or (input.read() and 0xFF)
log("SOCKS5 $host:$port")
Socket().use { remote ->
remote.connect(InetSocketAddress(host, port), 10_000)
remote.tcpNoDelay = true
sendSocksReply(output, 0x00)
tunnel(socket, input, output, remote)
}
}
}
private suspend fun handleConnectTunnel(
host: String,
port: Int,
client: Socket,
clientInput: InputStream,
clientOutput: OutputStream,
label: String,
) {
log("$label $host:$port")
Socket().use { remote ->
remote.connect(InetSocketAddress(host, port), 10_000)
remote.tcpNoDelay = true
clientOutput.write("HTTP/1.1 200 Connection Established\r\n\r\n".toByteArray())
clientOutput.flush()
tunnel(client, clientInput, clientOutput, remote)
}
}
private suspend fun tunnel(
client: Socket,
clientInput: InputStream,
clientOutput: OutputStream,
remote: Socket,
) {
val remoteInput = BufferedInputStream(remote.getInputStream())
val remoteOutput = BufferedOutputStream(remote.getOutputStream())
val aToB: Job = scope.launch {
copyStream(clientInput, remoteOutput)
closeQuietly(remoteOutput)
}
val bToA: Job = scope.launch {
copyStream(remoteInput, clientOutput)
closeQuietly(clientOutput)
}
aToB.join()
bToA.join()
closeQuietly(client)
closeQuietly(remote)
}
private fun copyStream(input: InputStream, output: OutputStream) {
val buffer = ByteArray(16 * 1024)
while (true) {
val count = try {
input.read(buffer)
} catch (_: Exception) {
-1
}
if (count <= 0) {
break
}
try {
output.write(buffer, 0, count)
output.flush()
} catch (_: Exception) {
break
}
}
}
private fun readHeaderBlock(input: InputStream): ByteArray? {
val buffer = ByteArrayOutputStream()
var matched = 0
val sentinel = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte(), '\r'.code.toByte(), '\n'.code.toByte())
while (buffer.size() < 64 * 1024) {
val b = input.read()
if (b < 0) {
return if (buffer.size() == 0) null else buffer.toByteArray()
}
val value = b.toByte()
buffer.write(value.toInt())
matched = if (value == sentinel[matched]) matched + 1 else if (value == sentinel[0]) 1 else 0
if (matched == sentinel.size) {
return buffer.toByteArray()
}
}
return null
}
private fun splitHostPort(target: String, defaultPort: Int): Pair<String, Int> {
val value = target.trim()
if (value.startsWith("[")) {
val host = value.substringAfter("[").substringBefore("]")
val port = value.substringAfter("]:", defaultPort.toString()).toIntOrNull() ?: defaultPort
return host to port
}
val idx = value.lastIndexOf(':')
if (idx > 0 && value.indexOf(':') == idx) {
val host = value.substring(0, idx)
val port = value.substring(idx + 1).toIntOrNull() ?: defaultPort
return host to port
}
return value to defaultPort
}
private fun resolveHttpTarget(rawTarget: String, lines: List<String>): HttpTarget? {
return if (rawTarget.startsWith("http://") || rawTarget.startsWith("https://")) {
val uri = URI(rawTarget)
val host = uri.host ?: return null
val port = if (uri.port > 0) uri.port else if (uri.scheme == "https") 443 else 80
val path = buildString {
append(uri.rawPath ?: "/")
if (!uri.rawQuery.isNullOrBlank()) {
append('?')
append(uri.rawQuery)
}
}
HttpTarget(host, port, path)
} else {
val hostHeader = lines.firstOrNull { it.startsWith("Host:", ignoreCase = true) }
?.substringAfter(':')
?.trim()
?: return null
val (host, port) = splitHostPort(hostHeader, 80)
HttpTarget(host, port, rawTarget.ifBlank { "/" })
}
}
private fun rebuildHttpRequest(parts: List<String>, lines: List<String>, path: String): String {
val requestLine = buildString {
append(parts[0])
append(' ')
append(path)
if (parts.size >= 3) {
append(' ')
append(parts[2])
}
}
val rewrittenHeaders = buildList {
add(requestLine)
for (line in lines.drop(1)) {
if (line.isEmpty()) {
continue
}
val lower = line.substringBefore(':').trim().lowercase(Locale.US)
if (lower == "proxy-connection") {
continue
}
add(line)
}
}
return rewrittenHeaders.joinToString("\r\n", postfix = "\r\n\r\n")
}
private fun sendSimpleResponse(output: OutputStream, status: Int, reason: String, body: String) {
val bodyBytes = body.toByteArray(StandardCharsets.UTF_8)
val response = buildString {
append("HTTP/1.1 ")
append(status)
append(' ')
append(reason)
append("\r\nContent-Type: text/plain; charset=utf-8")
append("\r\nContent-Length: ")
append(bodyBytes.size)
append("\r\nConnection: close\r\n\r\n")
}.toByteArray(StandardCharsets.UTF_8)
output.write(response)
output.write(bodyBytes)
output.flush()
}
private fun sendSocksReply(output: OutputStream, code: Int) {
output.write(byteArrayOf(0x05, code.toByte(), 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00))
output.flush()
}
private fun readSocksAddress(input: InputStream, atyp: Int): String? {
return when (atyp) {
0x01 -> {
val bytes = ByteArray(4)
input.readFully(bytes)
bytes.joinToString(".") { (it.toInt() and 0xFF).toString() }
}
0x03 -> {
val len = input.read()
if (len <= 0) return null
val bytes = ByteArray(len)
input.readFully(bytes)
bytes.toString(StandardCharsets.US_ASCII)
}
0x04 -> {
val bytes = ByteArray(16)
input.readFully(bytes)
InetAddress.getByAddress(bytes).hostAddress
}
else -> null
}
}
private fun InputStream.readFully(dst: ByteArray) {
var offset = 0
while (offset < dst.size) {
val count = read(dst, offset, dst.size - offset)
if (count < 0) {
throw IllegalStateException("Unexpected EOF")
}
offset += count
}
}
private fun closeQuietly(closeable: Any?) {
when (closeable) {
is Closeable -> runCatching { closeable.close() }
is Socket -> runCatching { closeable.close() }
is ServerSocket -> runCatching { closeable.close() }
}
}
private data class HttpTarget(
val host: String,
val port: Int,
val path: String,
)
}
@@ -0,0 +1,65 @@
package ir.masteralidns.androidproxy
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
private lateinit var configEditor: EditText
private lateinit var statusView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
configEditor = findViewById(R.id.configEditor)
statusView = findViewById(R.id.statusView)
val startButton: Button = findViewById(R.id.startButton)
val stopButton: Button = findViewById(R.id.stopButton)
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
configEditor.setText(prefs.getString(KEY_CONFIG, ProxyConfig.defaultJson()))
startButton.setOnClickListener {
val rawConfig = configEditor.text.toString().trim()
if (rawConfig.isEmpty()) {
Toast.makeText(this, "Config is empty", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
runCatching { ProxyConfig.fromJson(rawConfig) }
.onFailure {
Toast.makeText(this, "Config error: ${it.message}", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
prefs.edit().putString(KEY_CONFIG, rawConfig).apply()
val intent = Intent(this, ProxyService::class.java).apply {
action = ProxyService.ACTION_START
putExtra(ProxyService.EXTRA_CONFIG, rawConfig)
}
ContextCompat.startForegroundService(this, intent)
statusView.setText(R.string.status_running)
}
stopButton.setOnClickListener {
ContextCompat.startForegroundService(
this,
Intent(this, ProxyService::class.java).apply {
action = ProxyService.ACTION_STOP
},
)
statusView.setText(R.string.status_idle)
}
}
companion object {
private const val PREFS_NAME = "proxy_prefs"
private const val KEY_CONFIG = "config_json"
}
}
@@ -0,0 +1,54 @@
package ir.masteralidns.androidproxy
import org.json.JSONArray
import org.json.JSONObject
data class ProxyConfig(
val listenHost: String = "127.0.0.1",
val listenPort: Int = 8085,
val socks5Enabled: Boolean = true,
val socks5Port: Int = 1080,
val directDomains: List<String> = emptyList(),
) {
fun toJsonString(): String {
val obj = JSONObject()
obj.put("listen_host", listenHost)
obj.put("listen_port", listenPort)
obj.put("socks5_enabled", socks5Enabled)
obj.put("socks5_port", socks5Port)
obj.put("direct_domains", JSONArray(directDomains))
return obj.toString(2)
}
companion object {
fun defaultConfig(): ProxyConfig = ProxyConfig(
directDomains = listOf(
"chatgpt.com",
"openai.com",
"oaistatic.com",
"oaiusercontent.com",
),
)
fun defaultJson(): String = defaultConfig().toJsonString()
fun fromJson(raw: String): ProxyConfig {
val obj = JSONObject(raw)
val directDomains = mutableListOf<String>()
val arr = obj.optJSONArray("direct_domains") ?: JSONArray()
for (i in 0 until arr.length()) {
val value = arr.optString(i).trim()
if (value.isNotEmpty()) {
directDomains += value
}
}
return ProxyConfig(
listenHost = obj.optString("listen_host", "127.0.0.1"),
listenPort = obj.optInt("listen_port", 8085),
socks5Enabled = obj.optBoolean("socks5_enabled", true),
socks5Port = obj.optInt("socks5_port", 1080),
directDomains = directDomains,
)
}
}
}
@@ -0,0 +1,131 @@
package ir.masteralidns.androidproxy
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
class ProxyService : Service() {
private var engine: LocalProxyEngine? = null
private var currentConfig: ProxyConfig? = null
override fun onCreate() {
super.onCreate()
ensureNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_STOP -> {
stopEngine()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
return START_NOT_STICKY
}
ACTION_START -> {
val rawConfig = intent.getStringExtra(EXTRA_CONFIG) ?: ProxyConfig.defaultJson()
return runCatching {
startWithConfig(ProxyConfig.fromJson(rawConfig))
START_STICKY
}.getOrElse { error ->
startForeground(NOTIFICATION_ID, buildNotification("Config error: ${error.message}"))
stopSelf()
START_NOT_STICKY
}
}
else -> return START_NOT_STICKY
}
}
override fun onDestroy() {
stopEngine()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun startWithConfig(config: ProxyConfig) {
stopEngine()
currentConfig = config
startForeground(NOTIFICATION_ID, buildNotification(summary(config)))
engine = LocalProxyEngine(config) { message ->
val active = currentConfig ?: return@LocalProxyEngine
val text = summary(active) + " | $message"
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, buildNotification(text))
}.also { it.start() }
}
private fun stopEngine() {
engine?.stop()
engine = null
}
private fun summary(config: ProxyConfig): String {
return if (config.socks5Enabled) {
"HTTP ${config.listenHost}:${config.listenPort} | SOCKS5 ${config.listenHost}:${config.socks5Port}"
} else {
"HTTP ${config.listenHost}:${config.listenPort}"
}
}
private fun buildNotification(text: String): Notification {
val openIntent = PendingIntent.getActivity(
this,
1,
Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val stopIntent = PendingIntent.getService(
this,
2,
Intent(this, ProxyService::class.java).apply {
action = ACTION_STOP
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.notification_title))
.setContentText(text)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(openIntent)
.addAction(0, getString(R.string.stop_proxy), stopIntent)
.setOngoing(true)
.build()
}
private fun ensureNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_LOW,
).apply {
description = getString(R.string.notification_channel_desc)
}
manager.createNotificationChannel(channel)
}
companion object {
private const val CHANNEL_ID = "proxy_service"
private const val NOTIFICATION_ID = 1001
const val ACTION_START = "ir.masteralidns.androidproxy.START"
const val ACTION_STOP = "ir.masteralidns.androidproxy.STOP"
const val EXTRA_CONFIG = "config"
}
}
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_main"
android:textAppearance="?attr/textAppearanceHeadlineSmall" />
<TextView
android:id="@+id/subtitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/subtitle_main"
android:textAppearance="?attr/textAppearanceBodyMedium" />
<TextView
android:id="@+id/statusView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/status_idle"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<EditText
android:id="@+id/configEditor"
android:layout_width="match_parent"
android:layout_height="320dp"
android:layout_marginTop="16dp"
android:gravity="top|start"
android:hint="@string/config_hint"
android:inputType="textMultiLine|textNoSuggestions"
android:fontFamily="monospace"
android:padding="12dp"
android:scrollbars="vertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/startButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_proxy" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/stop_proxy" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="proxy_primary">#0F766E</color>
<color name="proxy_primary_dark">#134E4A</color>
<color name="proxy_surface">#F6F6F2</color>
<color name="proxy_text">#172321</color>
</resources>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MasterAliDns Android</string>
<string name="title_main">Android Native Proxy</string>
<string name="subtitle_main">First Kotlin slice: local HTTP and SOCKS5 proxy service on-device.</string>
<string name="status_idle">Status: stopped</string>
<string name="status_running">Status: running</string>
<string name="config_hint">Paste JSON config here</string>
<string name="start_proxy">Start</string>
<string name="stop_proxy">Stop</string>
<string name="notification_title">MasterAliDns proxy is running</string>
<string name="notification_channel_name">Proxy service</string>
<string name="notification_channel_desc">Keeps the local proxy alive in the foreground.</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.MasterAliDnsAndroid" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/proxy_primary</item>
<item name="colorPrimaryVariant">@color/proxy_primary_dark</item>
<item name="colorOnPrimary">@android:color/white</item>
<item name="android:windowBackground">@color/proxy_surface</item>
<item name="android:textColor">@color/proxy_text</item>
<item name="colorSurface">@color/proxy_surface</item>
<item name="colorOnSurface">@color/proxy_text</item>
<item name="android:statusBarColor" tools:targetApi="l">@color/proxy_primary_dark</item>
</style>
</resources>