初始化提交
@@ -0,0 +1,158 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterRoot = properties.getProperty("flutter.sdk")
|
||||
assert flutterRoot != null, "flutter.sdk not set in local.properties"
|
||||
|
||||
boolean hasKeyStore = false
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
hasKeyStore = true
|
||||
} else {
|
||||
println "+++"
|
||||
println "No keystore defined. The app will not be signed."
|
||||
println "Create a android/key.properties file with the following properties:"
|
||||
println "storePassword"
|
||||
println "keyPassword"
|
||||
println "keyAlias"
|
||||
println "storeFile"
|
||||
println "+++"
|
||||
}
|
||||
|
||||
def flutterVersionCode = properties.getProperty('flutter.versionCode')?: '1'
|
||||
|
||||
def flutterVersionName = properties.getProperty('flutter.versionName') ?: '1.0'
|
||||
|
||||
android {
|
||||
namespace 'com.hiddify.hiddify'
|
||||
testNamespace "test.com.hiddify.hiddify"
|
||||
compileSdk 36
|
||||
ndkVersion "26.1.10909125"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "app.brAccelerator.com"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 36
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
manifestPlaceholders = [
|
||||
'android.permission.ACCESS_NETWORK_STATE': true
|
||||
]
|
||||
android.defaultConfig.manifestPlaceholders += [
|
||||
'android:screenOrientation': "portrait"
|
||||
]
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include "armeabi-v7a", "arm64-v8a"
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasKeyStore) {
|
||||
signingConfigs {
|
||||
release {
|
||||
/* keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']*/
|
||||
storeFile file("upload-keystore.jks")
|
||||
storePassword "123456"
|
||||
keyAlias "upload"
|
||||
keyPassword "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (hasKeyStore) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a"
|
||||
debugSymbolLevel 'FULL'
|
||||
}
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
aidl true
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force "org.jetbrains.kotlin:kotlin-stdlib:2.1.0"
|
||||
force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0"
|
||||
force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0"
|
||||
force "androidx.annotation:annotation-jvm:1.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.versionCodeOverride = android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
constraints {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0") {
|
||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||
}
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0") {
|
||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 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.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,149 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Crisp 聊天所需权限 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="BearVPN"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
tools:targetApi="31">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/LaunchTheme"
|
||||
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>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="sing-box" />
|
||||
<data android:host="import-remote-profile" />
|
||||
<data android:scheme="clash" />
|
||||
<data android:host="install-config" />
|
||||
<data android:scheme="clashmeta" />
|
||||
<data android:scheme="hiddify" />
|
||||
<data android:host="install-sub" />
|
||||
<data android:scheme="hiddify" />
|
||||
<data android:host="import" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ShortcutActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/quick_toggle"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".bg.TileService"
|
||||
android:directBootAware="true"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_stat_logo"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".bg.VPNService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".bg.ProxyService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</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>
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.hiddify.hiddify;
|
||||
|
||||
import com.hiddify.hiddify.IServiceCallback;
|
||||
|
||||
interface IService {
|
||||
int getStatus();
|
||||
void registerCallback(in IServiceCallback callback);
|
||||
oneway void unregisterCallback(in IServiceCallback callback);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify;
|
||||
|
||||
interface IServiceCallback {
|
||||
void onServiceStatusChanged(int status);
|
||||
void onServiceAlert(int type, String message);
|
||||
void onServiceWriteLog(String message);
|
||||
void onServiceResetLogs(in List<String> messages);
|
||||
}
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,60 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.hiddify.hiddify.utils.CommandClient
|
||||
import com.hiddify.hiddify.utils.ParsedOutboundGroup
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
|
||||
class ActiveGroupsChannel(private val scope: CoroutineScope) : FlutterPlugin,
|
||||
CommandClient.Handler {
|
||||
companion object {
|
||||
const val TAG = "A/ActiveGroupsChannel"
|
||||
const val CHANNEL = "com.baer.app/active-groups"
|
||||
val gson = Gson()
|
||||
}
|
||||
|
||||
private val client =
|
||||
CommandClient(scope, CommandClient.ConnectionType.GroupOnly, this)
|
||||
|
||||
private var channel: EventChannel? = null
|
||||
private var event: EventChannel.EventSink? = null
|
||||
|
||||
override fun updateGroups(groups: List<OutboundGroup>) {
|
||||
MainActivity.instance.runOnUiThread {
|
||||
val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
|
||||
event?.success(gson.toJson(parsedGroups))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = EventChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
CHANNEL
|
||||
)
|
||||
|
||||
channel!!.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
event = events
|
||||
Log.d(TAG, "connecting active groups command client")
|
||||
client.connect()
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
event = null
|
||||
Log.d(TAG, "disconnecting active groups command client")
|
||||
client.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
event = null
|
||||
client.disconnect()
|
||||
channel?.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.hiddify.hiddify.bg.AppChangeReceiver
|
||||
import go.Seq
|
||||
import com.hiddify.hiddify.Application as BoxApplication
|
||||
|
||||
class Application : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
application = this
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Seq.setContext(this)
|
||||
|
||||
registerReceiver(AppChangeReceiver(), IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var application: BoxApplication
|
||||
val notification by lazy { application.getSystemService<NotificationManager>()!! }
|
||||
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
|
||||
val packageManager by lazy { application.packageManager }
|
||||
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
|
||||
val notificationManager by lazy { application.getSystemService<NotificationManager>()!! }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.JSONMethodCodec
|
||||
|
||||
class EventHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/EventHandler"
|
||||
const val SERVICE_STATUS = "com.baer.app/service.status"
|
||||
const val SERVICE_ALERTS = "com.baer.app/service.alerts"
|
||||
}
|
||||
|
||||
private var statusChannel: EventChannel? = null
|
||||
private var alertsChannel: EventChannel? = null
|
||||
|
||||
private var statusObserver: Observer<Status>? = null
|
||||
private var alertsObserver: Observer<ServiceEvent?>? = null
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS, JSONMethodCodec.INSTANCE)
|
||||
alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS, JSONMethodCodec.INSTANCE)
|
||||
|
||||
statusChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
statusObserver = Observer {
|
||||
Log.d(TAG, "new status: $it")
|
||||
val map = listOf(
|
||||
Pair("status", it.name)
|
||||
)
|
||||
.toMap()
|
||||
events?.success(map)
|
||||
}
|
||||
MainActivity.instance.serviceStatus.observeForever(statusObserver!!)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
if (statusObserver != null)
|
||||
MainActivity.instance.serviceStatus.removeObserver(statusObserver!!)
|
||||
}
|
||||
})
|
||||
|
||||
alertsChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
alertsObserver = Observer {
|
||||
if (it == null) return@Observer
|
||||
Log.d(TAG, "new alert: $it")
|
||||
val map = listOf(
|
||||
Pair("status", it.status.name),
|
||||
Pair("alert", it.alert?.name),
|
||||
Pair("message", it.message)
|
||||
)
|
||||
.mapNotNull { p -> p.second?.let { Pair(p.first, p.second) } }
|
||||
.toMap()
|
||||
events?.success(map)
|
||||
}
|
||||
MainActivity.instance.serviceAlerts.observeForever(alertsObserver!!)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
if (alertsObserver != null)
|
||||
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver!!)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
if (statusObserver != null)
|
||||
MainActivity.instance.serviceStatus.removeObserver(statusObserver!!)
|
||||
statusChannel?.setStreamHandler(null)
|
||||
if (alertsObserver != null)
|
||||
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver!!)
|
||||
alertsChannel?.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
|
||||
data class ServiceEvent(val status: Status, val alert: Alert? = null, val message: String? = null)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.hiddify.hiddify.utils.CommandClient
|
||||
import com.hiddify.hiddify.utils.ParsedOutboundGroup
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class GroupsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler {
|
||||
companion object {
|
||||
const val TAG = "A/GroupsChannel"
|
||||
const val CHANNEL = "com.baer.app/groups"
|
||||
val gson = Gson()
|
||||
}
|
||||
|
||||
private val client =
|
||||
CommandClient(scope, CommandClient.ConnectionType.Groups, this)
|
||||
|
||||
private var channel: EventChannel? = null
|
||||
private var event: EventChannel.EventSink? = null
|
||||
|
||||
override fun updateGroups(groups: List<OutboundGroup>) {
|
||||
MainActivity.instance.runOnUiThread {
|
||||
val parsedGroups = groups.map { group -> ParsedOutboundGroup.fromOutbound(group) }
|
||||
event?.success(gson.toJson(parsedGroups))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = EventChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
CHANNEL
|
||||
)
|
||||
|
||||
channel!!.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
event = events
|
||||
Log.d(TAG, "connecting groups command client")
|
||||
client.connect()
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
event = null
|
||||
Log.d(TAG, "disconnecting groups command client")
|
||||
client.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
event = null
|
||||
client.disconnect()
|
||||
channel?.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
||||
|
||||
class LogHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/LogHandler"
|
||||
const val SERVICE_LOGS = "com.baer.app/service.logs"
|
||||
}
|
||||
|
||||
private lateinit var logsChannel: EventChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
logsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_LOGS)
|
||||
|
||||
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
val activity = MainActivity.instance
|
||||
events?.success(activity.logList)
|
||||
activity.logCallback = {
|
||||
events?.success(activity.logList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
MainActivity.instance.logCallback = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hiddify.hiddify.bg.ServiceConnection
|
||||
import com.hiddify.hiddify.bg.ServiceNotification
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.ServiceMode
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.LinkedList
|
||||
|
||||
|
||||
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
|
||||
companion object {
|
||||
private const val TAG = "ANDROID/MyActivity"
|
||||
lateinit var instance: MainActivity
|
||||
|
||||
const val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
|
||||
}
|
||||
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
val logList = LinkedList<String>()
|
||||
var logCallback: ((Boolean) -> Unit)? = null
|
||||
val serviceStatus = MutableLiveData(Status.Stopped)
|
||||
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
instance = this
|
||||
reconnect()
|
||||
flutterEngine.plugins.add(MethodHandler(lifecycleScope))
|
||||
flutterEngine.plugins.add(PlatformSettingsHandler())
|
||||
flutterEngine.plugins.add(EventHandler())
|
||||
flutterEngine.plugins.add(LogHandler())
|
||||
flutterEngine.plugins.add(GroupsChannel(lifecycleScope))
|
||||
flutterEngine.plugins.add(ActiveGroupsChannel(lifecycleScope))
|
||||
flutterEngine.plugins.add(StatsChannel(lifecycleScope))
|
||||
}
|
||||
|
||||
fun reconnect() {
|
||||
connection.reconnect()
|
||||
}
|
||||
|
||||
fun startService() {
|
||||
// 暂时跳过通知权限检查
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (Settings.rebuildServiceMode()) {
|
||||
reconnect()
|
||||
}
|
||||
if (Settings.serviceMode == ServiceMode.VPN) {
|
||||
if (prepare()) {
|
||||
Log.d(TAG, "VPN permission required")
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(Application.application, Settings.serviceClass())
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(Application.application, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepare() = withContext(Dispatchers.Main) {
|
||||
try {
|
||||
val intent = VpnService.prepare(this@MainActivity)
|
||||
if (intent != null) {
|
||||
startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onServiceAlert(Alert.RequestVPNPermission, e.message)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
serviceStatus.postValue(status)
|
||||
}
|
||||
|
||||
override fun onServiceAlert(type: Alert, message: String?) {
|
||||
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
|
||||
}
|
||||
|
||||
override fun onServiceWriteLog(message: String?) {
|
||||
if (logList.size > 300) {
|
||||
logList.removeFirst()
|
||||
}
|
||||
logList.addLast(message ?: "")
|
||||
logCallback?.invoke(false)
|
||||
}
|
||||
|
||||
override fun onServiceResetLogs(messages: MutableList<String>) {
|
||||
logList.clear()
|
||||
logList.addAll(messages)
|
||||
logCallback?.invoke(true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
connection.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun grantNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
startService()
|
||||
} else onServiceAlert(Alert.RequestNotificationPermission, null)
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestVPNPermission, null)
|
||||
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestNotificationPermission, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import com.hiddify.hiddify.bg.BoxService
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.mobile.Mobile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
private var channel: MethodChannel? = null
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/MethodHandler"
|
||||
const val channelName = "com.baer.app/method"
|
||||
|
||||
enum class Trigger(val method: String) {
|
||||
Setup("setup"),
|
||||
ParseConfig("parse_config"),
|
||||
changeHiddifyOptions("change_hiddify_options"),
|
||||
GenerateConfig("generate_config"),
|
||||
Start("start"),
|
||||
Stop("stop"),
|
||||
Restart("restart"),
|
||||
SelectOutbound("select_outbound"),
|
||||
UrlTest("url_test"),
|
||||
ClearLogs("clear_logs"),
|
||||
GenerateWarpConfig("generate_warp_config"),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
channelName,
|
||||
)
|
||||
channel!!.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel?.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
Trigger.Setup.method -> {
|
||||
GlobalScope.launch {
|
||||
result.runCatching {
|
||||
val baseDir = Application.application.filesDir
|
||||
baseDir.mkdirs()
|
||||
val workingDir = Application.application.getExternalFilesDir(null)
|
||||
workingDir?.mkdirs()
|
||||
val tempDir = Application.application.cacheDir
|
||||
tempDir.mkdirs()
|
||||
Log.d(TAG, "base dir: ${baseDir.path}")
|
||||
Log.d(TAG, "working dir: ${workingDir?.path}")
|
||||
Log.d(TAG, "temp dir: ${tempDir.path}")
|
||||
|
||||
Mobile.setup(baseDir.path, workingDir?.path, tempDir.path, false)
|
||||
Libbox.redirectStderr(File(workingDir, "stderr2.log").path)
|
||||
|
||||
success("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.ParseConfig.method -> {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val path = args["path"] as String
|
||||
val tempPath = args["tempPath"] as String
|
||||
val debug = args["debug"] as Boolean
|
||||
val msg = BoxService.parseConfig(path, tempPath, debug)
|
||||
success(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.changeHiddifyOptions.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val args = call.arguments as String
|
||||
Settings.configOptions = args
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.GenerateConfig.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val path = args["path"] as String
|
||||
val options = Settings.configOptions
|
||||
if (options.isBlank() || path.isBlank()) {
|
||||
error("blank properties")
|
||||
}
|
||||
val config = BoxService.buildConfig(path, options)
|
||||
success(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.Start.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
Settings.activeConfigPath = args["path"] as String? ?: ""
|
||||
Settings.activeProfileName = args["name"] as String? ?: ""
|
||||
val mainActivity = MainActivity.instance
|
||||
val started = mainActivity.serviceStatus.value == Status.Started
|
||||
if (started) {
|
||||
Log.w(TAG, "service is already running")
|
||||
return@launch success(true)
|
||||
}
|
||||
mainActivity.startService()
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.Stop.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val mainActivity = MainActivity.instance
|
||||
val started = mainActivity.serviceStatus.value == Status.Started
|
||||
if (!started) {
|
||||
Log.w(TAG, "service is not running")
|
||||
return@launch success(true)
|
||||
}
|
||||
BoxService.stop()
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.Restart.method -> {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
Settings.activeConfigPath = args["path"] as String? ?: ""
|
||||
Settings.activeProfileName = args["name"] as String? ?: ""
|
||||
val mainActivity = MainActivity.instance
|
||||
val started = mainActivity.serviceStatus.value == Status.Started
|
||||
if (!started) return@launch success(true)
|
||||
val restart = Settings.rebuildServiceMode()
|
||||
if (restart) {
|
||||
mainActivity.reconnect()
|
||||
BoxService.stop()
|
||||
delay(1000L)
|
||||
mainActivity.startService()
|
||||
return@launch success(true)
|
||||
}
|
||||
runCatching {
|
||||
Libbox.newStandaloneCommandClient().serviceReload()
|
||||
success(true)
|
||||
}.onFailure {
|
||||
error(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.SelectOutbound.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
Libbox.newStandaloneCommandClient()
|
||||
.selectOutbound(
|
||||
args["groupTag"] as String,
|
||||
args["outboundTag"] as String
|
||||
)
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.UrlTest.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
Libbox.newStandaloneCommandClient()
|
||||
.urlTest(
|
||||
args["groupTag"] as String
|
||||
)
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.ClearLogs.method -> {
|
||||
scope.launch {
|
||||
result.runCatching {
|
||||
MainActivity.instance.onServiceResetLogs(mutableListOf())
|
||||
success(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.GenerateWarpConfig.method -> {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val warpConfig = Mobile.generateWarpConfig(
|
||||
args["license-key"] as String,
|
||||
args["previous-account-id"] as String,
|
||||
args["previous-access-token"] as String,
|
||||
)
|
||||
success(warpConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.hiddify.hiddify.Application.Companion.packageManager
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
|
||||
class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
|
||||
PluginRegistry.ActivityResultListener {
|
||||
private var channel: MethodChannel? = null
|
||||
private var activity: Activity? = null
|
||||
private lateinit var ignoreRequestResult: MethodChannel.Result
|
||||
|
||||
companion object {
|
||||
const val channelName = "com.baer.app/platform"
|
||||
|
||||
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44
|
||||
val gson = Gson()
|
||||
|
||||
enum class Trigger(val method: String) {
|
||||
IsIgnoringBatteryOptimizations("is_ignoring_battery_optimizations"),
|
||||
RequestIgnoreBatteryOptimizations("request_ignore_battery_optimizations"),
|
||||
GetInstalledPackages("get_installed_packages"),
|
||||
GetPackagesIcon("get_package_icon"),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
val taskQueue = flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
|
||||
channel = MethodChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
channelName,
|
||||
StandardMethodCodec.INSTANCE,
|
||||
taskQueue
|
||||
)
|
||||
channel!!.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel?.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) {
|
||||
ignoreRequestResult.success(resultCode == Activity.RESULT_OK)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
data class AppItem(
|
||||
@SerializedName("package-name") val packageName: String,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("is-system-app") val isSystemApp: Boolean
|
||||
)
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
Trigger.IsIgnoringBatteryOptimizations.method -> {
|
||||
result.runCatching {
|
||||
success(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Application.powerManager.isIgnoringBatteryOptimizations(Application.application.packageName)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.RequestIgnoreBatteryOptimizations.method -> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return result.success(true)
|
||||
}
|
||||
val intent = Intent(
|
||||
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:${Application.application.packageName}")
|
||||
)
|
||||
ignoreRequestResult = result
|
||||
activity?.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
|
||||
}
|
||||
|
||||
Trigger.GetInstalledPackages.method -> {
|
||||
GlobalScope.launch {
|
||||
result.runCatching {
|
||||
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
|
||||
}
|
||||
val installedPackages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(
|
||||
PackageManager.PackageInfoFlags.of(
|
||||
flag.toLong()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(flag)
|
||||
}
|
||||
val list = mutableListOf<AppItem>()
|
||||
installedPackages.forEach {
|
||||
if (it.packageName != Application.application.packageName &&
|
||||
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android")
|
||||
) {
|
||||
list.add(
|
||||
AppItem(
|
||||
it.packageName,
|
||||
it.applicationInfo?.loadLabel(packageManager)?.toString() ?: "",
|
||||
(it.applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM == 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
list.sortBy { it.name }
|
||||
success(gson.toJson(list))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trigger.GetPackagesIcon.method -> {
|
||||
result.runCatching {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val packageName =
|
||||
args["packageName"] as String
|
||||
val drawable = packageManager.getApplicationIcon(packageName)
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
val base64: String =
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
success(base64)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import com.hiddify.hiddify.bg.ProxyService
|
||||
import com.hiddify.hiddify.bg.VPNService
|
||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
||||
import com.hiddify.hiddify.constant.ServiceMode
|
||||
import com.hiddify.hiddify.constant.SettingsKey
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
|
||||
object Settings {
|
||||
|
||||
private val preferences by lazy {
|
||||
val context = Application.application.applicationContext
|
||||
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
|
||||
|
||||
var perAppProxyMode: String
|
||||
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
|
||||
|
||||
val perAppProxyEnabled: Boolean
|
||||
get() = perAppProxyMode != PerAppProxyMode.OFF
|
||||
|
||||
val perAppProxyList: List<String>
|
||||
get() {
|
||||
val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
||||
preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!
|
||||
} else {
|
||||
preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!
|
||||
}
|
||||
if (!stringValue.startsWith(LIST_IDENTIFIER)) {
|
||||
return emptyList()
|
||||
}
|
||||
return decodeListString(stringValue.substring(LIST_IDENTIFIER.length))
|
||||
}
|
||||
|
||||
private fun decodeListString(listString: String): List<String> {
|
||||
val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0)))
|
||||
return stream.readObject() as List<String>
|
||||
}
|
||||
|
||||
var activeConfigPath: String
|
||||
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
|
||||
|
||||
var activeProfileName: String
|
||||
get() = preferences.getString(SettingsKey.ACTIVE_PROFILE_NAME, "")!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_PROFILE_NAME, value).apply()
|
||||
|
||||
var serviceMode: String
|
||||
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.VPN)!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
|
||||
|
||||
var configOptions: String
|
||||
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!!
|
||||
set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply()
|
||||
|
||||
var debugMode: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.DEBUG_MODE, false)
|
||||
set(value) = preferences.edit().putBoolean(SettingsKey.DEBUG_MODE, value).apply()
|
||||
|
||||
var disableMemoryLimit: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
|
||||
set(value) =
|
||||
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
|
||||
|
||||
var dynamicNotification: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.DYNAMIC_NOTIFICATION, true)
|
||||
set(value) =
|
||||
preferences.edit().putBoolean(SettingsKey.DYNAMIC_NOTIFICATION, value).apply()
|
||||
|
||||
var systemProxyEnabled: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
|
||||
set(value) =
|
||||
preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
|
||||
|
||||
var startedByUser: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
|
||||
set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply()
|
||||
|
||||
fun serviceClass(): Class<*> {
|
||||
return when (serviceMode) {
|
||||
ServiceMode.VPN -> VPNService::class.java
|
||||
else -> ProxyService::class.java
|
||||
}
|
||||
}
|
||||
|
||||
private var currentServiceMode : String? = null
|
||||
|
||||
suspend fun rebuildServiceMode(): Boolean {
|
||||
var newMode = ServiceMode.NORMAL
|
||||
try {
|
||||
if (serviceMode == ServiceMode.VPN) {
|
||||
newMode = ServiceMode.VPN
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (currentServiceMode == newMode) {
|
||||
return false
|
||||
}
|
||||
currentServiceMode = newMode
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun needVPNService(): Boolean {
|
||||
val filePath = activeConfigPath
|
||||
if (filePath.isBlank()) return false
|
||||
val content = JSONObject(File(filePath).readText())
|
||||
val inbounds = content.getJSONArray("inbounds")
|
||||
for (index in 0 until inbounds.length()) {
|
||||
val inbound = inbounds.getJSONObject(index)
|
||||
if (inbound.getString("type") == "tun") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.hiddify.hiddify.bg.BoxService
|
||||
import com.hiddify.hiddify.bg.ServiceConnection
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
|
||||
class ShortcutActivity : Activity(), ServiceConnection.Callback {
|
||||
|
||||
private val connection = ServiceConnection(this, this, false)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
|
||||
setResult(
|
||||
RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(
|
||||
this,
|
||||
ShortcutInfoCompat.Builder(this, "toggle")
|
||||
.setIntent(
|
||||
Intent(
|
||||
this,
|
||||
ShortcutActivity::class.java
|
||||
).setAction(Intent.ACTION_MAIN)
|
||||
)
|
||||
.setIcon(
|
||||
IconCompat.createWithResource(
|
||||
this,
|
||||
R.mipmap.ic_launcher
|
||||
)
|
||||
)
|
||||
.setShortLabel(getString(R.string.quick_toggle))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
finish()
|
||||
} else {
|
||||
connection.connect()
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
getSystemService<ShortcutManager>()?.reportShortcutUsed("toggle")
|
||||
}
|
||||
}
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
when (status) {
|
||||
Status.Started -> BoxService.stop()
|
||||
Status.Stopped -> BoxService.start()
|
||||
else -> {}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
connection.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import com.hiddify.hiddify.utils.CommandClient
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.JSONMethodCodec
|
||||
import io.nekohasekai.libbox.StatusMessage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class StatsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler{
|
||||
companion object {
|
||||
const val TAG = "A/StatsChannel"
|
||||
const val STATS_CHANNEL = "com.baer.app/stats"
|
||||
}
|
||||
|
||||
private val commandClient =
|
||||
CommandClient(scope, CommandClient.ConnectionType.Status, this)
|
||||
|
||||
private var statsChannel: EventChannel? = null
|
||||
private var statsEvent: EventChannel.EventSink? = null
|
||||
|
||||
override fun updateStatus(status: StatusMessage) {
|
||||
MainActivity.instance.runOnUiThread {
|
||||
val map = listOf(
|
||||
Pair("connections-in", status.connectionsIn),
|
||||
Pair("connections-out", status.connectionsOut),
|
||||
Pair("uplink", status.uplink),
|
||||
Pair("downlink", status.downlink),
|
||||
Pair("uplink-total", status.uplinkTotal),
|
||||
Pair("downlink-total", status.downlinkTotal)
|
||||
).associate { it.first to it.second }
|
||||
statsEvent?.success(map)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
statsChannel = EventChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
STATS_CHANNEL,
|
||||
JSONMethodCodec.INSTANCE
|
||||
)
|
||||
|
||||
statsChannel!!.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
statsEvent = events
|
||||
Log.d(TAG, "connecting stats command client")
|
||||
commandClient.connect()
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
statsEvent = null
|
||||
Log.d(TAG, "disconnecting stats command client")
|
||||
commandClient.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
statsEvent = null
|
||||
commandClient.disconnect()
|
||||
statsChannel?.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.hiddify.hiddify.Settings
|
||||
|
||||
class AppChangeReceiver : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/AppChangeReceiver"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
checkUpdate(context, intent)
|
||||
}
|
||||
|
||||
private fun checkUpdate(context: Context, intent: Intent) {
|
||||
// if (!Settings.perAppProxyEnabled) {
|
||||
// return
|
||||
// }
|
||||
// val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
|
||||
// if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
|
||||
// return
|
||||
// }
|
||||
// val packageName = intent.dataString?.substringAfter("package:")
|
||||
// if (packageName.isNullOrBlank()) {
|
||||
// return
|
||||
// }
|
||||
// if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
|
||||
// Settings.perAppProxyList = Settings.perAppProxyList + packageName
|
||||
// } else {
|
||||
// Settings.perAppProxyList = Settings.perAppProxyList - packageName
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.hiddify.hiddify.Settings
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
}
|
||||
|
||||
else -> return
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (Settings.startedByUser) {
|
||||
withContext(Dispatchers.Main) {
|
||||
BoxService.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.hiddify.hiddify.Application
|
||||
import com.hiddify.hiddify.R
|
||||
import com.hiddify.hiddify.Settings
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import go.Seq
|
||||
import io.nekohasekai.libbox.BoxService
|
||||
import io.nekohasekai.libbox.CommandServer
|
||||
import io.nekohasekai.libbox.CommandServerHandler
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.libbox.SystemProxyStatus
|
||||
import io.nekohasekai.mobile.Mobile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class BoxService(
|
||||
private val service: Service,
|
||||
private val platformInterface: PlatformInterface
|
||||
) : CommandServerHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/BoxService"
|
||||
|
||||
private var initializeOnce = false
|
||||
private lateinit var workingDir: File
|
||||
private fun initialize() {
|
||||
if (initializeOnce) return
|
||||
val baseDir = Application.application.filesDir
|
||||
|
||||
baseDir.mkdirs()
|
||||
workingDir = Application.application.getExternalFilesDir(null) ?: return
|
||||
workingDir.mkdirs()
|
||||
val tempDir = Application.application.cacheDir
|
||||
tempDir.mkdirs()
|
||||
Log.d(TAG, "base dir: ${baseDir.path}")
|
||||
Log.d(TAG, "working dir: ${workingDir.path}")
|
||||
Log.d(TAG, "temp dir: ${tempDir.path}")
|
||||
|
||||
Mobile.setup(baseDir.path, workingDir.path, tempDir.path, false)
|
||||
Libbox.redirectStderr(File(workingDir, "stderr.log").path)
|
||||
initializeOnce = true
|
||||
return
|
||||
}
|
||||
|
||||
fun parseConfig(path: String, tempPath: String, debug: Boolean): String {
|
||||
return try {
|
||||
Mobile.parse(path, tempPath, debug)
|
||||
""
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
e.message ?: "invalid config"
|
||||
}
|
||||
}
|
||||
|
||||
fun buildConfig(path: String, options: String): String {
|
||||
return Mobile.buildConfig(path, options)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(Application.application, Settings.serviceClass())
|
||||
}
|
||||
}
|
||||
ContextCompat.startForegroundService(Application.application, intent)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Application.application.sendBroadcast(
|
||||
Intent(Action.SERVICE_CLOSE).setPackage(
|
||||
Application.application.packageName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
Application.application.sendBroadcast(
|
||||
Intent(Action.SERVICE_RELOAD).setPackage(
|
||||
Application.application.packageName
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var fileDescriptor: ParcelFileDescriptor? = null
|
||||
|
||||
private val status = MutableLiveData(Status.Stopped)
|
||||
private val binder = ServiceBinder(status)
|
||||
private val notification = ServiceNotification(status, service)
|
||||
private var boxService: BoxService? = null
|
||||
private var commandServer: CommandServer? = null
|
||||
private var receiverRegistered = false
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Action.SERVICE_CLOSE -> {
|
||||
stopService()
|
||||
}
|
||||
|
||||
Action.SERVICE_RELOAD -> {
|
||||
serviceReload()
|
||||
}
|
||||
|
||||
PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
serviceUpdateIdleMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCommandServer() {
|
||||
val commandServer =
|
||||
CommandServer(this, 300)
|
||||
commandServer.start()
|
||||
this.commandServer = commandServer
|
||||
}
|
||||
|
||||
private var activeProfileName = ""
|
||||
private suspend fun startService(delayStart: Boolean = false) {
|
||||
try {
|
||||
Log.d(TAG, "starting service")
|
||||
// 暂时禁用通知显示
|
||||
// withContext(Dispatchers.Main) {
|
||||
// notification.show(activeProfileName, R.string.status_starting)
|
||||
// }
|
||||
|
||||
val selectedConfigPath = Settings.activeConfigPath
|
||||
if (selectedConfigPath.isBlank()) {
|
||||
stopAndAlert(Alert.EmptyConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
activeProfileName = Settings.activeProfileName
|
||||
|
||||
val configOptions = Settings.configOptions
|
||||
if (configOptions.isBlank()) {
|
||||
stopAndAlert(Alert.EmptyConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
val content = try {
|
||||
Mobile.buildConfig(selectedConfigPath, configOptions)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
stopAndAlert(Alert.EmptyConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
if (Settings.debugMode) {
|
||||
File(workingDir, "current-config.json").writeText(content)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notification.show(activeProfileName, R.string.status_starting)
|
||||
binder.broadcast {
|
||||
it.onServiceResetLogs(listOf())
|
||||
}
|
||||
}
|
||||
|
||||
DefaultNetworkMonitor.start()
|
||||
Libbox.registerLocalDNSTransport(LocalResolver)
|
||||
Libbox.setMemoryLimit(!Settings.disableMemoryLimit)
|
||||
|
||||
val newService = try {
|
||||
Libbox.newService(content, platformInterface)
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.CreateService, e.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (delayStart) {
|
||||
delay(1000L)
|
||||
}
|
||||
|
||||
newService.start()
|
||||
boxService = newService
|
||||
commandServer?.setService(boxService)
|
||||
status.postValue(Status.Started)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notification.show(activeProfileName, R.string.status_started)
|
||||
}
|
||||
notification.start()
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.StartService, e.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun serviceReload() {
|
||||
notification.close()
|
||||
status.postValue(Status.Starting)
|
||||
val pfd = fileDescriptor
|
||||
if (pfd != null) {
|
||||
pfd.close()
|
||||
fileDescriptor = null
|
||||
}
|
||||
commandServer?.setService(null)
|
||||
boxService?.apply {
|
||||
runCatching {
|
||||
close()
|
||||
}.onFailure {
|
||||
writeLog("service: error when closing: $it")
|
||||
}
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
boxService = null
|
||||
runBlocking {
|
||||
startService(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSystemProxyStatus(): SystemProxyStatus {
|
||||
val status = SystemProxyStatus()
|
||||
if (service is VPNService) {
|
||||
status.available = service.systemProxyAvailable
|
||||
status.enabled = service.systemProxyEnabled
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
override fun setSystemProxyEnabled(isEnabled: Boolean) {
|
||||
serviceReload()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun serviceUpdateIdleMode() {
|
||||
if (Application.powerManager.isDeviceIdleMode) {
|
||||
boxService?.pause()
|
||||
} else {
|
||||
boxService?.wake()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
if (status.value != Status.Started) return
|
||||
status.value = Status.Stopping
|
||||
if (receiverRegistered) {
|
||||
service.unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
notification.close()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val pfd = fileDescriptor
|
||||
if (pfd != null) {
|
||||
pfd.close()
|
||||
fileDescriptor = null
|
||||
}
|
||||
commandServer?.setService(null)
|
||||
boxService?.apply {
|
||||
runCatching {
|
||||
close()
|
||||
}.onFailure {
|
||||
writeLog("service: error when closing: $it")
|
||||
}
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
boxService = null
|
||||
Libbox.registerLocalDNSTransport(null)
|
||||
DefaultNetworkMonitor.stop()
|
||||
|
||||
commandServer?.apply {
|
||||
close()
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
commandServer = null
|
||||
Settings.startedByUser = false
|
||||
withContext(Dispatchers.Main) {
|
||||
status.value = Status.Stopped
|
||||
service.stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun postServiceClose() {
|
||||
// Not used on Android
|
||||
}
|
||||
|
||||
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
|
||||
Settings.startedByUser = false
|
||||
withContext(Dispatchers.Main) {
|
||||
if (receiverRegistered) {
|
||||
service.unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
notification.close()
|
||||
binder.broadcast { callback ->
|
||||
callback.onServiceAlert(type.ordinal, message)
|
||||
}
|
||||
status.value = Status.Stopped
|
||||
}
|
||||
}
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
|
||||
status.value = Status.Starting
|
||||
|
||||
if (!receiverRegistered) {
|
||||
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
|
||||
addAction(Action.SERVICE_CLOSE)
|
||||
addAction(Action.SERVICE_RELOAD)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
}
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Settings.startedByUser = true
|
||||
initialize()
|
||||
try {
|
||||
startCommandServer()
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.StartCommandServer, e.message)
|
||||
return@launch
|
||||
}
|
||||
startService()
|
||||
}
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
binder.close()
|
||||
}
|
||||
|
||||
fun onRevoke() {
|
||||
stopService()
|
||||
}
|
||||
|
||||
fun writeLog(message: String) {
|
||||
binder.broadcast {
|
||||
it.onServiceWriteLog(message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
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 com.hiddify.hiddify.Application
|
||||
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) @TargetApi(23) {
|
||||
Application.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)
|
||||
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
|
||||
}
|
||||
}.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) {
|
||||
Application.connectivity.registerBestMatchingNetworkCallback(
|
||||
request,
|
||||
Callback,
|
||||
mainHandler
|
||||
)
|
||||
}
|
||||
|
||||
in 28 until 31 -> @TargetApi(28) { // we want REQUEST here instead of LISTEN
|
||||
Application.connectivity.requestNetwork(request, Callback, mainHandler)
|
||||
}
|
||||
|
||||
in 26 until 28 -> @TargetApi(26) {
|
||||
Application.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)
|
||||
}
|
||||
|
||||
in 24 until 26 -> @TargetApi(24) {
|
||||
Application.connectivity.registerDefaultNetworkCallback(Callback)
|
||||
}
|
||||
|
||||
else -> try {
|
||||
fallback = false
|
||||
Application.connectivity.requestNetwork(request, Callback)
|
||||
} catch (e: RuntimeException) {
|
||||
fallback =
|
||||
true // known bug on API 23: https://stackoverflow.com/a/33509180/2245107
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregister() {
|
||||
runCatching {
|
||||
Application.connectivity.unregisterNetworkCallback(Callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import com.hiddify.hiddify.Application
|
||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||
|
||||
import java.net.NetworkInterface
|
||||
|
||||
object DefaultNetworkMonitor {
|
||||
|
||||
var defaultNetwork: Network? = null
|
||||
private var listener: InterfaceUpdateListener? = null
|
||||
|
||||
suspend fun start() {
|
||||
DefaultNetworkListener.start(this) {
|
||||
defaultNetwork = it
|
||||
checkDefaultInterfaceUpdate(it)
|
||||
}
|
||||
defaultNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Application.connectivity.activeNetwork
|
||||
} else {
|
||||
DefaultNetworkListener.get()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stop() {
|
||||
DefaultNetworkListener.stop(this)
|
||||
}
|
||||
|
||||
suspend fun require(): Network {
|
||||
val network = defaultNetwork
|
||||
if (network != null) {
|
||||
return network
|
||||
}
|
||||
return DefaultNetworkListener.get()
|
||||
}
|
||||
|
||||
fun setListener(listener: InterfaceUpdateListener?) {
|
||||
this.listener = listener
|
||||
checkDefaultInterfaceUpdate(defaultNetwork)
|
||||
}
|
||||
|
||||
private fun checkDefaultInterfaceUpdate(
|
||||
newNetwork: Network?
|
||||
) {
|
||||
val listener = listener ?: return
|
||||
if (newNetwork != null) {
|
||||
val interfaceName =
|
||||
(Application.connectivity.getLinkProperties(newNetwork) ?: return).interfaceName
|
||||
for (times in 0 until 10) {
|
||||
var interfaceIndex: Int
|
||||
try {
|
||||
interfaceIndex = NetworkInterface.getByName(interfaceName).index
|
||||
} catch (e: Exception) {
|
||||
Thread.sleep(100)
|
||||
continue
|
||||
}
|
||||
listener.updateDefaultInterface(interfaceName, interfaceIndex)
|
||||
}
|
||||
} else {
|
||||
listener.updateDefaultInterface("", -1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.net.DnsResolver
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.system.ErrnoException
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hiddify.hiddify.ktx.tryResumeWithException
|
||||
import io.nekohasekai.libbox.ExchangeContext
|
||||
import io.nekohasekai.libbox.LocalDNSTransport
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object LocalResolver : LocalDNSTransport {
|
||||
|
||||
private const val RCODE_NXDOMAIN = 3
|
||||
|
||||
override fun raw(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
|
||||
return runBlocking {
|
||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||
suspendCoroutine { continuation ->
|
||||
val signal = CancellationSignal()
|
||||
ctx.onCancel(signal::cancel)
|
||||
val callback = object : DnsResolver.Callback<ByteArray> {
|
||||
override fun onAnswer(answer: ByteArray, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.rawSuccess(answer)
|
||||
} else {
|
||||
ctx.errorCode(rcode)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
|
||||
override fun onError(error: DnsResolver.DnsException) {
|
||||
when (val cause = error.cause) {
|
||||
is ErrnoException -> {
|
||||
ctx.errnoCode(cause.errno)
|
||||
continuation.resume(Unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
continuation.tryResumeWithException(error)
|
||||
}
|
||||
}
|
||||
DnsResolver.getInstance().rawQuery(
|
||||
defaultNetwork,
|
||||
message,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
|
||||
return runBlocking {
|
||||
val defaultNetwork = DefaultNetworkMonitor.require()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
suspendCoroutine { continuation ->
|
||||
val signal = CancellationSignal()
|
||||
ctx.onCancel(signal::cancel)
|
||||
val callback = object : DnsResolver.Callback<Collection<InetAddress>> {
|
||||
@Suppress("ThrowableNotThrown")
|
||||
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.success((answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
|
||||
.joinToString("\n"))
|
||||
} else {
|
||||
ctx.errorCode(rcode)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
|
||||
override fun onError(error: DnsResolver.DnsException) {
|
||||
when (val cause = error.cause) {
|
||||
is ErrnoException -> {
|
||||
ctx.errnoCode(cause.errno)
|
||||
continuation.resume(Unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
continuation.tryResumeWithException(error)
|
||||
}
|
||||
}
|
||||
val type = when {
|
||||
network.endsWith("4") -> DnsResolver.TYPE_A
|
||||
network.endsWith("6") -> DnsResolver.TYPE_AAAA
|
||||
else -> null
|
||||
}
|
||||
if (type != null) {
|
||||
DnsResolver.getInstance().query(
|
||||
defaultNetwork,
|
||||
domain,
|
||||
type,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
DnsResolver.getInstance().query(
|
||||
defaultNetwork,
|
||||
domain,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val answer = try {
|
||||
defaultNetwork.getAllByName(domain)
|
||||
} catch (e: UnknownHostException) {
|
||||
ctx.errorCode(RCODE_NXDOMAIN)
|
||||
return@runBlocking
|
||||
}
|
||||
ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hiddify.hiddify.Application
|
||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
||||
import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.libbox.StringIterator
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
import io.nekohasekai.libbox.WIFIState
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.InterfaceAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.Enumeration
|
||||
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
|
||||
|
||||
interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
}
|
||||
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
error("invalid argument")
|
||||
}
|
||||
|
||||
override fun useProcFS(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun findConnectionOwner(
|
||||
ipProtocol: Int,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int
|
||||
): Int {
|
||||
val uid = Application.connectivity.getConnectionOwnerUid(
|
||||
ipProtocol,
|
||||
InetSocketAddress(sourceAddress, sourcePort),
|
||||
InetSocketAddress(destinationAddress, destinationPort)
|
||||
)
|
||||
if (uid == Process.INVALID_UID) error("android: connection owner not found")
|
||||
return uid
|
||||
}
|
||||
|
||||
override fun packageNameByUid(uid: Int): String {
|
||||
val packages = Application.packageManager.getPackagesForUid(uid)
|
||||
if (packages.isNullOrEmpty()) error("android: package not found")
|
||||
return packages[0]
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun uidByPackageName(packageName: String): Int {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getPackageUid(
|
||||
packageName, PackageManager.PackageInfoFlags.of(0)
|
||||
)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Application.packageManager.getPackageUid(packageName, 0)
|
||||
} else {
|
||||
Application.packageManager.getApplicationInfo(packageName, 0).uid
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
error("android: package not found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun usePlatformDefaultInterfaceMonitor(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(listener)
|
||||
}
|
||||
|
||||
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(null)
|
||||
}
|
||||
|
||||
override fun usePlatformInterfaceGetter(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
}
|
||||
|
||||
override fun getInterfaces(): NetworkInterfaceIterator {
|
||||
return InterfaceArray(NetworkInterface.getNetworkInterfaces())
|
||||
}
|
||||
|
||||
override fun underNetworkExtension(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun includeAllNetworks(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun clearDNSCache() {
|
||||
}
|
||||
|
||||
override fun readWIFIState(): WIFIState? {
|
||||
return null
|
||||
}
|
||||
|
||||
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
|
||||
NetworkInterfaceIterator {
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasMoreElements()
|
||||
}
|
||||
|
||||
override fun next(): LibboxNetworkInterface {
|
||||
val element = iterator.nextElement()
|
||||
return LibboxNetworkInterface().apply {
|
||||
name = element.name
|
||||
index = element.index
|
||||
runCatching {
|
||||
mtu = element.mtu
|
||||
}
|
||||
addresses =
|
||||
StringArray(
|
||||
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
|
||||
.iterator()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InterfaceAddress.toPrefix(): String {
|
||||
return if (address is Inet6Address) {
|
||||
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
|
||||
} else {
|
||||
"${address.hostAddress}/${networkPrefixLength}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasNext()
|
||||
}
|
||||
|
||||
override fun next(): String {
|
||||
return iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
|
||||
class ProxyService : Service(), PlatformInterfaceWrapper {
|
||||
|
||||
private val service = BoxService(this, this)
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
|
||||
service.onStartCommand(intent, flags, startId)
|
||||
|
||||
override fun onBind(intent: Intent) = service.onBind(intent)
|
||||
override fun onDestroy() = service.onDestroy()
|
||||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.os.RemoteCallbackList
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.hiddify.hiddify.IService
|
||||
import com.hiddify.hiddify.IServiceCallback
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub() {
|
||||
private val callbacks = RemoteCallbackList<IServiceCallback>()
|
||||
private val broadcastLock = Mutex()
|
||||
|
||||
init {
|
||||
status.observeForever {
|
||||
broadcast { callback ->
|
||||
callback.onServiceStatusChanged(it.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun broadcast(work: (IServiceCallback) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
broadcastLock.withLock {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
repeat(count) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(): Int {
|
||||
return (status.value ?: Status.Stopped).ordinal
|
||||
}
|
||||
|
||||
override fun registerCallback(callback: IServiceCallback) {
|
||||
callbacks.register(callback)
|
||||
}
|
||||
|
||||
override fun unregisterCallback(callback: IServiceCallback?) {
|
||||
callbacks.unregister(callback)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
callbacks.kill()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import com.hiddify.hiddify.IService
|
||||
import com.hiddify.hiddify.IServiceCallback
|
||||
import com.hiddify.hiddify.Settings
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServiceConnection(
|
||||
private val context: Context,
|
||||
callback: Callback,
|
||||
private val register: Boolean = true,
|
||||
) : ServiceConnection {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ServiceConnection"
|
||||
}
|
||||
|
||||
private val callback = ServiceCallback(callback)
|
||||
private var service: IService? = null
|
||||
|
||||
val status get() = service?.status?.let { Status.values()[it] } ?: Status.Stopped
|
||||
|
||||
fun connect() {
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(context, Settings.serviceClass()).setAction(Action.SERVICE)
|
||||
}
|
||||
}
|
||||
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnect() {
|
||||
try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(context, Settings.serviceClass()).setAction(Action.SERVICE)
|
||||
}
|
||||
}
|
||||
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
val service = IService.Stub.asInterface(binder)
|
||||
this.service = service
|
||||
try {
|
||||
if (register) service.registerCallback(callback)
|
||||
callback.onServiceStatusChanged(service.status)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "initialize service connection", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
try {
|
||||
service?.unregisterCallback(callback)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "cleanup service connection", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
reconnect()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onServiceStatusChanged(status: Status)
|
||||
fun onServiceAlert(type: Alert, message: String?) {}
|
||||
fun onServiceWriteLog(message: String?) {}
|
||||
fun onServiceResetLogs(messages: MutableList<String>) {}
|
||||
}
|
||||
|
||||
class ServiceCallback(private val callback: Callback) : IServiceCallback.Stub() {
|
||||
override fun onServiceStatusChanged(status: Int) {
|
||||
callback.onServiceStatusChanged(Status.values()[status])
|
||||
}
|
||||
|
||||
override fun onServiceAlert(type: Int, message: String?) {
|
||||
callback.onServiceAlert(Alert.values()[type], message)
|
||||
}
|
||||
|
||||
override fun onServiceWriteLog(message: String?) = callback.onServiceWriteLog(message)
|
||||
|
||||
override fun onServiceResetLogs(messages: MutableList<String>) =
|
||||
callback.onServiceResetLogs(messages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
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.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.hiddify.hiddify.Application
|
||||
import com.hiddify.hiddify.MainActivity
|
||||
import com.hiddify.hiddify.R
|
||||
import com.hiddify.hiddify.Settings
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import com.hiddify.hiddify.utils.CommandClient
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.StatusMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) : BroadcastReceiver(), CommandClient.Handler {
|
||||
companion object {
|
||||
private const val notificationId = 1
|
||||
private const val notificationChannel = "service"
|
||||
private val flags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
fun checkPermission(): Boolean {
|
||||
// 暂时禁用通知权限检查
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val commandClient =
|
||||
CommandClient(GlobalScope, CommandClient.ConnectionType.Status, this)
|
||||
private var receiverRegistered = false
|
||||
|
||||
|
||||
private val notificationBuilder by lazy {
|
||||
NotificationCompat.Builder(service, notificationChannel)
|
||||
.setShowWhen(false)
|
||||
.setOngoing(true)
|
||||
.setContentTitle("BearVPN")
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.ic_stat_logo)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
service,
|
||||
0,
|
||||
Intent(
|
||||
service,
|
||||
MainActivity::class.java
|
||||
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
flags
|
||||
)
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW).apply {
|
||||
addAction(
|
||||
NotificationCompat.Action.Builder(
|
||||
0, service.getText(R.string.stop), PendingIntent.getBroadcast(
|
||||
service,
|
||||
0,
|
||||
Intent(Action.SERVICE_CLOSE).setPackage(service.packageName),
|
||||
flags
|
||||
)
|
||||
).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(profileName: String, @StringRes contentTextId: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Application.notification.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
service.startForeground(
|
||||
notificationId, notificationBuilder
|
||||
.setContentTitle(profileName.takeIf { it.isNotBlank() } ?: "Hiddify")
|
||||
.setContentText(service.getString(contentTextId)).build()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
suspend fun start() {
|
||||
if (Settings.dynamicNotification) {
|
||||
commandClient.connect()
|
||||
withContext(Dispatchers.Main) {
|
||||
registerReceiver()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerReceiver() {
|
||||
service.registerReceiver(this, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
})
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
override fun updateStatus(status: StatusMessage) {
|
||||
val content =
|
||||
Libbox.formatBytes(status.uplink) + "/s ↑\t" + Libbox.formatBytes(status.downlink) + "/s ↓"
|
||||
Application.notificationManager.notify(
|
||||
notificationId,
|
||||
notificationBuilder.setContentText(content).build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
commandClient.connect()
|
||||
}
|
||||
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
commandClient.disconnect()
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
if (receiverRegistered) {
|
||||
service.unregisterReceiver(this)
|
||||
receiverRegistered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
|
||||
@RequiresApi(24)
|
||||
class TileService : TileService(), ServiceConnection.Callback {
|
||||
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
qsTile?.apply {
|
||||
state = when (status) {
|
||||
Status.Started -> Tile.STATE_ACTIVE
|
||||
Status.Stopped -> Tile.STATE_INACTIVE
|
||||
else -> Tile.STATE_UNAVAILABLE
|
||||
}
|
||||
updateTile()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
connection.connect()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
connection.disconnect()
|
||||
super.onStopListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
when (connection.status) {
|
||||
Status.Stopped -> {
|
||||
BoxService.start()
|
||||
}
|
||||
|
||||
Status.Started -> {
|
||||
BoxService.stop()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
import android.util.Log
|
||||
|
||||
import com.hiddify.hiddify.Settings
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import com.hiddify.hiddify.constant.PerAppProxyMode
|
||||
import com.hiddify.hiddify.ktx.toIpPrefix
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/VPNService"
|
||||
}
|
||||
|
||||
private val service = BoxService(this, this)
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
|
||||
service.onStartCommand(intent, flags, startId)
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
val binder = super.onBind(intent)
|
||||
if (binder != null) {
|
||||
return binder
|
||||
}
|
||||
return service.onBind(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
service.onDestroy()
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.Main) {
|
||||
service.onRevoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
protect(fd)
|
||||
}
|
||||
|
||||
var systemProxyAvailable = false
|
||||
var systemProxyEnabled = false
|
||||
fun addIncludePackage(builder: Builder, packageName: String) {
|
||||
if (packageName == this.packageName) {
|
||||
Log.d("VpnService","Cannot include myself: $packageName")
|
||||
return
|
||||
}
|
||||
try {
|
||||
Log.d("VpnService","Including $packageName")
|
||||
builder.addAllowedApplication(packageName)
|
||||
} catch (e: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun addExcludePackage(builder: Builder, packageName: String) {
|
||||
try {
|
||||
Log.d("VpnService","Excluding $packageName")
|
||||
builder.addDisallowedApplication(packageName)
|
||||
} catch (e: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
if (prepare(this) != null) error("android: missing vpn permission")
|
||||
|
||||
val builder = Builder()
|
||||
.setSession("sing-box")
|
||||
.setMtu(options.mtu)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
}
|
||||
|
||||
val inet4Address = options.inet4Address
|
||||
while (inet4Address.hasNext()) {
|
||||
val address = inet4Address.next()
|
||||
builder.addAddress(address.address(), address.prefix())
|
||||
}
|
||||
|
||||
val inet6Address = options.inet6Address
|
||||
while (inet6Address.hasNext()) {
|
||||
val address = inet6Address.next()
|
||||
builder.addAddress(address.address(), address.prefix())
|
||||
}
|
||||
|
||||
if (options.autoRoute) {
|
||||
builder.addDnsServer(options.dnsServerAddress)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val inet4RouteAddress = options.inet4RouteAddress
|
||||
if (inet4RouteAddress.hasNext()) {
|
||||
while (inet4RouteAddress.hasNext()) {
|
||||
builder.addRoute(inet4RouteAddress.next().toIpPrefix())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
val inet6RouteAddress = options.inet6RouteAddress
|
||||
if (inet6RouteAddress.hasNext()) {
|
||||
while (inet6RouteAddress.hasNext()) {
|
||||
builder.addRoute(inet6RouteAddress.next().toIpPrefix())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
|
||||
val inet4RouteExcludeAddress = options.inet4RouteExcludeAddress
|
||||
while (inet4RouteExcludeAddress.hasNext()) {
|
||||
builder.excludeRoute(inet4RouteExcludeAddress.next().toIpPrefix())
|
||||
}
|
||||
|
||||
val inet6RouteExcludeAddress = options.inet6RouteExcludeAddress
|
||||
while (inet6RouteExcludeAddress.hasNext()) {
|
||||
builder.excludeRoute(inet6RouteExcludeAddress.next().toIpPrefix())
|
||||
}
|
||||
} else {
|
||||
val inet4RouteAddress = options.inet4RouteRange
|
||||
if (inet4RouteAddress.hasNext()) {
|
||||
while (inet4RouteAddress.hasNext()) {
|
||||
val address = inet4RouteAddress.next()
|
||||
builder.addRoute(address.address(), address.prefix())
|
||||
}
|
||||
}
|
||||
|
||||
val inet6RouteAddress = options.inet6RouteRange
|
||||
if (inet6RouteAddress.hasNext()) {
|
||||
while (inet6RouteAddress.hasNext()) {
|
||||
val address = inet6RouteAddress.next()
|
||||
builder.addRoute(address.address(), address.prefix())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Settings.perAppProxyEnabled) {
|
||||
val appList = Settings.perAppProxyList
|
||||
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
|
||||
appList.forEach {
|
||||
addIncludePackage(builder,it)
|
||||
}
|
||||
addIncludePackage(builder,packageName)
|
||||
} else {
|
||||
appList.forEach {
|
||||
addExcludePackage(builder,it)
|
||||
}
|
||||
//addExcludePackage(builder,packageName)
|
||||
}
|
||||
} else {
|
||||
val includePackage = options.includePackage
|
||||
if (includePackage.hasNext()) {
|
||||
while (includePackage.hasNext()) {
|
||||
addIncludePackage(builder,includePackage.next())
|
||||
}
|
||||
}
|
||||
val excludePackage = options.excludePackage
|
||||
if (excludePackage.hasNext()) {
|
||||
while (excludePackage.hasNext()) {
|
||||
addExcludePackage(builder,excludePackage.next())
|
||||
}
|
||||
}
|
||||
//addExcludePackage(builder,packageName)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isHTTPProxyEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
systemProxyAvailable = true
|
||||
systemProxyEnabled = Settings.systemProxyEnabled
|
||||
if (systemProxyEnabled) builder.setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
options.httpProxyServer, options.httpProxyServerPort
|
||||
)
|
||||
)
|
||||
} else {
|
||||
systemProxyAvailable = false
|
||||
systemProxyEnabled = false
|
||||
}
|
||||
|
||||
val pfd =
|
||||
builder.establish() ?: error("android: the application is not prepared or is revoked")
|
||||
service.fileDescriptor = pfd
|
||||
return pfd.fd
|
||||
}
|
||||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object Action {
|
||||
const val SERVICE = "com.baer.app.SERVICE"
|
||||
const val SERVICE_CLOSE = "com.baer.app.SERVICE_CLOSE"
|
||||
const val SERVICE_RELOAD = "com.baer.app.sfa.SERVICE_RELOAD"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Alert {
|
||||
RequestVPNPermission,
|
||||
RequestNotificationPermission,
|
||||
EmptyConfiguration,
|
||||
StartCommandServer,
|
||||
CreateService,
|
||||
StartService
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object PerAppProxyMode {
|
||||
const val OFF = "off"
|
||||
const val INCLUDE = "include"
|
||||
const val EXCLUDE = "exclude"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object ServiceMode {
|
||||
const val NORMAL = "proxy"
|
||||
const val VPN = "vpn"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object SettingsKey {
|
||||
private const val KEY_PREFIX = "flutter."
|
||||
|
||||
const val SERVICE_MODE = "${KEY_PREFIX}service-mode"
|
||||
const val ACTIVE_CONFIG_PATH = "${KEY_PREFIX}active_config_path"
|
||||
const val ACTIVE_PROFILE_NAME = "${KEY_PREFIX}active_profile_name"
|
||||
|
||||
const val PER_APP_PROXY_MODE = "${KEY_PREFIX}per_app_proxy_mode"
|
||||
const val PER_APP_PROXY_INCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_include_list"
|
||||
const val PER_APP_PROXY_EXCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_exclude_list"
|
||||
|
||||
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
|
||||
const val DISABLE_MEMORY_LIMIT = "${KEY_PREFIX}disable_memory_limit"
|
||||
const val DYNAMIC_NOTIFICATION = "${KEY_PREFIX}dynamic_notification"
|
||||
const val SYSTEM_PROXY_ENABLED = "${KEY_PREFIX}system_proxy_enabled"
|
||||
|
||||
// cache
|
||||
|
||||
const val STARTED_BY_USER = "${KEY_PREFIX}started_by_user"
|
||||
const val CONFIG_OPTIONS = "config_options_json"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Status {
|
||||
Stopped,
|
||||
Starting,
|
||||
Started,
|
||||
Stopping,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hiddify.hiddify.ktx
|
||||
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
|
||||
fun <T> Continuation<T>.tryResume(value: T) {
|
||||
try {
|
||||
resumeWith(Result.success(value))
|
||||
} catch (ignored: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Continuation<T>.tryResumeWithException(exception: Throwable) {
|
||||
try {
|
||||
resumeWith(Result.failure(exception))
|
||||
} catch (ignored: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hiddify.hiddify.ktx
|
||||
|
||||
import android.net.IpPrefix
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.nekohasekai.libbox.RoutePrefix
|
||||
import io.nekohasekai.libbox.StringIterator
|
||||
import java.net.InetAddress
|
||||
|
||||
fun StringIterator.toList(): List<String> {
|
||||
return mutableListOf<String>().apply {
|
||||
while (hasNext()) {
|
||||
add(next())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
fun RoutePrefix.toIpPrefix() = IpPrefix(InetAddress.getByName(address()), prefix())
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.hiddify.hiddify.utils
|
||||
|
||||
import go.Seq
|
||||
import io.nekohasekai.libbox.CommandClient
|
||||
import io.nekohasekai.libbox.CommandClientHandler
|
||||
import io.nekohasekai.libbox.CommandClientOptions
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import io.nekohasekai.libbox.OutboundGroupIterator
|
||||
import io.nekohasekai.libbox.StatusMessage
|
||||
import io.nekohasekai.libbox.StringIterator
|
||||
import com.hiddify.hiddify.ktx.toList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class CommandClient(
|
||||
private val scope: CoroutineScope,
|
||||
private val connectionType: ConnectionType,
|
||||
private val handler: Handler
|
||||
) {
|
||||
|
||||
enum class ConnectionType {
|
||||
Status, Groups, Log, ClashMode, GroupOnly
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun onConnected() {}
|
||||
fun onDisconnected() {}
|
||||
fun updateStatus(status: StatusMessage) {}
|
||||
fun updateGroups(groups: List<OutboundGroup>) {}
|
||||
fun clearLog() {}
|
||||
fun appendLog(message: String) {}
|
||||
fun initializeClashMode(modeList: List<String>, currentMode: String) {}
|
||||
fun updateClashMode(newMode: String) {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private var commandClient: CommandClient? = null
|
||||
private val clientHandler = ClientHandler()
|
||||
fun connect() {
|
||||
disconnect()
|
||||
val options = CommandClientOptions()
|
||||
options.command = when (connectionType) {
|
||||
ConnectionType.Status -> Libbox.CommandStatus
|
||||
ConnectionType.Groups -> Libbox.CommandGroup
|
||||
ConnectionType.Log -> Libbox.CommandLog
|
||||
ConnectionType.ClashMode -> Libbox.CommandClashMode
|
||||
ConnectionType.GroupOnly -> Libbox.CommandGroupInfoOnly
|
||||
}
|
||||
options.statusInterval = 2 * 1000 * 1000 * 1000
|
||||
val commandClient = CommandClient(clientHandler, options)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
for (i in 1..10) {
|
||||
delay(100 + i.toLong() * 50)
|
||||
try {
|
||||
commandClient.connect()
|
||||
} catch (ignored: Exception) {
|
||||
continue
|
||||
}
|
||||
if (!isActive) {
|
||||
runCatching {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
this@CommandClient.commandClient = commandClient
|
||||
return@launch
|
||||
}
|
||||
runCatching {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
commandClient?.apply {
|
||||
runCatching {
|
||||
disconnect()
|
||||
}
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
commandClient = null
|
||||
}
|
||||
|
||||
private inner class ClientHandler : CommandClientHandler {
|
||||
|
||||
override fun connected() {
|
||||
handler.onConnected()
|
||||
}
|
||||
|
||||
override fun disconnected(message: String?) {
|
||||
handler.onDisconnected()
|
||||
}
|
||||
|
||||
override fun writeGroups(message: OutboundGroupIterator?) {
|
||||
if (message == null) {
|
||||
return
|
||||
}
|
||||
val groups = mutableListOf<OutboundGroup>()
|
||||
while (message.hasNext()) {
|
||||
groups.add(message.next())
|
||||
}
|
||||
handler.updateGroups(groups)
|
||||
}
|
||||
|
||||
override fun clearLog() {
|
||||
handler.clearLog()
|
||||
}
|
||||
|
||||
override fun writeLog(message: String?) {
|
||||
if (message == null) {
|
||||
return
|
||||
}
|
||||
handler.appendLog(message)
|
||||
}
|
||||
|
||||
override fun writeStatus(message: StatusMessage?) {
|
||||
if (message == null) {
|
||||
return
|
||||
}
|
||||
handler.updateStatus(message)
|
||||
}
|
||||
|
||||
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {
|
||||
handler.initializeClashMode(modeList.toList(), currentMode)
|
||||
}
|
||||
|
||||
override fun updateClashMode(newMode: String) {
|
||||
handler.updateClashMode(newMode)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.hiddify.hiddify.utils
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import io.nekohasekai.libbox.OutboundGroupItem
|
||||
|
||||
data class ParsedOutboundGroup(
|
||||
@SerializedName("tag") val tag: String,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("selected") val selected: String,
|
||||
@SerializedName("items") val items: List<ParsedOutboundGroupItem>
|
||||
) {
|
||||
companion object {
|
||||
fun fromOutbound(group: OutboundGroup): ParsedOutboundGroup {
|
||||
val outboundItems = group.items
|
||||
val items = mutableListOf<ParsedOutboundGroupItem>()
|
||||
while (outboundItems.hasNext()) {
|
||||
items.add(ParsedOutboundGroupItem(outboundItems.next()))
|
||||
}
|
||||
return ParsedOutboundGroup(group.tag, group.type, group.selected, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ParsedOutboundGroupItem(
|
||||
@SerializedName("tag") val tag: String,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("url-test-delay") val urlTestDelay: Int,
|
||||
) {
|
||||
constructor(item: OutboundGroupItem) : this(item.tag, item.type, item.urlTestDelay)
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 背景色 -->
|
||||
|
||||
<!-- 启动图片 -->
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@drawable/splash"
|
||||
android:width="50dp"
|
||||
android:height="50dp" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,53 @@
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="640dp"
|
||||
android:height="360dp"
|
||||
android:viewportWidth="640"
|
||||
android:viewportHeight="360">
|
||||
|
||||
<group android:scaleX="0.6666667"
|
||||
android:scaleY="0.6666667"
|
||||
android:translateX="110"
|
||||
android:translateY="65">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M60,115.28h114.5v113.28h-114.5z"/>
|
||||
<path
|
||||
android:pathData="M144.37,140.82C144.37,139.77 144.92,138.8 145.83,138.26L169.93,123.95C171.94,122.76 174.49,124.19 174.49,126.51V151.05C174.49,152.7 173.15,154.03 171.48,154.03H147.38C145.71,154.03 144.37,152.7 144.37,151.05V140.82Z"
|
||||
android:fillColor="#455FE9"/>
|
||||
<path
|
||||
android:pathData="M102.18,164.67C102.18,163.62 102.74,162.65 103.64,162.11L127.75,147.8C129.76,146.61 132.31,148.04 132.31,150.36V192.79V195.77V222.6C132.31,224.25 130.96,225.58 129.3,225.58H105.2C103.53,225.58 102.18,224.25 102.18,222.6V195.77V192.79V164.67ZM61.46,188.94C60.56,189.48 60,190.45 60,191.5V222.6C60,224.25 61.35,225.58 63.01,225.58H87.12C88.78,225.58 90.13,224.25 90.13,222.6V177.19C90.13,174.87 87.58,173.44 85.57,174.63L61.46,188.94Z"
|
||||
android:fillColor="#455FE9"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M90.13,195.77H72.05V219.62H90.13V213.66C90.13,212.01 91.48,210.68 93.14,210.68H99.17C100.83,210.68 102.18,212.01 102.18,213.66V219.62H126.29V195.77H102.18V201.73C102.18,203.38 100.83,204.71 99.17,204.71H93.14C91.48,204.71 90.13,203.38 90.13,201.73V195.77Z"
|
||||
android:fillColor="#455FE9"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M144.37,162.98V222.6C144.37,224.25 145.71,225.58 147.38,225.58H171.48C173.15,225.58 174.49,224.25 174.49,222.6V162.98C174.49,161.33 173.15,160 171.48,160H147.38C145.71,160 144.37,161.33 144.37,162.98Z"
|
||||
android:fillColor="#455FE9"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M536.46,237.06C531.61,237.06 528.04,235.92 525.75,233.66C523.5,231.43 522.38,227.88 522.38,223.01C522.38,221.42 523.18,220.63 524.78,220.63C526.38,220.63 527.18,221.42 527.18,223.01C527.18,226.56 527.85,228.99 529.19,230.32C530.56,231.64 532.98,232.3 536.46,232.3H555.46C558.97,232.3 561.4,231.64 562.73,230.32C564.07,228.99 564.74,226.56 564.74,223.01V207.04C562.64,209.68 559.89,211.59 556.49,212.76C553.14,213.93 549.13,214.51 544.47,214.51H542.07C535.2,214.51 530.2,212.97 527.07,209.87C523.94,206.77 522.38,201.86 522.38,195.14V164.44C522.38,162.86 523.18,162.07 524.78,162.07C526.38,162.07 527.18,162.86 527.18,164.44V195.14C527.18,200.58 528.27,204.38 530.45,206.53C532.66,208.68 536.53,209.76 542.07,209.76H544.47C551.3,209.76 556.38,208.55 559.7,206.13C563.06,203.68 564.74,200.01 564.74,195.14V164.44C564.74,162.86 565.54,162.07 567.14,162.07C568.75,162.07 569.55,162.86 569.55,164.44V223.01C569.55,227.88 568.42,231.43 566.17,233.66C563.92,235.92 560.35,237.06 555.46,237.06H536.46Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M491.05,214.4C489.45,214.4 488.64,213.61 488.64,212.02V166.94H483.61C482,166.94 481.2,166.15 481.2,164.56C481.2,162.97 482,162.18 483.61,162.18H488.64V155.72C488.64,148.93 490.21,143.98 493.34,140.88C496.47,137.79 501.47,136.24 508.34,136.24C510.02,136.24 510.86,137.03 510.86,138.62C510.86,140.2 510.02,141 508.34,141C502.84,141 498.99,142.09 496.77,144.28C494.56,146.47 493.45,150.29 493.45,155.72V162.18H508.45C510.05,162.18 510.86,162.97 510.86,164.56C510.86,166.15 510.05,166.94 508.45,166.94H493.45V212.02C493.45,213.61 492.65,214.4 491.05,214.4Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M463.89,151.53C461.6,151.53 460.46,150.36 460.46,148.02V145.7C460.46,143.43 461.6,142.3 463.89,142.3H466.24C468.49,142.3 469.62,143.43 469.62,145.7V148.02C469.62,150.36 468.49,151.53 466.24,151.53H463.89ZM465.1,214.4C463.49,214.4 462.69,213.61 462.69,212.02V164.56C462.69,162.97 463.49,162.18 465.1,162.18C466.7,162.18 467.5,162.97 467.5,164.56V212.02C467.5,213.61 466.7,214.4 465.1,214.4Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M413.71,214.4C406.73,214.4 401.63,212.84 398.43,209.7C395.26,206.53 393.68,201.49 393.68,194.58V182C393.68,175.09 395.26,170.07 398.43,166.94C401.63,163.77 406.73,162.18 413.71,162.18H438.33V138.62C438.33,137.03 439.13,136.24 440.73,136.24C442.34,136.24 443.14,137.03 443.14,138.62V212.02C443.14,213.61 442.34,214.4 440.73,214.4C439.13,214.4 438.33,213.61 438.33,212.02V205.79C436.73,208.63 434.28,210.78 431,212.25C427.76,213.68 423.45,214.4 418.06,214.4H413.71ZM413.71,209.64H418.06C424.32,209.64 429.27,208.63 432.89,206.59C436.52,204.51 438.33,201.41 438.33,197.3V166.94H413.71C408.1,166.94 404.15,168.07 401.86,170.34C399.61,172.56 398.48,176.45 398.48,182V194.58C398.48,200.13 399.61,204.04 401.86,206.3C404.15,208.53 408.1,209.64 413.71,209.64Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M345.84,214.4C338.86,214.4 333.76,212.84 330.56,209.7C327.39,206.53 325.81,201.49 325.81,194.58V182C325.81,175.09 327.39,170.07 330.56,166.94C333.76,163.77 338.86,162.18 345.84,162.18H370.46V138.62C370.46,137.03 371.26,136.24 372.86,136.24C374.47,136.24 375.27,137.03 375.27,138.62V212.02C375.27,213.61 374.47,214.4 372.86,214.4C371.26,214.4 370.46,213.61 370.46,212.02V205.79C368.86,208.63 366.41,210.78 363.13,212.25C359.89,213.68 355.58,214.4 350.19,214.4H345.84ZM345.84,209.64H350.19C356.45,209.64 361.39,208.63 365.02,206.59C368.65,204.51 370.46,201.41 370.46,197.3V166.94H345.84C340.23,166.94 336.28,168.07 333.99,170.34C331.74,172.56 330.61,176.45 330.61,182V194.58C330.61,200.13 331.74,204.04 333.99,206.3C336.28,208.53 340.23,209.64 345.84,209.64Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M302.77,151.53C300.48,151.53 299.34,150.36 299.34,148.02V145.7C299.34,143.43 300.48,142.3 302.77,142.3H305.12C307.37,142.3 308.5,143.43 308.5,145.7V148.02C308.5,150.36 307.37,151.53 305.12,151.53H302.77ZM303.97,214.4C302.37,214.4 301.57,213.61 301.57,212.02V164.56C301.57,162.97 302.37,162.18 303.97,162.18C305.58,162.18 306.38,162.97 306.38,164.56V212.02C306.38,213.61 305.58,214.4 303.97,214.4Z"
|
||||
android:fillColor="#495057"/>
|
||||
<path
|
||||
android:pathData="M225.37,214.4C224.57,214.4 223.96,214.21 223.54,213.84C223.16,213.42 222.97,212.82 222.97,212.02V145.07C222.97,144.28 223.16,143.7 223.54,143.32C223.96,142.9 224.57,142.7 225.37,142.7C226.17,142.7 226.76,142.9 227.15,143.32C227.57,143.7 227.77,144.28 227.77,145.07V175.66H276.21V145.07C276.21,144.28 276.4,143.7 276.78,143.32C277.2,142.9 277.81,142.7 278.61,142.7C279.41,142.7 280,142.9 280.39,143.32C280.8,143.7 281.02,144.28 281.02,145.07V212.02C281.02,212.82 280.8,213.42 280.39,213.84C280,214.21 279.41,214.4 278.61,214.4C277.81,214.4 277.2,214.21 276.78,213.84C276.4,213.42 276.21,212.82 276.21,212.02V180.42H227.77V212.02C227.77,212.82 227.57,213.42 227.15,213.84C226.76,214.21 226.17,214.4 225.37,214.4Z"
|
||||
android:fillColor="#495057"/>
|
||||
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="2048dp"
|
||||
android:height="2048dp"
|
||||
android:viewportWidth="2048"
|
||||
android:viewportHeight="2048">
|
||||
<group>
|
||||
<!-- Filled rectangle -->
|
||||
<path
|
||||
android:pathData="M0,0 L2048,0 L2048,2048 L0,2048 Z"
|
||||
android:fillColor="#FFF0F3FA" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@drawable/splash"
|
||||
android:width="50dp"
|
||||
android:height="50dp" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background"/>
|
||||
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1008 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
@@ -0,0 +1,21 @@
|
||||
<?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.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#F0F3FA</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</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,22 @@
|
||||
<?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>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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,21 @@
|
||||
<?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">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#ffffff</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</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,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#F0F3FA</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="quick_toggle">Toggle</string>
|
||||
<string name="status_starting">Service starting…</string>
|
||||
<string name="status_started">Service started</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:shortcutId="toggle"
|
||||
android:shortcutLongLabel="@string/quick_toggle"
|
||||
android:shortcutShortLabel="@string/quick_toggle">
|
||||
<intent
|
||||
android:action="android.intent.action.MAIN"
|
||||
android:targetClass="com.hiddify.hiddify.ShortcutActivity"
|
||||
android:targetPackage="app.myhiddify.com" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 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.INTERNET"/>
|
||||
</manifest>
|
||||