update ios project

update ios project
This commit is contained in:
Zeus
2024-07-24 11:51:39 +08:00
parent 876d355b18
commit 1bf69048f3
275 changed files with 3372 additions and 1015 deletions
+13
View File
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
+107
View File
@@ -0,0 +1,107 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
main.jniLibs.srcDirs = ['libs', 'src/main/jniLibs']
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.sail_tunnel.sail"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 26
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// ndk {
// abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', "x86_64"
// }
// dexOptions {
// incremental true
// javaMaxHeapSize "4g"
// preDexLibraries true
// dexInProcess = true
// }
ndk {
if (!project.hasProperty('target-platform')) {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
} else {
def platforms = project.property('target-platform').split(',')
def platformMap = [
'android-arm' : 'armeabi-v7a',
'android-arm64': 'arm64-v8a',
'android-x86' : 'x86',
'android-x64' : 'x86_64',
]
abiFilters = platforms.stream().map({ e ->
platformMap.containsKey(e) ? platformMap[e] : e
}).toArray()
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation "dnsjava:dnsjava:3.5.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
}
@@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.sail_tunnel.sail",
"variantName": "profile",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 3,
"versionName": "1.0.2",
"outputFile": "app-profile.apk"
}
],
"elementType": "File"
}
@@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sail_tunnel.sail">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
</manifest>
@@ -0,0 +1,87 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sail_tunnel.sail">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
<uses-permission android:name="android.permission.WAKE_LOCK"></uses-permission>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
/>
<application
android:label="UUVPN"
android:name="${applicationName}"
android:icon="@mipmap/app_icon"
android:largeHeap="true"
android:supportsRtl="true"
android:allowBackup="true"
android:hardwareAccelerated="false">
<!-- android:icon="@mipmap/ic_launcher"-->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="false"
android:windowSoftInputMode="adjustResize"
>
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".TunnelService"
android:enabled="true"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,54 @@
package com.sail_tunnel.sail
import android.app.*
import android.content.*
import android.net.ConnectivityManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.core.content.getSystemService
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.reflect.KClass
import androidx.work.Configuration
import com.sail_tunnel.sail.utils.DeviceStorageApp
object Core : Configuration.Provider {
lateinit var app: MainActivity
@VisibleForTesting set
lateinit var configureIntent: (Context) -> PendingIntent
val activity by lazy { app.getSystemService<ActivityManager>()!! }
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
val deviceStorage by lazy { DeviceStorageApp(app) }
fun init(app: MainActivity, configureClass: KClass<out Any>) {
this.app = app
this.configureIntent = {
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), PendingIntent.FLAG_IMMUTABLE)
}
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Timber.plant(object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (t == null) {
if (priority != Log.DEBUG || BuildConfig.DEBUG) Log.println(priority, tag, message)
} else {
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
}
}
})
}
override fun getWorkManagerConfiguration() = Configuration.Builder().apply {
setDefaultProcessName(app.packageName + ":bg")
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
setExecutor { GlobalScope.launch { it.run() } }
setTaskExecutor { GlobalScope.launch { it.run() } }
}.build()
}
@@ -0,0 +1,160 @@
package com.sail_tunnel.sail
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.VpnService
import android.os.BatteryManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import com.sail_tunnel.sail.services.VpnState
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import java.io.File
class MainActivity: FlutterActivity() {
private val channel = "com.vpntunnel.vpnzeus/vpn_manager"
private fun getServiceIntent(): Intent {
return Intent(this, TunnelService::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Core.init(this, MainActivity::class)
// GeneratedPluginRegistrant.registerWith(FlutterEngine(this))
// GeneratedPluginRegister.registerGeneratedPlugins(FlutterEngine(this));
// MethodChannel(FlutterView(this), channel).setMethodCallHandler { call, result ->
// if (call.method == "getBatteryLevel") {
// val batteryLevel : Int = getBatteryLevel()
//
// if (batteryLevel != -1) {
// result.success(batteryLevel)
// } else {
// result.error("UNAVAILABLE", "Battery level not available.", null)
// }
// } else {
// Log.e("user_debug", "getBatteryLevel is not method of called")
// result.notImplemented()
// }
// }
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
}
private fun getVPNConnectionStatus(): Boolean? {
val context = this@MainActivity
val connectivityManager =
context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val networks = connectivityManager.allNetworks
//val TAG = "getVPNConnectionStatus"
//Log.i(TAG, "Network count: " + networks.size);
var hasvpn = false
for (i in 0 until networks.size) {
// connectivityManager.getNetworkCapabilities()
val caps: NetworkCapabilities? = connectivityManager.getNetworkCapabilities(networks.get(i))
hasvpn = caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
// Log.i(TAG, "Network " + i + ": " + networks.get(i).toString())
// Log.i(TAG, "VPN transport is: " + caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
// Log.i(TAG, "NOT_VPN capability is: " + caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) )
}
return hasvpn
// val activeNetwork = connectivityManager.activeNetwork
// val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
//
// return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}
override fun configureFlutterEngine( flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler {
call, result ->
// This method is invoked on the main thread.
if (call.method == "getStatus") {
result.success(getVPNConnectionStatus())
} else if (call.method == "toggle") {
val intent = VpnService.prepare(this)
if (intent != null) {
startActivityForResult(intent, 0)
result.success(false)
} else {
startService(getServiceIntent())
result.success(true)
}
} else if (call.method == "getTunnelLog") {
// val context = Core.deviceStorage
// val configRoot = context.noBackupFilesDir
// val config = File(configRoot, VpnState.LEAF_LOG_FILE).readText()
// result.success(config)
} else if (call.method == "getTunnelConfiguration") {
val context = Core.deviceStorage
val configRoot = context.noBackupFilesDir
val config = File(configRoot, VpnState.CONFIG_FILE).readText()
val TAGs = "getVPNConnectionStatus"
Log.i(TAGs, config);
result.success(config)
} else if (call.method == "setTunnelConfiguration") {
try {
val context = Core.deviceStorage
val configRoot = context.noBackupFilesDir
val TAGs = "setTunnelConfiguration"
Log.i(TAGs, call.arguments.toString());
File(configRoot, VpnState.CONFIG_FILE).writeText(call.arguments.toString())
}catch (e: Exception ){
println("setTunnelConfiguration FILE NullPointerException")
}
} else if (call.method == "update") {
//
} else {
result.notImplemented()
}
}
}
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
if (result == RESULT_OK) {
startService(getServiceIntent())
} else {
super.onActivityResult(request, result, data)
}
}
}
@@ -0,0 +1,251 @@
package com.sail_tunnel.sail
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.net.*
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.core.app.NotificationCompat
import com.sail_tunnel.sail.net.DefaultNetworkListener
import com.sail_tunnel.sail.net.DnsResolverCompat
import com.sail_tunnel.sail.services.LocalDnsWorker
import com.sail_tunnel.sail.services.TunnelInstance
import com.sail_tunnel.sail.services.VpnState
import com.sail_tunnel.sail.utils.readableMessage
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
class TunnelService : VpnService() {
companion object {
private const val CORE_NAME = "leaf"
private const val VPN_MTU = 1500
private const val PRIVATE_VLAN4_CLIENT = "198.18.20.20"
private const val PRIVATE_VLAN4_ROUTER = "1.1.1.1"
}
inner class NullConnectionException : NullPointerException() {
override fun getLocalizedMessage() =
"Failed to start VPN service. You might need to reboot your device."
}
private val data = VpnState.Data()
private fun startRunner() {
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
else startService(Intent(this, javaClass))
}
private var pfd: ParcelFileDescriptor? = null
private var active = false
private var metered = false
var notification: Notification? = null
private val mResultData: Intent? = null
private val mMediaProjection: MediaProjection? = null
private val mMediaProjectionManager: MediaProjectionManager? = null
@Volatile
private var underlyingNetwork: Network? = null
set(value) {
field = value
if (active) setUnderlyingNetworks(underlyingNetworks)
}
private val underlyingNetworks
get() =
// clearing underlyingNetworks makes Android 9 consider the network to be metered
if (Build.VERSION.SDK_INT == 28 && metered) null else underlyingNetwork?.let {
arrayOf(
it
)
}
override fun onRevoke() = stopRunner()
private fun killProcesses(scope: CoroutineScope) {
data.proxy?.run {
shutdown(scope)
data.proxy = null
}
data.localDns?.shutdown(scope)
data.localDns = null
active = false
scope.launch { DefaultNetworkListener.stop(this) }
pfd?.close()
pfd = null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val data = data
if (data.state != VpnState.State.Stopped) return Service.START_NOT_STICKY
try {
data.proxy = TunnelInstance()
} catch (e: IllegalArgumentException) {
stopRunner(false, e.message)
return Service.START_NOT_STICKY
}
createNotificationChannel()
data.changeState(VpnState.State.Connecting)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try {
preInit()
startVpn()
data.changeState(VpnState.State.Connected)
} catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (exc: Throwable) {
stopRunner(false, exc.readableMessage)
} finally {
data.connectingJob = null
}
}
return START_NOT_STICKY
}
private fun createNotificationChannel() {
val CHANNEL_ID: String = "notification_id"
val builder = NotificationCompat.Builder(this.applicationContext,CHANNEL_ID) //获取一个Notification构造器
val nfIntent = Intent(this, MainActivity::class.java) //点击后跳转的界面,可以设置跳转数据
val pendingIntent: PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, nfIntent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(this, 0, nfIntent, PendingIntent.FLAG_ONE_SHOT)
}
val intentClick = Intent(
this,MainActivity::class.java
// BroadcastReceiver::class.java
)
intentClick.action = "notification_clicked"
// intentClick.putExtra(BroadcastReceiver.TYPE, type)
val pendingIntentClick =
PendingIntent.getBroadcast(this, 0, intentClick, PendingIntent.FLAG_ONE_SHOT)
//.setContentIntent(pendingIntent) // 设置PendingIntent
builder.setContentIntent(pendingIntentClick).setLargeIcon(
BitmapFactory.decodeResource(
this.resources,
R.mipmap.app_icon_round
)
) // 设置下拉列表中的图标(大图标)
.setContentTitle("UUVPN") // 设置下拉列表里的标题
.setSmallIcon( R.mipmap.app_icon_round) // 设置状态栏内的小图标
.setContentText("正在运行...") // 设置上下文内容
.setWhen(System.currentTimeMillis()) // 设置该通知发生的时间
/*以下是对Android 8.0的适配*/
//普通notification适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId("notification_id")
}
//前台服务notification适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"notification_id",
"notification_name",
NotificationManager.IMPORTANCE_LOW
)
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
notification = builder.build() // 获取构建好的Notification
// notification.defaulte = Notification.DEFAULT_SOUND //设置为默认的声音
try {
startForeground(110, notification)
} catch (w: Exception) {
w.printStackTrace()
}
}
private suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
private suspend fun rawResolver(query: ByteArray) =
// no need to listen for network here as this is only used for forwarding local DNS queries.
// retries should be attempted by client.
DnsResolverCompat.resolveRaw(underlyingNetwork ?: throw IOException("no network"), query)
private suspend fun startVpn() {
val builder = Builder()
.setConfigureIntent(Core.configureIntent(this))
.setSession(CORE_NAME)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN4_CLIENT, 24)
.addDnsServer(PRIVATE_VLAN4_ROUTER)
.addRoute("0.0.0.0", 0)
active = true // possible race condition here?
builder.setUnderlyingNetworks(underlyingNetworks)
if (Build.VERSION.SDK_INT >= 29) builder.setMetered(metered)
this.pfd = builder.establish() ?: throw NullConnectionException()
val context = Core.deviceStorage
val configRoot = context.noBackupFilesDir
data.localDns = LocalDnsWorker(this::rawResolver).apply { start() }
val configFile = File(configRoot, VpnState.CONFIG_FILE)
val configContent = configFile
.readText()
.replace("{{leafLogFile}}", File(configRoot, VpnState.LEAF_LOG_FILE).absolutePath)
.replace("{{tunFd}}", this.pfd?.fd?.toLong().toString())
configFile.writeText(configContent)
data.proxy!!.start(
this,
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
configFile,
)
}
private fun stopRunner(restart: Boolean = false, msg: String? = null) {
if (data.state == VpnState.State.Stopping) return
// channge the state
data.changeState(VpnState.State.Stopping)
GlobalScope.launch(Dispatchers.Main.immediate) {
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
// we use a coroutineScope here to allow clean-up in parallel
coroutineScope {
killProcesses(GlobalScope)
}
// change the state
data.changeState(VpnState.State.Stopped, msg)
// stop the service if nothing has bound to it
if (restart) startRunner() else {
stopSelf()
}
}
}
}
@@ -0,0 +1,23 @@
package com.sail_tunnel.sail.net
import android.net.LocalSocket
import kotlinx.coroutines.*
import timber.log.Timber
import java.io.File
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
CoroutineScope {
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> Timber.w(t) }
override fun accept(socket: LocalSocket) {
launch { super.accept(socket) }
}
override fun shutdown(scope: CoroutineScope) {
running = false
cancel()
super.shutdown(scope)
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
}
}
@@ -0,0 +1,126 @@
package com.sail_tunnel.sail.net
import android.annotation.TargetApi
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import com.sail_tunnel.sail.Core
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException
object DefaultNetworkListener {
private sealed class NetworkMessage {
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
class Get : NetworkMessage() {
val response = CompletableDeferred<Network>()
}
class Stop(val key: Any) : NetworkMessage()
class Put(val network: Network) : NetworkMessage()
class Update(val network: Network) : NetworkMessage()
class Lost(val network: Network) : NetworkMessage()
}
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null
val pendingRequests = arrayListOf<NetworkMessage.Get>()
for (message in channel) when (message) {
is NetworkMessage.Start -> {
if (listeners.isEmpty()) register()
listeners[message.key] = message.listener
if (network != null) message.listener(network)
}
is NetworkMessage.Get -> {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(network)
}
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()) {
network = null
unregister()
}
is NetworkMessage.Put -> {
network = message.network
pendingRequests.forEach { it.response.complete(message.network) }
pendingRequests.clear()
listeners.values.forEach { it(network) }
}
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach { it(network) }
is NetworkMessage.Lost -> if (network == message.network) {
network = null
listeners.values.forEach { it(null) }
}
}
}
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(NetworkMessage.Start(key, listener))
suspend fun get() = if (fallback) {
Core.connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
} else NetworkMessage.Get().run {
networkActor.send(this)
response.await()
}
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
private object Callback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) = runBlocking { networkActor.send(NetworkMessage.Put(network)) }
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
// it's a good idea to refresh capabilities
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
}
override fun onLost(network: Network) = runBlocking { networkActor.send(NetworkMessage.Lost(network)) }
}
private var fallback = false
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private val mainHandler = Handler(Looper.getMainLooper())
/**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately, we need to have
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private fun register() {
when (Build.VERSION.SDK_INT) {
in 31..Int.MAX_VALUE -> @TargetApi(31) {
Core.connectivity.registerBestMatchingNetworkCallback(request, Callback, mainHandler)
}
in 28 until 31 -> @TargetApi(28) { // we want REQUEST here instead of LISTEN
Core.connectivity.requestNetwork(request, Callback, mainHandler)
}
in 26 until 28 -> @TargetApi(26) {
Core.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)
}
in 24 until 26 -> {
Core.connectivity.registerDefaultNetworkCallback(Callback)
}
else -> try {
fallback = false
Core.connectivity.requestNetwork(request, Callback)
} catch (e: RuntimeException) {
fallback = true // known bug on API 23: https://stackoverflow.com/a/33509180/2245107
}
}
}
private fun unregister() = Core.connectivity.unregisterNetworkCallback(Callback)
}
@@ -0,0 +1,167 @@
package com.sail_tunnel.sail.net
import android.annotation.TargetApi
import android.net.DnsResolver
import android.net.Network
import android.os.Build
import android.os.CancellationSignal
import androidx.annotation.RequiresApi
import com.sail_tunnel.sail.Core
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.xbill.DNS.*
import java.io.IOException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
sealed class DnsResolverCompat {
companion object : DnsResolverCompat() {
private val instance by lazy {
when (Build.VERSION.SDK_INT) {
in 29..Int.MAX_VALUE -> DnsResolverCompat29
in 23 until 29 -> DnsResolverCompat23
else -> error("Unsupported API level")
}
}
override suspend fun resolve(network: Network, host: String) = instance.resolve(network, host)
override suspend fun resolveOnActiveNetwork(host: String) = instance.resolveOnActiveNetwork(host)
override suspend fun resolveRaw(network: Network, query: ByteArray) = instance.resolveRaw(network, query)
override suspend fun resolveRawOnActiveNetwork(query: ByteArray) = instance.resolveRawOnActiveNetwork(query)
// additional platform-independent DNS helpers
/**
* TTL returned from localResolver is set to 120. Android API does not provide TTL,
* so we suppose Android apps should not care about TTL either.
*/
private const val TTL = 120L
fun prepareDnsResponse(request: Message) = Message(request.header.id).apply {
header.setFlag(Flags.QR.toInt()) // this is a response
header.setFlag(Flags.RA.toInt()) // recursion available
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
request.question?.also { addRecord(it, Section.QUESTION) }
}
}
abstract suspend fun resolve(network: Network, host: String): Array<InetAddress>
abstract suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress>
abstract suspend fun resolveRaw(network: Network, query: ByteArray): ByteArray
abstract suspend fun resolveRawOnActiveNetwork(query: ByteArray): ByteArray
private object DnsResolverCompat23 : DnsResolverCompat() {
/**
* This dispatcher is used for noncancellable possibly-forever-blocking operations in network IO.
*
* See also: https://issuetracker.google.com/issues/133874590
*/
private val unboundedIO by lazy {
if (Core.activity.isLowRamDevice) Dispatchers.IO
else Executors.newCachedThreadPool().asCoroutineDispatcher()
}
override suspend fun resolve(network: Network, host: String) =
withContext(unboundedIO) { network.getAllByName(host) }
override suspend fun resolveOnActiveNetwork(host: String) =
withContext(unboundedIO) { InetAddress.getAllByName(host) }
private suspend fun resolveRaw(query: ByteArray, networkSpecified: Boolean = true,
hostResolver: suspend (String) -> Array<InetAddress>): ByteArray {
val request = try {
Message(query)
} catch (e: IOException) {
throw UnsupportedOperationException(e) // unrecognized packet
}
when (val opcode = request.header.opcode) {
Opcode.QUERY -> { }
else -> throw UnsupportedOperationException("Unsupported opcode $opcode")
}
val question = request.question
val isIpv6 = when (val type = question?.type) {
Type.A -> false
Type.AAAA -> true
Type.PTR -> {
/* Android does not provide a PTR lookup API for Network prior to Android 10 */
if (networkSpecified) throw IOException(UnsupportedOperationException("Network unspecified"))
val ip = try {
ReverseMap.fromName(question.name)
} catch (e: IOException) {
throw UnsupportedOperationException(e) // unrecognized PTR name
}
val hostname = withContext(unboundedIO) { ip.hostName }.let { hostname ->
if (hostname == ip.hostAddress) null else Name.fromString("$hostname.")
}
return prepareDnsResponse(request).apply {
hostname?.let { addRecord(PTRRecord(question.name, DClass.IN, TTL, it), Section.ANSWER) }
}.toWire()
}
else -> throw UnsupportedOperationException("Unsupported query type $type")
}
val host = question.name.canonicalize().toString(true)
return prepareDnsResponse(request).apply {
for (address in hostResolver(host).asIterable().run {
if (isIpv6) filterIsInstance<Inet6Address>() else filterIsInstance<Inet4Address>()
}) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> error("Unsupported address $address")
}, Section.ANSWER)
}.toWire()
}
override suspend fun resolveRaw(network: Network, query: ByteArray) =
resolveRaw(query) { resolve(network, it) }
override suspend fun resolveRawOnActiveNetwork(query: ByteArray) =
resolveRaw(query, false, this::resolveOnActiveNetwork)
}
@TargetApi(29)
private object DnsResolverCompat29 : DnsResolverCompat(), Executor {
/**
* This executor will run on its caller directly. On Q beta 3 thru 4, this results in calling in main thread.
*/
override fun execute(command: Runnable) = command.run()
private val activeNetwork get() = Core.connectivity.activeNetwork ?: throw IOException("no network")
override suspend fun resolve(network: Network, host: String): Array<InetAddress> {
return suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
// retry should be handled by client instead
DnsResolver.getInstance().query(network, host, DnsResolver.FLAG_NO_RETRY, this,
signal, object : DnsResolver.Callback<Collection<InetAddress>> {
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) =
cont.resume(answer.toTypedArray())
override fun onError(error: DnsResolver.DnsException) = cont.resumeWithException(
IOException(error)
)
})
}
}
override suspend fun resolveOnActiveNetwork(host: String) = resolve(activeNetwork, host)
override suspend fun resolveRaw(network: Network, query: ByteArray): ByteArray {
return suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
DnsResolver.getInstance().rawQuery(network, query, DnsResolver.FLAG_NO_RETRY, this,
signal, object : DnsResolver.Callback<ByteArray> {
override fun onAnswer(answer: ByteArray, rcode: Int) = cont.resume(answer)
override fun onError(error: DnsResolver.DnsException) = cont.resumeWithException(
IOException(error)
)
})
}
}
override suspend fun resolveRawOnActiveNetwork(query: ByteArray) = resolveRaw(activeNetwork, query)
}
}
@@ -0,0 +1,62 @@
package com.sail_tunnel.sail.net
import android.annotation.SuppressLint
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.IOException
abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name) {
private val localSocket = LocalSocket().apply {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
}
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
private val closeChannel = Channel<Unit>(1)
@Volatile
protected var running = true
/**
* Inherited class do not need to close input/output streams as they will be closed automatically.
*/
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
protected abstract fun acceptInternal(socket: LocalSocket)
final override fun run() {
localSocket.use {
while (running) {
try {
accept(serverSocket.accept())
} catch (e: IOException) {
if (running) Timber.w(e)
continue
}
}
}
closeChannel.trySendBlocking(Unit).onFailure { throw it!! }
}
@SuppressLint("NewApi")
open fun shutdown(scope: CoroutineScope) {
running = false
localSocket.fileDescriptor?.apply {
// see also: https://issuetracker.google.com/issues/36945762#comment15
if (valid()) try {
Os.shutdown(this, OsConstants.SHUT_RDWR)
} catch (e: ErrnoException) {
// suppress fd inactive or already closed
if (e.errno != OsConstants.EBADF && e.errno != OsConstants.ENOTCONN) throw e.rethrowAsSocketException()
}
}
scope.launch { closeChannel.receive() }
}
}
@@ -0,0 +1,64 @@
package com.sail_tunnel.sail.services
import android.net.LocalSocket
import android.os.Build
import androidx.annotation.RequiresApi
import com.sail_tunnel.sail.Core
import com.sail_tunnel.sail.net.ConcurrentLocalSocketListener
import com.sail_tunnel.sail.net.DnsResolverCompat
import com.sail_tunnel.sail.utils.readableMessage
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.IOException
import org.xbill.DNS.Message
import org.xbill.DNS.Rcode
class LocalDnsWorker(private val resolver: suspend (ByteArray) -> ByteArray) : ConcurrentLocalSocketListener(
"LocalDnsThread", File(Core.deviceStorage.noBackupFilesDir, "local_dns_path")
), CoroutineScope {
override fun acceptInternal(socket: LocalSocket) = error("big no no")
override fun accept(socket: LocalSocket) {
launch {
socket.use {
val input = DataInputStream(socket.inputStream)
val query = try {
ByteArray(input.readUnsignedShort()).also { input.read(it) }
} catch (e: IOException) { // connection early close possibly due to resolving timeout
return@use Timber.d(e)
}
try {
resolver(query)
} catch (e: Exception) {
when (e) {
is TimeoutCancellationException -> Timber.w("Resolving timed out")
is CancellationException -> { } // ignore
is IOException -> Timber.d(e)
is UnsupportedOperationException -> Timber.w(e.message)
else -> Timber.w(e)
}
try {
DnsResolverCompat.prepareDnsResponse(Message(query)).apply {
header.rcode = Rcode.SERVFAIL
}.toWire()
} catch (_: IOException) {
byteArrayOf() // return empty if cannot parse packet
}
}.let { response ->
try {
val output = DataOutputStream(socket.outputStream)
output.writeShort(response.size)
output.write(response)
} catch (e: IOException) {
Timber.d(e.readableMessage)
}
}
}
}
}
}
@@ -0,0 +1,89 @@
package com.sail_tunnel.sail.services
import android.net.LocalSocket
import android.os.SystemClock
import com.sail_tunnel.sail.net.LocalSocketListener
import com.sail_tunnel.sail.utils.TrafficStats
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder
class TrafficMonitor(statFile: File) {
val thread = object : LocalSocketListener("TrafficMonitor-" + statFile.name, statFile) {
private val buffer = ByteArray(16)
private val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
override fun acceptInternal(socket: LocalSocket) {
when (val read = socket.inputStream.read(buffer)) {
-1 -> return
16 -> { }
else -> throw IOException("Unexpected traffic stat length $read")
}
val tx = stat.getLong(0)
val rx = stat.getLong(8)
if (current.txTotal != tx) {
current.txTotal = tx
dirty = true
}
if (current.rxTotal != rx) {
current.rxTotal = rx
dirty = true
}
}
}.apply { start() }
val current = TrafficStats()
var out = TrafficStats()
private var timestampLast = 0L
private var dirty = false
private var persisted: TrafficStats? = null
fun requestUpdate(): Pair<TrafficStats, Boolean> {
val now = SystemClock.elapsedRealtime()
val delta = now - timestampLast
timestampLast = now
var updated = false
if (delta != 0L) {
if (dirty) {
out = current.copy().apply {
txRate = (txTotal - out.txTotal) * 1024 / delta
rxRate = (rxTotal - out.rxTotal) * 1024 / delta
}
dirty = false
updated = true
} else {
if (out.txRate != 0L) {
out.txRate = 0
updated = true
}
if (out.rxRate != 0L) {
out.rxRate = 0
updated = true
}
}
}
return Pair(out, updated)
}
fun persistStats() {
val current = current
check(persisted == null || persisted == current) { "Data loss occurred" }
persisted = current
// try {
// // profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
// val profile = ProfileManager.getProfile(id) ?: return
// profile.tx += current.txTotal
// profile.rx += current.rxTotal
// ProfileManager.updateProfile(profile)
// } catch (e: IOException) {
// if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
// val profile = DirectBoot.getDeviceProfile()!!.toList().single { it.id == id }
// profile.tx += current.txTotal
// profile.rx += current.rxTotal
// profile.dirty = true
// DirectBoot.update(profile)
// DirectBoot.listenForUnlock()
// }
}
}
@@ -0,0 +1,106 @@
package com.sail_tunnel.sail.services
import android.net.LocalServerSocket
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.system.Os
import com.sail_tunnel.sail.Core
import com.sail_tunnel.sail.TunnelService
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import java.io.File
import java.nio.ByteBuffer
import kotlin.concurrent.thread
class TunnelInstance() {
private var configFile: File? = null
var trafficMonitor: TrafficMonitor? = null
private val rtId: Int = 666
init {
System.loadLibrary("leaf")
}
private external fun leafRun(rtId: Int, configPath: String)
private external fun leafReload(rtId: Int)
private external fun leafShutdown(rtId: Int)
private external fun leafTestConfig(configPath: String)
/**
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
* device storage, depending on which is currently available.
*/
fun start(
service: TunnelService,
stat: File,
configFile: File,
) {
// setup traffic monitor path
trafficMonitor = TrafficMonitor(stat)
Timber.i("start process")
val context = Core.deviceStorage
val configRoot = context.noBackupFilesDir //noBackupFilesDir
val protectPath =
File.createTempFile("socket_protect", ".sock", configRoot).absolutePath
Os.setenv("SOCKET_PROTECT_PATH", protectPath, true)
println("protectPath: $protectPath")
thread (start = true) {
val localSocket = LocalSocket()
localSocket.bind(
LocalSocketAddress(
protectPath,
LocalSocketAddress.Namespace.FILESYSTEM
)
)
val socket = LocalServerSocket(localSocket.fileDescriptor)
val buffer = ByteBuffer.allocate(4)
while (true) {
val stream = socket.accept()
buffer.clear()
val n = stream.inputStream.read(buffer.array())
System.out.write(n)
if (n == 4) {
val fd = buffer.int
if (!service.protect(fd)) {
println("protect failed")
}
buffer.clear()
buffer.putInt(0)
} else {
buffer.clear()
buffer.putInt(1)
}
stream.outputStream.write(buffer.array())
}
}
// start tunnel program
thread(start = true) {
leafRun(rtId, configFile.absolutePath)
}
}
fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply {
thread.shutdown(scope)
persistStats() // Make sure update total traffic when stopping the runner
}
trafficMonitor = null
configFile?.delete() // remove old config possibly in device storage
configFile = null
// stop tunnel program
thread(start = true, isDaemon = true) {
leafShutdown(rtId)
}
}
}
@@ -0,0 +1,32 @@
package com.sail_tunnel.sail.services
import kotlinx.coroutines.Job
object VpnState {
enum class State {
/**
* Idle state is only used by UI and will never be returned by BaseService.
*/
Idle,
Connecting,
Connected,
Stopping,
Stopped,
}
const val CONFIG_FILE = "leaf.conf"
const val LEAF_LOG_FILE = "leaf.log"
class Data internal constructor() {
var state = State.Stopped
var proxy: TunnelInstance? = null
var localDns: LocalDnsWorker? = null
var connectingJob: Job? = null
fun changeState(s: State, msg: String? = null) {
if (state == s && msg == null) return
state = s
}
}
}
@@ -0,0 +1,18 @@
package com.sail_tunnel.sail.utils
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
@SuppressLint("Registered")
class DeviceStorageApp(context: Context) : Application() {
init {
attachBaseContext(context.createDeviceProtectedStorageContext())
}
/**
* Thou shalt not get the REAL underlying application context which would no longer be operating under device
* protected storage.
*/
override fun getApplicationContext() = this
}
@@ -0,0 +1,20 @@
package com.sail_tunnel.sail.utils
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class TrafficStats(
// Bytes per second
var txRate: Long = 0L,
var rxRate: Long = 0L,
// Bytes for the current session
var txTotal: Long = 0L,
var rxTotal: Long = 0L
) : Parcelable {
operator fun plus(other: TrafficStats) = TrafficStats(
txRate + other.txRate, rxRate + other.rxRate,
txTotal + other.txTotal, rxTotal + other.rxTotal
)
}
@@ -0,0 +1,33 @@
package com.sail_tunnel.sail.utils
import android.annotation.SuppressLint
import android.os.Build
import kotlinx.coroutines.*
import java.io.FileDescriptor
import java.net.HttpURLConnection
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
val Throwable.readableMessage: String get() = localizedMessage ?: javaClass.name
/**
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
*/
@SuppressLint("DiscouragedPrivateApi")
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
val FileDescriptor.int get() = getInt.invoke(this) as Int
suspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T {
return suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
}
GlobalScope.launch(Dispatchers.IO) {
try {
cont.resume(block())
} catch (e: Throwable) {
cont.resumeWithException(e)
}
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sail_tunnel.sail">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
</manifest>
+31
View File
@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.suppressUnsupportedCompileSdk=33
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
+11
View File
@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"