Commit 8885f6de authored by sheteng's avatar sheteng

加速修改模式切换

parent db25102b
......@@ -54,10 +54,10 @@ class MainActivity() : XPageActivity(), TencentLocationListener {
super.onCreate(savedInstanceState)
acceleratorViewModel =
ViewModelProvider(this).get(AccelertorViewModel::class.java)
ViewModelProvider(this).get(AccelertorViewModel::class.java)
// app 启动
if (SettingSPUtils.lastAPPOpenReportTime == 0L ||
System.currentTimeMillis() - SettingSPUtils.lastAPPOpenReportTime > 30 * 60 * 1000
System.currentTimeMillis() - SettingSPUtils.lastAPPOpenReportTime > 30 * 60 * 1000
) {
DataRePortUtils.report("start_app")
SettingSPUtils.lastAPPOpenReportTime = System.currentTimeMillis()
......@@ -94,10 +94,12 @@ class MainActivity() : XPageActivity(), TencentLocationListener {
}
private fun getPermission() {
PermissionUtils.permission(Manifest.permission.READ_PHONE_STATE,
PermissionUtils.permission(
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION).rationale({
Manifest.permission.ACCESS_FINE_LOCATION
).rationale({
}).callback(object : PermissionUtils.FullCallback {
override fun onGranted(permissionsGranted: MutableList<String>?) {
permissionsGranted?.forEach {
......@@ -150,13 +152,12 @@ class MainActivity() : XPageActivity(), TencentLocationListener {
super.onResume()
val mSensor = mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mSensorManager?.registerListener(
mSensorEventListener,
mSensor,
SensorManager.SENSOR_DELAY_NORMAL
mSensorEventListener,
mSensor,
SensorManager.SENSOR_DELAY_NORMAL
)
Handler().postDelayed({
AcceleratorUtils.checkVpn()
}, 1000)
AcceleratorUtils.checkVpn(this)
//QQ官方sdk授权
Tencent.setIsPermissionGranted(true)
}
......@@ -183,9 +184,9 @@ class MainActivity() : XPageActivity(), TencentLocationListener {
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
UMShareAPI.get(this).onActivityResult(requestCode, resultCode, data)
......
......@@ -122,14 +122,13 @@ object AcceleratorUtils {
this.connect = connect
NetworkAdapter.initNetworks(context)
ProfileManager.ensureNotEmpty()
}
fun initConnect(context: Context) {
connection.connect(context, shadowsocksConnection)
}
fun checkVpn() {
fun checkVpn(context: Context) {
if (AuthManager.Last_ACC_TIme == 0L || System.currentTimeMillis() - AuthManager.Last_ACC_TIme > 2 * 3600 * 1000) {
return
}
......@@ -173,16 +172,12 @@ object AcceleratorUtils {
}
}
// proxyGames.addAll(profile.individual.split("\n"))
game?.boosterPackageName?.split(",")?.forEach {
if (!StringUtils.isEmpty(it)) {
proxyGames.add(it)
}
}
profile.individual = proxyGames.joinToString("\n")
profile.route = Acl.BYPASS_LAN
profile.password = "barfoo!"
profile.method = "none"
profile.beatsInterval = 10
NetworkAdapter.initNetworks(context)
withContext(Dispatchers.Main) {
ProfileManager.updateProfile(profile)
connect.launch(null)
......
......@@ -37,12 +37,12 @@ object AuthManager {
val executor = Executors.newScheduledThreadPool(1)
var scheduleAtFixedRate: ScheduledFuture<*>? = null
var scheduleAtFixedRate1: ScheduledFuture<*>? = null
val okHttpClient: OkHttpClient
val gson: Gson
val mmkv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE)
private const val KEY_TOKEN = "com.ccwangluo.KEY_TOKEN"
init {
......@@ -98,6 +98,7 @@ object AuthManager {
class AuthException(msg: String) : IOException(msg), BaseService.ExpectedException
suspend fun initAuthProfile(profile: Profile) {
withContext(Dispatchers.IO) {
val gameId: Int = gameId?.toInt() ?: throw AuthException("游戏不存在")
......@@ -128,12 +129,6 @@ object AuthManager {
scheduleAtFixedRate?.cancel(false)
var failedCount: Int = 0
scheduleAtFixedRate = executor.scheduleAtFixedRate({
val context =
if (Build.VERSION.SDK_INT < 24 || Core.user.isUserUnlocked) Core.app else Core.deviceStorage
val configFile = File(context.noBackupFilesDir, BaseService.CONFIG_FILE)
val configJson = JsonStreamParser(configFile.readText()).asSequence()
.single().asJsonObject
if (data.state == BaseService.State.Connected) {
data.proxy?.let { proxyInstance ->
val refreshTokenResult = postSync<RefreshTokenRes>(
......@@ -144,8 +139,6 @@ object AuthManager {
if (refreshTokenResult.isSuccessful()) {
failedCount = 0
proxyInstance.profile.token = refreshTokenResult.unwrapData().token
configJson.addProperty("token", proxyInstance.profile.token)
configFile.writeText(configJson.toString())
data.tunFd?.let {
AccVpn.writeConfig(Core.app, it, proxyInstance.profile)
}
......@@ -162,34 +155,52 @@ object AuthManager {
}
}
}
GlobalScope.launch {
testLatency()
}
}
}, 1, 1, TimeUnit.MINUTES)
scheduleAtFixedRate1?.cancel(false)
scheduleAtFixedRate1 = executor.scheduleAtFixedRate(
{
Timber.d("testLatency")
GlobalScope.launch {
testLatency()
if (wifiLatencyList.size == 5) {
val wifiavg = wifiLatencyList.average().toInt()
val celavg = cellularLatencyList.average().toInt()
if (wifiavg > celavg && wifiavg > 200) {
NetworkAdapter.useCellular(Core.deviceStorage)
}
wifiLatencyList.clear()
cellularLatencyList.clear()
}
}
},
20,
20,
TimeUnit.SECONDS
)
}
private val wifiLatencyList = mutableListOf<Int>()
private val cellularLatencyList = mutableListOf<Int>()
private suspend fun testLatency() {
val networkAutoSwitch =
AuthManager.isSmartAccOpen // 根据用户设置测试延迟 ,过高时切换 connection.bindNetwork(NetworkMode.CELLULAR.code)
isSmartAccOpen // 根据用户设置测试延迟 ,过高时切换 connection.bindNetwork(NetworkMode.CELLULAR.code)
if (networkAutoSwitch) {
val wifiLatency: Int = doTestLatency(
DataStore.pingAccHost,
DataStore.pingAccPort,
NetworkAdapter.wifiNetwork
)
wifiLatencyList.add(wifiLatency)
val cellularLatency: Int = doTestLatency(
DataStore.pingAccHost,
DataStore.pingAccPort,
NetworkAdapter.cellularNetwork
)
cellularLatencyList.add(cellularLatency)
Timber.d("wifi latency: $wifiLatency, cellular latency: $cellularLatency")
} else {
val currentNetworkLatency =
doTestLatency(DataStore.pingAccHost, DataStore.pingAccPort, null, true)
Timber.d("network latency: $currentNetworkLatency")
}
}
......
plugins {
id("com.android.application")
id("com.google.android.gms.oss-licenses-plugin")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
kotlin("android")
id("kotlin-parcelize")
}
setupApp()
android.defaultConfig.applicationId = "com.github.shadowsocks"
dependencies {
val cameraxVersion = "1.0.0"
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.camera:camera-camera2:$cameraxVersion")
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
implementation("androidx.camera:camera-view:1.0.0-alpha26")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("com.google.mlkit:barcode-scanning:16.2.0")
implementation("com.google.zxing:core:3.4.1")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.4")
implementation("me.zhanghai.android.fastscroll:library:1.1.7")
}
{
"project_info": {
"project_number": "261400168171",
"firebase_url": "https://admob-app-id-3330146721.firebaseio.com",
"project_id": "admob-app-id-3330146721",
"storage_bucket": "admob-app-id-3330146721.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:261400168171:android:dbdd6331c434162f",
"android_client_info": {
"package_name": "com.github.shadowsocks"
}
},
"oauth_client": [
{
"client_id": "261400168171-pcgrp7aprh210vvfc6vut3am2n3chopq.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.github.shadowsocks",
"certificate_hash": "7309a97ff2680788113c6d0ce53898b911885ff5"
}
},
{
"client_id": "261400168171-sfik8o3pj7e243583olorh7s5974vab1.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.github.shadowsocks",
"certificate_hash": "58a90f84cfe99d4280aec677c9a1292fae131677"
}
},
{
"client_id": "261400168171-g7aelv5bu012ojr7dod7lq09c9anjimh.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCee3fAad7nb3YsxeUO9mqqHFfAvsSCbVs"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "261400168171-g7aelv5bu012ojr7dod7lq09c9anjimh.apps.googleusercontent.com",
"client_type": 3
}
]
}
},
"admob_app_id": "ca-app-pub-3283768469187309~3571758745"
},
{
"client_info": {
"mobilesdk_app_id": "1:261400168171:android:0dbac07695d93817",
"android_client_info": {
"package_name": "com.github.shadowsocks.tv"
}
},
"oauth_client": [
{
"client_id": "261400168171-g7aelv5bu012ojr7dod7lq09c9anjimh.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCee3fAad7nb3YsxeUO9mqqHFfAvsSCbVs"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "261400168171-g7aelv5bu012ojr7dod7lq09c9anjimh.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.shadowsocks"
tools:ignore="MissingLeanbackSupport">
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.CAMERA" />
<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" />
<application
android:name=".App"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="MissingTvBanner">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Shadowsocks.Immersive.Navigation"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ProfileConfigActivity"
android:theme="@style/Theme.Shadowsocks.Immersive"
android:excludeFromRecents="true"
android:label="@string/profile_config"
android:launchMode="singleTask"/>
<activity
android:name=".AppManager"
android:label="@string/proxied_apps"
android:parentActivityName=".ProfileConfigActivity"
android:theme="@style/Theme.Shadowsocks.Immersive"
android:launchMode="singleTask"
android:excludeFromRecents="true"/>
<activity
android:name=".UdpFallbackProfileActivity"
android:label="@string/udp_fallback"
android:theme="@style/Theme.Shadowsocks.Immersive"
android:launchMode="singleTask"
android:excludeFromRecents="true"/>
<activity
android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Shadowsocks.Immersive"
android:launchMode="singleTask"
android:screenOrientation="locked"
android:excludeFromRecents="true"/>
<activity android:name=".tasker.ConfigActivity"
android:theme="@style/Theme.Shadowsocks.Immersive">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<activity
android:name=".QuickToggleShortcut"
android:label="@string/quick_toggle"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:taskAffinity=""
android:process=":bg">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
<service android:name=".bg.TileService"
android:label="@string/quick_toggle"
android:process=":bg"
android:directBootAware="true"
android:icon="@drawable/ic_service_active"
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>
<receiver android:name=".tasker.ActionListener"
android:process=":bg"
android:directBootAware="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
</receiver>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3283768469187309~3571758745"/>
</application>
</manifest>
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.ViewCompat
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
class AboutFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_about, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
toolbar.title = getString(R.string.about_title, BuildConfig.VERSION_NAME)
view.findViewById<TextView>(R.id.tv_about).apply {
ViewCompat.setOnApplyWindowInsetsListener(this, MainListListener)
text = SpannableStringBuilder(resources.openRawResource(R.raw.about).bufferedReader().readText()
.parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM)).apply {
for (span in getSpans(0, length, URLSpan::class.java)) {
setSpan(object : ClickableSpan() {
override fun onClick(view: View) = when {
span.url.startsWith("#") -> {
startActivity(Intent(context, OssLicensesMenuActivity::class.java))
}
span.url.startsWith("mailto:") -> {
startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SENDTO
data = span.url.toUri()
}, getString(R.string.send_email)))
}
else -> (activity as MainActivity).launchUrl(span.url)
}
}, getSpanStart(span), getSpanEnd(span), getSpanFlags(span))
removeSpan(span)
}
}
movementMethod = LinkMovementMethod.getInstance()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Application
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
class App : Application(), androidx.work.Configuration.Provider by Core {
override fun onCreate() {
super.onCreate()
Core.init(this, MainActivity::class)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Core.updateNotificationChannels()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.SparseBooleanArray
import android.view.*
import android.widget.*
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.util.set
import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.listenForPackageChanges
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.ListListener
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import me.zhanghai.android.fastscroll.PopupTextProvider
import kotlin.coroutines.coroutineContext
class AppManager : AppCompatActivity() {
companion object {
@SuppressLint("StaticFieldLeak")
private var instance: AppManager? = null
private const val SWITCH = "switch"
private var receiver: BroadcastReceiver? = null
private var cachedApps: Map<String, PackageInfo>? = null
private fun getCachedApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = app.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
cachedApps = null
}
instance?.loadApps()
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES)
.filter {
when (it.packageName) {
app.packageName -> false
"android" -> true
else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
}
}
.associateBy { it.packageName }
this.cachedApps = cachedApps
cachedApps
}
}
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
val packageName: String) {
val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting
val icon: Drawable get() = appInfo.loadIcon(pm)
val uid get() = appInfo.uid
}
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private lateinit var item: ProxiedApp
init {
view.setOnClickListener(this)
}
fun bind(app: ProxiedApp) {
item = app
itemView.findViewById<ImageView>(R.id.itemicon).setImageDrawable(app.icon)
itemView.findViewById<TextView>(R.id.title).text = app.name
itemView.findViewById<TextView>(R.id.desc).text = "${app.packageName} (${app.uid})"
itemView.findViewById<Switch>(R.id.itemcheck).isChecked = isProxiedApp(app)
}
fun handlePayload(payloads: List<String>) {
if (payloads.contains(SWITCH)) itemView.findViewById<Switch>(R.id.itemcheck).isChecked = isProxiedApp(item)
}
override fun onClick(v: View?) {
if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true
DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName }
DataStore.dirty = true
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
}
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), Filterable, PopupTextProvider {
private var filteredApps = apps
suspend fun reload() {
apps = getCachedApps(packageManager).map { (packageName, packageInfo) ->
coroutineContext[Job]!!.ensureActive()
ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)
}.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(filteredApps[position])
override fun onBindViewHolder(holder: AppViewHolder, position: Int, payloads: List<Any>) {
if (payloads.isNotEmpty()) {
@Suppress("UNCHECKED_CAST")
holder.handlePayload(payloads as List<String>)
return
}
onBindViewHolder(holder, position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =
AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_apps_item, parent, false))
override fun getItemCount(): Int = filteredApps.size
private val filterImpl = object : Filter() {
override fun performFiltering(constraint: CharSequence) = FilterResults().apply {
val filteredApps = if (constraint.isEmpty()) apps else apps.filter {
it.name.contains(constraint, true) ||
it.packageName.contains(constraint, true) ||
it.uid.toString().contains(constraint)
}
count = filteredApps.size
values = filteredApps
}
override fun publishResults(constraint: CharSequence, results: FilterResults) {
@Suppress("UNCHECKED_CAST")
filteredApps = results.values as List<ProxiedApp>
notifyDataSetChanged()
}
}
override fun getFilter(): Filter = filterImpl
override fun getPopupText(position: Int) = filteredApps[position].name.firstOrNull()?.toString() ?: ""
}
private val loading by lazy { findViewById<View>(R.id.loading) }
private lateinit var toolbar: Toolbar
private lateinit var bypassGroup: RadioGroup
private lateinit var list: RecyclerView
private lateinit var search: SearchView
private val proxiedUids = SparseBooleanArray()
private var loader: Job? = null
private var apps = emptyList<ProxiedApp>()
private val appsAdapter = AppsAdapter()
private val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() }
private fun View.crossFadeFrom(other: View) {
clearAnimation()
other.clearAnimation()
if (visibility == View.VISIBLE && other.visibility == View.GONE) return
alpha = 0F
visibility = View.VISIBLE
animate().alpha(1F).duration = shortAnimTime
other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
other.visibility = View.GONE
}
}).duration = shortAnimTime
}
private fun initProxiedUids(str: String = DataStore.individual) {
proxiedUids.clear()
val apps = getCachedApps(packageManager)
for (line in str.lineSequence()) proxiedUids[(apps[line] ?: continue).applicationInfo.uid] = true
}
private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid]
@UiThread
private fun loadApps() {
loader?.cancel()
loader = lifecycleScope.launchWhenCreated {
loading.crossFadeFrom(list)
val adapter = list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() }
adapter.filter.filter(search.query)
list.crossFadeFrom(loading)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_apps)
ListHolderListener.setup(this)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (!DataStore.proxyApps) {
DataStore.proxyApps = true
DataStore.dirty = true
}
bypassGroup = findViewById(R.id.bypassGroup)
bypassGroup.check(if (DataStore.bypass) R.id.btn_bypass else R.id.btn_on)
bypassGroup.setOnCheckedChangeListener { _, checkedId ->
DataStore.dirty = true
when (checkedId) {
R.id.btn_off -> {
DataStore.proxyApps = false
finish()
}
R.id.btn_on -> DataStore.bypass = false
R.id.btn_bypass -> DataStore.bypass = true
}
}
initProxiedUids()
list = findViewById(R.id.list)
ViewCompat.setOnApplyWindowInsetsListener(list, ListListener)
list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = appsAdapter
FastScrollerBuilder(list).useMd2Style().build()
search = findViewById(R.id.search)
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?) = true.also { appsAdapter.filter.filter(newText) }
})
instance = this
loadApps()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.app_manager_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_apply_all -> {
val profiles = ProfileManager.getAllProfiles()
if (profiles != null) {
val proxiedAppString = DataStore.individual
profiles.forEach {
it.individual = proxiedAppString
it.bypass = DataStore.bypass
ProfileManager.updateProfile(it)
}
if (DataStore.directBootAware) DirectBoot.update()
Snackbar.make(list, R.string.action_apply_all, Snackbar.LENGTH_LONG).show()
} else Snackbar.make(list, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_export_clipboard -> {
val success = Core.trySetPrimaryClip("${DataStore.bypass}\n${DataStore.individual}")
Snackbar.make(list,
if (success) R.string.action_export_msg else R.string.action_export_err,
Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_import_clipboard -> {
val proxiedAppString = Core.clipboard.primaryClip?.getItemAt(0)?.text?.toString()
if (!proxiedAppString.isNullOrEmpty()) {
val i = proxiedAppString.indexOf('\n')
try {
val (enabled, apps) = if (i < 0) {
proxiedAppString to ""
} else proxiedAppString.substring(0, i) to proxiedAppString.substring(i + 1)
bypassGroup.check(if (enabled.toBoolean()) R.id.btn_bypass else R.id.btn_on)
DataStore.individual = apps
DataStore.dirty = true
Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedUids(apps)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
return true
} catch (_: IllegalArgumentException) { }
}
Snackbar.make(list, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
}
}
return super.onOptionsItemSelected(item)
}
override fun supportNavigateUpTo(upIntent: Intent) =
super.supportNavigateUpTo(upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU) {
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
} else super.onKeyUp(keyCode, event)
override fun onDestroy() {
instance = null
loader?.cancel()
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import com.github.shadowsocks.widget.ListHolderListener
class GlobalSettingsFragment : ToolbarFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_global_settings, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
toolbar.setTitle(R.string.settings)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.view.ViewCompat
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.EditTextPreferenceModifiers
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.remove
import com.github.shadowsocks.widget.MainListListener
class GlobalSettingsPreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore
DataStore.initGlobal()
addPreferencesFromResource(R.xml.pref_global)
findPreference<SwitchPreference>(Key.persistAcrossReboot)!!.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean
true
}
val canToggleLocked = findPreference<Preference>(Key.directBootAware)!!
if (Build.VERSION.SDK_INT >= 24) canToggleLocked.setOnPreferenceChangeListener { _, newValue ->
if (Core.directBootSupported && newValue as Boolean) DirectBoot.update() else DirectBoot.clean()
true
} else canToggleLocked.remove()
val serviceMode = findPreference<Preference>(Key.serviceMode)!!
val portProxy = findPreference<EditTextPreference>(Key.portProxy)!!
portProxy.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
val portLocalDns = findPreference<EditTextPreference>(Key.portLocalDns)!!
portLocalDns.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
val portTransproxy = findPreference<EditTextPreference>(Key.portTransproxy)!!
portTransproxy.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
val onServiceModeChange = Preference.OnPreferenceChangeListener { _, newValue ->
portTransproxy.isEnabled = newValue as String? == Key.modeTransproxy
true
}
val listener: (BaseService.State) -> Unit = {
val stopped = it == BaseService.State.Stopped
serviceMode.isEnabled = stopped
portProxy.isEnabled = stopped
portLocalDns.isEnabled = stopped
if (stopped) onServiceModeChange.onPreferenceChange(null, DataStore.serviceMode) else {
portTransproxy.isEnabled = false
}
}
listener((activity as MainActivity).state)
MainActivity.stateListener = listener
serviceMode.onPreferenceChangeListener = onServiceModeChange
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(listView, MainListListener)
}
override fun onDestroy() {
MainActivity.stateListener = null
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.ActivityNotFoundException
import android.os.Bundle
import android.os.RemoteException
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MenuItem
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.*
import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.subscription.SubscriptionFragment
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.StartService
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.analytics.ktx.logEvent
import com.google.firebase.ktx.Firebase
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPreferenceDataStoreChangeListener,
NavigationView.OnNavigationItemSelectedListener {
companion object {
var stateListener: ((BaseService.State) -> Unit)? = null
}
// UI
private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar
internal lateinit var drawer: DrawerLayout
private lateinit var navigation: NavigationView
lateinit var snackbar: CoordinatorLayout private set
fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG).apply {
anchorView = fab
}
private val customTabsIntent by lazy {
CustomTabsIntent.Builder().apply {
setColorScheme(CustomTabsIntent.COLOR_SCHEME_SYSTEM)
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, CustomTabColorSchemeParams.Builder().apply {
setToolbarColor(ContextCompat.getColor(this@MainActivity, R.color.light_color_primary))
}.build())
setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, CustomTabColorSchemeParams.Builder().apply {
setToolbarColor(ContextCompat.getColor(this@MainActivity, R.color.dark_color_primary))
}.build())
}.build()
}
fun launchUrl(uri: String) = try {
customTabsIntent.launchUrl(this, uri.toUri())
} catch (_: ActivityNotFoundException) {
snackbar(uri).show()
}
// service
var state = BaseService.State.Idle
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
changeState(state, msg, true)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) this@MainActivity.stats.updateTraffic(
stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal)
if (state != BaseService.State.Stopping) {
(supportFragmentManager.findFragmentById(R.id.fragment_holder) as? ProfilesFragment)
?.onTrafficUpdated(profileId, stats)
}
}
override fun trafficPersisted(profileId: Long) {
ProfilesFragment.instance?.onTrafficPersisted(profileId)
}
private fun changeState(state: BaseService.State, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, this.state, animate)
stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
this.state = state
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state)
}
private fun toggle() = if (state.canStop) {
Core.stopService()
} else {
connect.launch(null)
}
private val connection = ShadowsocksConnection(true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
BaseService.State.values()[service.state]
} catch (_: RemoteException) {
BaseService.State.Idle
})
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onBinderDied() {
connection.disconnect(this)
connection.connect(this, this)
}
private val connect = registerForActivityResult(StartService()) {
if (it) snackbar().setText(R.string.vpn_permission_denied).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContentView(R.layout.layout_main)
snackbar = findViewById(R.id.snackbar)
ViewCompat.setOnApplyWindowInsetsListener(snackbar, ListHolderListener)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.State.Connected) stats.testConnection() }
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)
navigation.setNavigationItemSelectedListener(this)
if (savedInstanceState == null) {
navigation.menu.findItem(R.id.profiles).isChecked = true
displayFragment(ProfilesFragment())
}
fab = findViewById(R.id.fab)
fab.initProgress(findViewById(R.id.fabProgress))
fab.setOnClickListener { toggle() }
ViewCompat.setOnApplyWindowInsetsListener(fab) { view, insets ->
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom +
resources.getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fab_bottom_margin)
}
insets
}
changeState(BaseService.State.Idle) // reset everything to init state
connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
when (key) {
Key.serviceMode -> {
connection.disconnect(this)
connection.connect(this, this)
}
}
}
private fun displayFragment(fragment: ToolbarFragment) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_holder, fragment).commitAllowingStateLoss()
drawer.closeDrawers()
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
if (item.isChecked) drawer.closeDrawers() else {
when (item.itemId) {
R.id.profiles -> {
displayFragment(ProfilesFragment())
connection.bandwidthTimeout = connection.bandwidthTimeout // request stats update
}
R.id.globalSettings -> displayFragment(GlobalSettingsFragment())
R.id.about -> {
// Firebase.analytics.logEvent("about") { }
displayFragment(AboutFragment())
}
R.id.faq -> {
launchUrl(getString(R.string.faq_url))
return true
}
R.id.customRules -> displayFragment(CustomRulesFragment())
R.id.subscriptions -> displayFragment(SubscriptionFragment())
else -> return false
}
item.isChecked = true
}
return true
}
override fun onStart() {
super.onStart()
connection.bandwidthTimeout = 500
}
override fun onBackPressed() {
if (drawer.isDrawerOpen(GravityCompat.START)) drawer.closeDrawers() else {
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment
if (!currentFragment.onBackPressed()) {
if (currentFragment is ProfilesFragment) super.onBackPressed() else {
navigation.menu.findItem(R.id.profiles).isChecked = true
displayFragment(ProfilesFragment())
}
}
}
}
override fun onKeyShortcut(keyCode: Int, event: KeyEvent) = when {
keyCode == KeyEvent.KEYCODE_G && event.hasModifiers(KeyEvent.META_CTRL_ON) -> {
toggle()
true
}
keyCode == KeyEvent.KEYCODE_T && event.hasModifiers(KeyEvent.META_CTRL_ON) -> {
stats.testConnection()
true
}
else -> (supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment).toolbar.menu.let {
it.setQwertyMode(KeyCharacterMap.load(event.deviceId).keyboardType != KeyCharacterMap.NUMERIC)
it.performShortcut(keyCode, event, 0)
}
}
override fun onStop() {
connection.bandwidthTimeout = 0
super.onStop()
}
override fun onDestroy() {
super.onDestroy()
DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect(this)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.github.shadowsocks.plugin.fragment.Empty
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.widget.ListHolderListener
class ProfileConfigActivity : AppCompatActivity() {
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.unsaved_changes_prompt)
setPositiveButton(R.string.yes, listener)
setNegativeButton(R.string.no, listener)
setNeutralButton(android.R.string.cancel, null)
}
}
private val child by lazy { supportFragmentManager.findFragmentById(R.id.content) as ProfileConfigFragment }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_profile_config)
ListHolderListener.setup(this)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_navigation_close)
}
}
override fun onSupportNavigateUp(): Boolean {
if (!super.onSupportNavigateUp()) finish()
return true
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.profile_config_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
override fun onBackPressed() {
if (DataStore.dirty) UnsavedChangesDialogFragment().apply {
key()
}.show(supportFragmentManager, null) else super.onBackPressed()
}
val pluginHelp = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
(resultCode, data) ->
if (resultCode == Activity.RESULT_OK) AlertDialog.Builder(this)
.setTitle("?")
.setMessage(data?.getCharSequenceExtra(PluginContract.EXTRA_HELP_MESSAGE))
.show()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import android.view.View
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenCreated
import androidx.preference.*
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.*
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.github.shadowsocks.plugin.fragment.Empty
import com.github.shadowsocks.plugin.fragment.showAllowingStateLoss
import com.github.shadowsocks.preference.*
import com.github.shadowsocks.utils.*
import com.github.shadowsocks.widget.ListListener
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class ProfileConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, OnPreferenceDataStoreChangeListener {
companion object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference?) = "\u2022".repeat(preference?.text?.length ?: 0)
}
@Parcelize
data class ProfileIdArg(val profileId: Long) : Parcelable
class DeleteConfirmationDialogFragment : AlertDialogFragment<ProfileIdArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.delete_confirm_prompt)
setPositiveButton(R.string.yes) { _, _ ->
ProfileManager.delProfile(arg.profileId)
requireActivity().finish()
}
setNegativeButton(R.string.no, null)
}
}
private var profileId = -1L
private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: PluginPreference
private lateinit var pluginConfigure: EditTextPreference
private lateinit var pluginConfiguration: PluginConfiguration
private lateinit var receiver: BroadcastReceiver
private lateinit var udpFallback: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity()
profileId = activity.intent.getLongExtra(Action.EXTRA_PROFILE_ID, -1L)
if (profileId != -1L && profileId != DataStore.editingId) {
activity.finish()
return
}
addPreferencesFromResource(R.xml.pref_profile)
findPreference<EditTextPreference>(Key.remotePort)!!.setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
findPreference<EditTextPreference>(Key.password)!!.summaryProvider = PasswordSummaryProvider
val serviceMode = DataStore.serviceMode
findPreference<Preference>(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps)!!
isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceChangeListener { _, newValue ->
startActivity(Intent(activity, AppManager::class.java))
if (newValue as Boolean) DataStore.dirty = true
newValue
}
findPreference<Preference>(Key.metered)!!.apply {
if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove()
}
plugin = findPreference(Key.plugin)!!
pluginConfigure = findPreference(Key.pluginConfigure)!!
pluginConfigure.setOnBindEditTextListener(EditTextPreferenceModifiers.Monospace)
pluginConfigure.onPreferenceChangeListener = this
pluginConfiguration = PluginConfiguration(DataStore.plugin)
initPlugins()
udpFallback = findPreference(Key.udpFallback)!!
DataStore.privateStore.registerChangeListener(this)
val profile = ProfileManager.getProfile(profileId) ?: Profile()
if (profile.subscription == Profile.SubscriptionStatus.Active) {
findPreference<Preference>(Key.name)!!.isEnabled = false
findPreference<Preference>(Key.host)!!.isEnabled = false
findPreference<Preference>(Key.password)!!.isEnabled = false
findPreference<Preference>(Key.method)!!.isEnabled = false
findPreference<Preference>(Key.remotePort)!!.isEnabled = false
plugin.isEnabled = false
pluginConfigure.isEnabled = false
udpFallback.isEnabled = false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)
setFragmentResultListener(PluginPreferenceDialogFragment::class.java.name) { _, bundle ->
val selected = plugin.plugins.lookup.getValue(
bundle.getString(PluginPreferenceDialogFragment.KEY_SELECTED_ID)!!)
val override = pluginConfiguration.pluginsOptions.keys.firstOrNull {
plugin.plugins.lookup[it] == selected
}
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions, override ?: selected.id)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
plugin.value = pluginConfiguration.selected
pluginConfigure.isEnabled = selected !is NoPlugin
pluginConfigure.text = pluginConfiguration.getOptions().toString()
if (!selected.trusted) {
Snackbar.make(requireView(), R.string.plugin_untrusted, Snackbar.LENGTH_LONG).show()
}
}
AlertDialogFragment.setResultListener<ProfileConfigActivity.UnsavedChangesDialogFragment, Empty>(this) {
which, _ ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> saveAndExit()
DialogInterface.BUTTON_NEGATIVE -> requireActivity().finish()
}
}
}
private fun initPlugins() {
plugin.value = pluginConfiguration.selected
plugin.init()
pluginConfigure.isEnabled = plugin.selectedEntry?.let { it is NoPlugin } == false
pluginConfigure.text = pluginConfiguration.getOptions().toString()
}
private fun showPluginEditor() {
PluginConfigurationDialogFragment().apply {
setArg(Key.pluginConfigure, pluginConfiguration.selected)
setTargetFragment(this@ProfileConfigFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, Key.pluginConfigure)
}
private fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId
profile.deserialize()
ProfileManager.updateProfile(profile)
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profileId)
if (profileId in Core.activeProfileIds && DataStore.directBootAware) DirectBoot.update()
requireActivity().finish()
}
override fun onAttach(context: Context) {
super.onAttach(context)
receiver = context.listenForPackageChanges(false) {
lifecycleScope.launch(Dispatchers.Main) { // wait until changes were flushed
whenCreated { initPlugins() }
}
}
}
override fun onResume() {
super.onResume()
isProxyApps.isChecked = DataStore.proxyApps // fetch proxyApps updated by AppManager
val fallbackProfile = DataStore.udpFallback?.let { ProfileManager.getProfile(it) }
if (fallbackProfile == null) udpFallback.setSummary(R.string.plugin_disabled)
else udpFallback.summary = fallbackProfile.formattedName
}
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean = try {
val selected = pluginConfiguration.selected
pluginConfiguration = PluginConfiguration(pluginConfiguration.pluginsOptions +
(pluginConfiguration.selected to PluginOptions(selected, newValue as? String?)), selected)
DataStore.plugin = pluginConfiguration.toString()
DataStore.dirty = true
true
} catch (exc: RuntimeException) {
Snackbar.make(requireView(), exc.readableMessage, Snackbar.LENGTH_LONG).show()
false
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
if (key != Key.proxyApps && findPreference<Preference>(key) != null) DataStore.dirty = true
}
override fun onDisplayPreferenceDialog(preference: Preference) {
when (preference.key) {
Key.plugin -> PluginPreferenceDialogFragment().apply {
setArg(Key.plugin)
setTargetFragment(this@ProfileConfigFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, Key.plugin)
Key.pluginConfigure -> {
val intent = PluginManager.buildIntent(plugin.selectedEntry!!.id, PluginContract.ACTION_CONFIGURE)
if (intent.resolveActivity(requireContext().packageManager) == null) showPluginEditor() else {
configurePlugin.launch(intent
.putExtra(PluginContract.EXTRA_OPTIONS, pluginConfiguration.getOptions().toString()))
}
}
else -> super.onDisplayPreferenceDialog(preference)
}
}
private val configurePlugin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
(resultCode, data) ->
when (resultCode) {
Activity.RESULT_OK -> {
val options = data?.getStringExtra(PluginContract.EXTRA_OPTIONS)
pluginConfigure.text = options
onPreferenceChange(null, options)
}
PluginContract.RESULT_FALLBACK -> showPluginEditor()
}
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> {
DeleteConfirmationDialogFragment().apply {
arg(ProfileIdArg(profileId))
key()
}.show(parentFragmentManager, null)
true
}
R.id.action_apply -> {
saveAndExit()
true
}
else -> false
}
override fun onDetach() {
requireContext().unregisterReceiver(receiver)
super.onDetach()
}
override fun onDestroy() {
DataStore.privateStore.unregisterChangeListener(this)
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.text.format.Formatter
import android.util.LongSparseArray
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.appcompat.widget.TooltipCompat
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.*
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.fragment.showAllowingStateLoss
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.OpenJson
import com.github.shadowsocks.utils.SaveJson
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.WriterException
import timber.log.Timber
import java.nio.charset.StandardCharsets
class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, SearchView.OnQueryTextListener {
companion object {
/**
* used for callback from stateChanged from MainActivity
*/
var instance: ProfilesFragment? = null
private const val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
private val iso88591 = StandardCharsets.ISO_8859_1.newEncoder()
}
/**
* Is ProfilesFragment editable at all.
*/
private val isEnabled get() = (activity as MainActivity).state.let { it.canStop || it == BaseService.State.Stopped }
private fun isProfileEditable(id: Long) =
(activity as MainActivity).state == BaseService.State.Stopped || id !in Core.activeProfileIds
class QRCodeDialog() : DialogFragment() {
constructor(url: String) : this() {
arguments = bundleOf(Pair(KEY_URL, url))
}
/**
* Based on:
* https://android.googlesource.com/platform/packages/apps/Settings/+/0d706f0/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java
* https://android.googlesource.com/platform/packages/apps/Settings/+/8a9ccfd/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java#153
*/
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = try {
val url = arguments?.getString(KEY_URL)!!
val size = resources.getDimensionPixelSize(R.dimen.qrcode_size)
val hints = mutableMapOf<EncodeHintType, Any>()
if (!iso88591.canEncode(url)) hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name()
val qrBits = MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, size, size, hints)
ImageView(context).apply {
layoutParams = ViewGroup.LayoutParams(size, size)
setImageBitmap(Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).apply {
for (x in 0 until size) for (y in 0 until size) {
setPixel(x, y, if (qrBits.get(x, y)) Color.BLACK else Color.WHITE)
}
})
}
} catch (e: WriterException) {
Timber.w(e)
(activity as MainActivity).snackbar().setText(e.readableMessage).show()
dismiss()
null
}
}
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, PopupMenu.OnMenuItemClickListener {
internal lateinit var item: Profile
private val text1 = itemView.findViewById<TextView>(android.R.id.text1)
private val text2 = itemView.findViewById<TextView>(android.R.id.text2)
private val traffic = itemView.findViewById<TextView>(R.id.traffic)
private val edit = itemView.findViewById<View>(R.id.edit)
private val subscription = itemView.findViewById<View>(R.id.subscription)
init {
edit.setOnClickListener {
item = ProfileManager.getProfile(item.id)!!
startConfig(item)
}
subscription.setOnClickListener {
item = ProfileManager.getProfile(item.id) ?: return@setOnClickListener
startConfig(item)
}
TooltipCompat.setTooltipText(edit, edit.contentDescription)
TooltipCompat.setTooltipText(subscription, subscription.contentDescription)
itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share)
share.setOnClickListener {
val popup = PopupMenu(requireContext(), share)
popup.menuInflater.inflate(R.menu.profile_share_popup, popup.menu)
popup.setOnMenuItemClickListener(this)
popup.show()
}
TooltipCompat.setTooltipText(share, share.contentDescription)
}
fun bind(item: Profile) {
this.item = item
val editable = isProfileEditable(item.id)
edit.isEnabled = editable
edit.alpha = if (editable) 1F else .5F
subscription.isEnabled = editable
subscription.alpha = if (editable) 1F else .5F
var tx = item.tx
var rx = item.rx
statsCache[item.id]?.apply {
tx += txTotal
rx += rxTotal
}
text1.text = item.formattedName
text2.text = ArrayList<String>().apply {
if (!item.name.isNullOrEmpty()) this += item.formattedAddress
val id = PluginConfiguration(item.plugin ?: "").selected
if (id.isNotEmpty()) this += getString(R.string.profile_plugin, id)
}.joinToString("\n")
val context = requireContext()
traffic.text = if (tx <= 0 && rx <= 0) null else getString(R.string.traffic,
Formatter.formatFileSize(context, tx), Formatter.formatFileSize(context, rx))
if (item.id == DataStore.profileId) {
itemView.isSelected = true
selectedItem = this
} else {
itemView.isSelected = false
if (selectedItem === this) selectedItem = null
}
if (item.subscription == Profile.SubscriptionStatus.Active) {
edit.visibility = View.GONE
subscription.visibility = View.VISIBLE
} else {
edit.visibility = View.VISIBLE
subscription.visibility = View.GONE
}
}
override fun onClick(v: View?) {
if (isEnabled) {
val activity = activity as MainActivity
val old = DataStore.profileId
Core.switchProfile(item.id)
profilesAdapter.refreshId(old)
itemView.isSelected = true
if (activity.state.canStop) Core.reloadService()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_qr_code -> {
QRCodeDialog(this.item.toString()).showAllowingStateLoss(parentFragmentManager)
true
}
R.id.action_export_clipboard -> {
val success = Core.trySetPrimaryClip(this.item.toString())
(activity as MainActivity).snackbar().setText(
if (success) R.string.action_export_msg else R.string.action_export_err).show()
true
}
else -> false
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>(), ProfileManager.Listener {
internal val profiles = ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf()
private val updated = HashSet<Profile>()
init {
setHasStableIds(true) // see: http://stackoverflow.com/a/32488059/2245107
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) = holder.bind(profiles[position])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.layout_profile, parent, false))
override fun getItemCount(): Int = profiles.size
override fun getItemId(position: Int): Long = profiles[position].id
override fun onAdd(profile: Profile) {
undoManager.flush()
val pos = itemCount
profiles += profile
notifyItemInserted(pos)
}
fun filter(name: String) {
val active = ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf()
profiles.clear()
val locale = resources.configuration.locale
val lower = name.lowercase(locale)
profiles.addAll(active.filter {
it.name?.lowercase(locale)?.contains(lower) == true || it.host.lowercase(locale).contains(lower)
})
notifyDataSetChanged()
}
fun move(from: Int, to: Int) {
undoManager.flush()
val first = profiles[from]
var previousOrder = first.userOrder
val (step, range) = if (from < to) Pair(1, from until to) else Pair(-1, to + 1 downTo from)
for (i in range) {
val next = profiles[i + step]
val order = next.userOrder
next.userOrder = previousOrder
previousOrder = order
profiles[i] = next
updated.add(next)
}
first.userOrder = previousOrder
profiles[to] = first
updated.add(first)
notifyItemMoved(from, to)
}
fun commitMove() {
updated.forEach { ProfileManager.updateProfile(it) }
updated.clear()
}
fun remove(pos: Int) {
profiles.removeAt(pos)
notifyItemRemoved(pos)
}
fun undo(actions: List<Pair<Int, Profile>>) {
for ((index, item) in actions) {
profiles.add(index, item)
notifyItemInserted(index)
}
}
fun commit(actions: List<Pair<Int, Profile>>) {
for ((_, item) in actions) ProfileManager.delProfile(item.id)
}
fun refreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
if (index >= 0) notifyItemChanged(index)
}
fun deepRefreshId(id: Long) {
val index = profiles.indexOfFirst { it.id == id }
if (index < 0) return
profiles[index] = ProfileManager.getProfile(id)!!
notifyItemChanged(index)
}
override fun onRemove(profileId: Long) {
val index = profiles.indexOfFirst { it.id == profileId }
if (index < 0) return
profiles.removeAt(index)
notifyItemRemoved(index)
if (profileId == DataStore.profileId) DataStore.profileId = 0 // switch to null profile
}
override fun onCleared() {
profiles.clear()
notifyDataSetChanged()
}
override fun reloadProfiles() {
profiles.clear()
ProfileManager.getActiveProfiles()?.let { profiles.addAll(it) }
notifyDataSetChanged()
}
}
private var selectedItem: ProfileViewHolder? = null
val profilesAdapter by lazy { ProfilesAdapter() }
private lateinit var profilesList: RecyclerView
private val layoutManager by lazy { LinearLayoutManager(context, RecyclerView.VERTICAL, false) }
private lateinit var undoManager: UndoSnackbarManager<Profile>
private val statsCache = LongSparseArray<TrafficStats>()
private fun startConfig(profile: Profile) {
profile.serialize()
startActivity(Intent(context, ProfileConfigActivity::class.java).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
}
override fun onQueryTextChange(query: String): Boolean {
profilesAdapter.filter(query)
return false
}
override fun onQueryTextSubmit(query: String): Boolean = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
toolbar.setTitle(R.string.profiles)
toolbar.inflateMenu(R.menu.profile_manager_menu)
toolbar.setOnMenuItemClickListener(this)
val searchView = toolbar.findViewById<SearchView>(R.id.action_search)
searchView.setOnQueryTextListener(this)
searchView.queryHint = getString(android.R.string.search_go)
ProfileManager.ensureNotEmpty()
profilesList = view.findViewById(R.id.list)
ViewCompat.setOnApplyWindowInsetsListener(profilesList, MainListListener)
profilesList.layoutManager = layoutManager
profilesList.addItemDecoration(DividerItemDecoration(context, layoutManager.orientation))
layoutManager.scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == DataStore.profileId })
val animator = DefaultItemAnimator()
animator.supportsChangeAnimations = false // prevent fading-in/out when rebinding
profilesList.itemAnimator = animator
profilesList.adapter = profilesAdapter
instance = this
ProfileManager.listener = profilesAdapter
undoManager = UndoSnackbarManager(activity as MainActivity, profilesAdapter::undo, profilesAdapter::commit)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START) {
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isProfileEditable((viewHolder as ProfileViewHolder).item.id)) {
super.getSwipeDirs(recyclerView, viewHolder)
} else 0
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isEnabled) super.getDragDirs(recyclerView, viewHolder) else 0
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val index = viewHolder.adapterPosition
profilesAdapter.remove(index)
undoManager.remove(Pair(index, (viewHolder as ProfileViewHolder).item))
}
override fun onMove(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
profilesAdapter.move(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
profilesAdapter.commitMove()
}
}).attachToRecyclerView(profilesList)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_scan_qr_code -> {
startActivity(Intent(context, ScannerActivity::class.java))
true
}
R.id.action_import_clipboard -> {
try {
val profiles = Profile.findAllUrls(
Core.clipboard.primaryClip!!.getItemAt(0).text,
Core.currentProfile?.main
).toList()
if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) }
(activity as MainActivity).snackbar().setText(R.string.action_import_msg).show()
return true
}
} catch (exc: Exception) {
Timber.d(exc)
}
(activity as MainActivity).snackbar().setText(R.string.action_import_err).show()
true
}
R.id.action_import_file -> {
startFilesForResult(importProfiles)
true
}
R.id.action_replace_file -> {
startFilesForResult(replaceProfiles)
true
}
R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile(
Profile().also { Core.currentProfile?.main?.copyFeatureSettingsTo(it) }))
true
}
R.id.action_export_clipboard -> {
val profiles = ProfileManager.getActiveProfiles()
val success = profiles != null && Core.trySetPrimaryClip(profiles.joinToString("\n"))
(activity as MainActivity).snackbar().setText(
if (success) R.string.action_export_msg else R.string.action_export_err).show()
true
}
R.id.action_export_file -> {
startFilesForResult(exportProfiles)
true
}
else -> false
}
}
private fun startFilesForResult(launcher: ActivityResultLauncher<String>) {
try {
return launcher.launch("")
} catch (_: ActivityNotFoundException) {
} catch (_: SecurityException) {
}
(activity as MainActivity).snackbar(getString(R.string.file_manager_missing)).show()
}
private fun importOrReplaceProfiles(dataUris: List<Uri>, replace: Boolean = false) {
if (dataUris.isEmpty()) return
val activity = activity as MainActivity
try {
ProfileManager.createProfilesFromJson(dataUris.asSequence().map {
activity.contentResolver.openInputStream(it)
}.filterNotNull(), replace)
} catch (e: Exception) {
activity.snackbar(e.readableMessage).show()
}
}
private val importProfiles = registerForActivityResult(OpenJson()) { importOrReplaceProfiles(it) }
private val replaceProfiles = registerForActivityResult(OpenJson()) { importOrReplaceProfiles(it, true) }
private val exportProfiles = registerForActivityResult(SaveJson()) { data ->
if (data != null) ProfileManager.serializeToJson()?.let { profiles ->
val activity = activity as MainActivity
try {
activity.contentResolver.openOutputStream(data)!!.bufferedWriter().use {
it.write(profiles.toString(2))
}
} catch (e: Exception) {
Timber.w(e)
activity.snackbar(e.readableMessage).show()
}
}
}
fun onTrafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId != 0L) { // ignore aggregate stats
statsCache.put(profileId, stats)
profilesAdapter.refreshId(profileId)
}
}
fun onTrafficPersisted(profileId: Long) {
statsCache.remove(profileId)
profilesAdapter.deepRefreshId(profileId)
}
override fun onDestroyView() {
undoManager.flush()
super.onDestroyView()
}
override fun onDestroy() {
instance = null
ProfileManager.listener = null
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
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.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.bg.BaseService
@Suppress("DEPRECATION")
@Deprecated("This shortcut is inefficient and should be superseded by TileService for API 24+.")
class QuickToggleShortcut : Activity(), ShadowsocksConnection.Callback {
private val connection = ShadowsocksConnection()
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, QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
.setIcon(IconCompat.createWithResource(this, R.drawable.ic_qu_shadowsocks_launcher))
.setShortLabel(getString(R.string.quick_toggle))
.build()))
finish()
} else {
connection.connect(this, this)
if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("toggle")
}
}
override fun onServiceConnected(service: IShadowsocksService) {
val state = BaseService.State.values()[service.state]
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
finish()
}
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) { }
override fun onDestroy() {
connection.disconnect(this)
super.onDestroy()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.Manifest
import android.content.Intent
import android.content.pm.ShortcutManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope
import androidx.work.await
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.forEachTry
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.widget.ListHolderListener
import com.google.mlkit.vision.barcode.Barcode
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import kotlinx.coroutines.*
import kotlinx.coroutines.tasks.await
import timber.log.Timber
class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient(BarcodeScannerOptions.Builder().apply {
setBarcodeFormats(Barcode.FORMAT_QR_CODE)
}.build())
private val imageAnalysis by lazy {
ImageAnalysis.Builder().apply {
setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
setBackgroundExecutor(Dispatchers.Default.asExecutor())
}.build().also { it.setAnalyzer(Dispatchers.Main.immediate.asExecutor(), this) }
}
@ExperimentalGetImage
override fun analyze(image: ImageProxy) {
val mediaImage = image.image ?: return
lifecycleScope.launchWhenCreated {
val result = try {
process { InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees) }.also {
if (it) imageAnalysis.clearAnalyzer()
}
} catch (_: CancellationException) {
return@launchWhenCreated
} catch (e: Exception) {
return@launchWhenCreated Timber.w(e)
} finally {
image.close()
}
if (result) onSupportNavigateUp()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("scan")
setContentView(R.layout.layout_scanner)
ListHolderListener.setup(this)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
lifecycle.addObserver(scanner)
requestCamera.launch(Manifest.permission.CAMERA)
}
private val requestCamera = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) lifecycleScope.launchWhenCreated {
val cameraProvider = ProcessCameraProvider.getInstance(this@ScannerActivity).await()
val selector = if (cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) {
CameraSelector.DEFAULT_BACK_CAMERA
} else CameraSelector.DEFAULT_FRONT_CAMERA
val preview = Preview.Builder().build()
preview.setSurfaceProvider(findViewById<PreviewView>(R.id.barcode).surfaceProvider)
try {
cameraProvider.bindToLifecycle(this@ScannerActivity, selector, preview, imageAnalysis)
} catch (e: IllegalArgumentException) {
Timber.d(e)
startImport()
}
} else permissionMissing()
}
private suspend inline fun process(feature: Profile? = Core.currentProfile?.main,
crossinline image: () -> InputImage): Boolean {
val barcodes = withContext(Dispatchers.Default) { scanner.process(image()).await() }
var result = false
for (profile in Profile.findAllUrls(barcodes.mapNotNull { it.rawValue }.joinToString("\n"), feature)) {
ProfileManager.createProfile(profile)
result = true
}
return result
}
private fun permissionMissing() {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
startImport()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.scanner_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_import_clipboard -> {
startImport(true)
true
}
else -> false
}
/**
* See also: https://stackoverflow.com/a/31350642/2245107
*/
override fun shouldUpRecreateTask(targetIntent: Intent?) = super.shouldUpRecreateTask(targetIntent) || isTaskRoot
private var finished = false
override fun onSupportNavigateUp(): Boolean {
if (finished) return false
finished = true
return super.onSupportNavigateUp()
}
private fun startImport(manual: Boolean = false) = (if (manual) import else importFinish).launch("image/*")
private val import = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { importOrFinish(it) }
private val importFinish = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {
importOrFinish(it, true)
}
private fun importOrFinish(dataUris: List<Uri>, finish: Boolean = false) {
if (dataUris.isNotEmpty()) GlobalScope.launch(Dispatchers.Main.immediate) {
onSupportNavigateUp()
val feature = Core.currentProfile?.main
try {
var success = false
dataUris.forEachTry { uri ->
if (process(feature) { InputImage.fromFilePath(app, uri) }) success = true
}
Toast.makeText(app, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
}
} else if (finish) onSupportNavigateUp()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
open class ToolbarFragment : Fragment() {
lateinit var toolbar: Toolbar
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar = view.findViewById(R.id.toolbar)
toolbar.setNavigationIcon(R.drawable.ic_navigation_menu)
toolbar.setNavigationOnClickListener { (activity as MainActivity).drawer.openDrawer(GravityCompat.START) }
}
open fun onBackPressed(): Boolean = false
}
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks
import android.content.res.Resources
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.ListListener
class UdpFallbackProfileActivity : AppCompatActivity() {
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private var item: Profile? = null
private val text = itemView.findViewById<CheckedTextView>(android.R.id.text1)
init {
view.setBackgroundResource(theme.resolveResourceId(android.R.attr.selectableItemBackground))
itemView.setOnClickListener(this)
}
fun bind(item: Profile?) {
this.item = item
if (item == null) text.setText(R.string.plugin_disabled) else text.text = item.formattedName
text.isChecked = udpFallback == item?.id
}
override fun onClick(v: View?) {
DataStore.udpFallback = item?.id
DataStore.dirty = true
finish()
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = (ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf())
.filter { it.id != editingId && PluginConfiguration(it.plugin ?: "").selected.isEmpty() }
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
holder.bind(if (position == 0) null else profiles[position - 1])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(Resources.getSystem()
.getIdentifier("select_dialog_singlechoice_material", "layout", "android"), parent, false))
override fun getItemCount(): Int = 1 + profiles.size
}
private var editingId = DataStore.editingId
private var udpFallback = DataStore.udpFallback
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (editingId == null) {
finish()
return
}
setContentView(R.layout.layout_udp_fallback)
ListHolderListener.setup(this)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.udp_fallback)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener { finish() }
findViewById<RecyclerView>(R.id.list).apply {
ViewCompat.setOnApplyWindowInsetsListener(this, ListListener)
itemAnimator = DefaultItemAnimator()
adapter = profilesAdapter
layoutManager = LinearLayoutManager(this@UdpFallbackProfileActivity, RecyclerView.VERTICAL, false).apply {
if (DataStore.udpFallback != null) {
scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == DataStore.udpFallback } + 1)
}
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.acl
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.AdapterView
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.Core
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout
import kotlinx.parcelize.Parcelize
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import timber.log.Timber
import java.net.IDN
import java.net.MalformedURLException
import java.net.URL
import java.util.*
import java.util.regex.PatternSyntaxException
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback {
companion object {
private const val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private const val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
private const val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
// unescaped lol: (?<=^(?:\(\^\|\\\.\)|\^\(\.\*\\\.\)\?|\(\?:\^\|\\\.\))).*(?=\$$)
private val domainPattern =
"(?<=^(?:\\(\\^\\|\\\\\\.\\)|\\^\\(\\.\\*\\\\\\.\\)\\?|\\(\\?:\\^\\|\\\\\\.\\))).*(?=\\\$\$)".toRegex()
@Suppress("FunctionName")
private fun AclItem(item: Any) = when (item) {
is String -> AclItem(item, false)
is Subnet -> AclItem(item.toString(), false)
is URL -> AclItem(item.toString(), true)
else -> throw IllegalArgumentException("item")
}
}
private enum class Template {
Generic,
Domain,
Url;
}
@Parcelize
data class AclItem(val item: String = "", val isUrl: Boolean = false) : Parcelable {
fun toAny() = if (isUrl) URL(item) else Subnet.fromString(item) ?: item
}
@Parcelize
data class AclArg(val item: AclItem? = null) : Parcelable
@Parcelize
data class AclEditResult(val edited: AclItem?, val replacing: AclItem?) : Parcelable
class AclRuleDialogFragment : AlertDialogFragment<AclArg, AclEditResult>(),
TextWatcher, AdapterView.OnItemSelectedListener {
private lateinit var templateSelector: Spinner
private lateinit var editText: EditText
private lateinit var inputLayout: TextInputLayout
private val positive by lazy { (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) }
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val activity = requireActivity()
@SuppressLint("InflateParams")
val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
templateSelector = view.findViewById(R.id.template_selector)
editText = view.findViewById(R.id.content)
inputLayout = view.findViewById(R.id.content_layout)
templateSelector.setSelection(Template.Generic.ordinal)
val arg = arg.item
editText.setText(arg?.item)
when {
arg == null -> { }
arg.isUrl -> templateSelector.setSelection(Template.Url.ordinal)
Subnet.fromString(arg.item) == null -> {
val match = domainPattern.find(arg.item)
if (match != null) {
templateSelector.setSelection(Template.Domain.ordinal)
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
}
}
}
templateSelector.onItemSelectedListener = this@AclRuleDialogFragment
editText.addTextChangedListener(this@AclRuleDialogFragment)
setTitle(R.string.edit_rule)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
if (!arg?.item.isNullOrEmpty()) setNeutralButton(R.string.delete, listener)
setView(view)
}
override fun onStart() {
super.onStart()
validate()
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(s: Editable) = validate(value = s)
override fun onNothingSelected(parent: AdapterView<*>?) = check(false)
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate(position)
private fun validate(template: Int = templateSelector.selectedItemPosition, value: Editable = editText.text) {
var message = ""
positive.isEnabled = when (Template.values()[template]) {
Template.Generic -> value.toString().run {
try {
if (Subnet.fromString(this) == null) toPattern()
true
} catch (e: PatternSyntaxException) {
message = e.readableMessage
false
}
}
Template.Domain -> try {
IDN.toASCII(value.toString(), IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
true
} catch (e: IllegalArgumentException) {
message = e.cause?.readableMessage ?: e.readableMessage
false
}
Template.Url -> try {
val url = URL(value.toString())
if ("http".equals(url.protocol, true)) message = getString(R.string.cleartext_http_warning)
true
} catch (e: MalformedURLException) {
message = e.readableMessage
false
}
}
inputLayout.error = message
}
override fun ret(which: Int) = when (which) {
DialogInterface.BUTTON_POSITIVE -> {
AclEditResult(editText.text.toString().let { text ->
when (Template.values()[templateSelector.selectedItemPosition]) {
Template.Generic -> AclItem(text)
Template.Domain -> AclItem(IDN.toASCII(text, IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
.replace(".", "\\.").let { "(?:^|\\.)$it\$" })
Template.Url -> AclItem(text, true)
}
}, arg.item)
}
DialogInterface.BUTTON_NEUTRAL -> AclEditResult(null, arg.item)
else -> null
}
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which != DialogInterface.BUTTON_NEGATIVE) super.onClick(dialog, which)
}
}
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener {
lateinit var item: Any
private val text = view.findViewById<TextView>(android.R.id.text1)
init {
view.isFocusable = true
view.setOnClickListener(this)
view.setOnLongClickListener(this)
view.setBackgroundResource(R.drawable.background_selectable)
}
fun bind(hostname: String) {
item = hostname
text.text = hostname
itemView.isSelected = selectedItems.contains(hostname)
}
fun bind(subnet: Subnet) {
item = subnet
text.text = subnet.toString()
itemView.isSelected = selectedItems.contains(subnet)
}
fun bind(url: URL) {
item = url
text.text = url.toString()
itemView.isSelected = selectedItems.contains(url)
}
override fun onClick(v: View?) {
if (selectedItems.isNotEmpty()) onLongClick(v) else AclRuleDialogFragment().apply {
arg(AclArg(AclItem(item)))
key()
}.show(parentFragmentManager, null)
}
override fun onLongClick(v: View?): Boolean {
if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
onSelectedItemsUpdated()
itemView.isSelected = !itemView.isSelected
return true
}
}
private inner class AclRulesAdapter : RecyclerView.Adapter<AclRuleViewHolder>() {
private val acl = Acl.customRules
private var savePending = false
override fun onBindViewHolder(holder: AclRuleViewHolder, i: Int) {
val j = i - acl.subnets.size()
if (j < 0) holder.bind(acl.subnets[i]) else {
val k = j - acl.proxyHostnames.size()
if (k < 0) holder.bind(acl.proxyHostnames[j]) else holder.bind(acl.urls[k])
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AclRuleViewHolder(LayoutInflater
.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false))
override fun getItemCount(): Int = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.size()
private fun apply() {
if (!savePending) {
savePending = true
list.post {
Acl.customRules = acl
savePending = false
}
}
}
fun add(item: Any): Int? = when (item) {
is Subnet -> addSubnet(item)
is String -> addHostname(item)
is URL -> addURL(item)
else -> null
}
fun addSubnet(subnet: Subnet): Int {
val old = acl.subnets.size()
val index = acl.subnets.add(subnet)
if (old != acl.subnets.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addHostname(hostname: String): Int {
val old = acl.proxyHostnames.size()
val index = acl.subnets.size() + acl.proxyHostnames.add(hostname)
if (old != acl.proxyHostnames.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addURL(url: URL): Int {
val old = acl.urls.size()
val index = acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.add(url)
if (old != acl.urls.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun addToProxy(input: String): Int? {
val acl = Acl().fromReader(input.reader(), true)
var result: Int? = null
if (acl.bypass) acl.subnets.asIterable().asSequence().map { addSubnet(it) }
.forEach { if (result == null) result = it }
(acl.proxyHostnames.asIterable().asSequence().map { addHostname(it) } +
acl.urls.asIterable().asSequence().map { addURL(it) })
.forEach { if (result == null) result = it }
return result
}
fun remove(i: Int) {
val j = i - acl.subnets.size()
if (j < 0) {
undoManager.remove(Pair(i, acl.subnets[i]))
acl.subnets.removeItemAt(i)
} else {
val k = j - acl.proxyHostnames.size()
if (k < 0) {
undoManager.remove(Pair(j, acl.proxyHostnames[j]))
acl.proxyHostnames.removeItemAt(j)
} else {
undoManager.remove(Pair(k, acl.urls[k]))
acl.urls.removeItemAt(k)
}
}
notifyItemRemoved(i)
apply()
}
fun remove(item: Any) {
when (item) {
is Subnet -> {
notifyItemRemoved(acl.subnets.indexOf(item))
acl.subnets.remove(item)
apply()
}
is String -> {
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.indexOf(item))
acl.proxyHostnames.remove(item)
apply()
}
is URL -> {
notifyItemRemoved(acl.subnets.size() + acl.proxyHostnames.size() + acl.urls.indexOf(item))
acl.urls.remove(item)
apply()
}
}
}
fun removeSelected() {
undoManager.remove(selectedItems.map { Pair(0, it) })
selectedItems.forEach { remove(it) }
selectedItems.clear()
onSelectedItemsUpdated()
}
fun undo(actions: List<Pair<Int, Any>>) {
for ((_, item) in actions) add(item)
}
fun selectAll() {
selectedItems.clear()
selectedItems.addAll(acl.subnets.asIterable())
selectedItems.addAll(acl.proxyHostnames.asIterable())
selectedItems.addAll(acl.urls.asIterable())
onSelectedItemsUpdated()
notifyDataSetChanged()
}
}
private val isEnabled get() = (activity as? MainActivity)?.state == BaseService.State.Stopped ||
Core.currentProfile?.main?.route != Acl.CUSTOM_RULES
private val selectedItems = HashSet<Any>()
private val adapter by lazy { AclRulesAdapter() }
private lateinit var list: RecyclerView
private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any>
private fun onSelectedItemsUpdated() {
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_custom_rules, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
AlertDialogFragment.setResultListener<AclRuleDialogFragment, AclEditResult>(this) { which, ret ->
val (edited, replacing) = ret ?: return@setResultListener
replacing?.toAny()?.let { item ->
adapter.remove(item)
if (which == DialogInterface.BUTTON_NEUTRAL) undoManager.remove(Pair(-1, item))
}
if (edited != null) adapter.add(edited.toAny())?.also { list.post { list.scrollToPosition(it) } }
}
if (savedInstanceState != null) {
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS)
?.mapNotNull { Subnet.fromString(it) } ?: listOf())
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_HOSTNAMES)
?: arrayOf())
selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_URLS)?.map { URL(it) }
?: listOf())
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
toolbar.inflateMenu(R.menu.custom_rules_menu)
toolbar.setOnMenuItemClickListener(this)
val activity = activity as MainActivity
list = view.findViewById(R.id.list)
ViewCompat.setOnApplyWindowInsetsListener(list, MainListListener)
list.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = adapter
FastScrollerBuilder(list).useMd2Style().build()
undoManager = UndoSnackbarManager(activity, adapter::undo)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START) {
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int =
if (isEnabled && selectedItems.isEmpty()) super.getSwipeDirs(recyclerView, viewHolder) else 0
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) =
adapter.remove(viewHolder.adapterPosition)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean = false
}).attachToRecyclerView(list)
}
override fun onBackPressed(): Boolean {
val mode = mode
return if (mode != null) {
mode.finish()
true
} else super.onBackPressed()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filterIsInstance<Subnet>().map(Subnet::toString)
.toTypedArray())
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filterIsInstance<String>().toTypedArray())
outState.putStringArray(SELECTED_URLS, selectedItems.filterIsInstance<URL>().map(URL::toString).toTypedArray())
}
private fun copySelected() {
val acl = Acl()
acl.bypass = true
selectedItems.forEach {
when (it) {
is Subnet -> acl.subnets.add(it)
is String -> acl.proxyHostnames.add(it)
is URL -> acl.urls.add(it)
}
}
val success = Core.trySetPrimaryClip(acl.toString())
(activity as MainActivity).snackbar().setText(
if (success) R.string.action_export_msg else R.string.action_export_err).show()
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_manual_settings -> {
AclRuleDialogFragment().apply {
arg(AclArg(AclItem()))
key()
}.show(parentFragmentManager, null)
true
}
R.id.action_import_clipboard -> {
try {
check(adapter.addToProxy(Core.clipboard.primaryClip!!.getItemAt(0).text.toString()) != null)
} catch (exc: Exception) {
(activity as MainActivity).snackbar().setText(R.string.action_import_err).show()
Timber.d(exc)
}
true
}
R.id.action_import_gfwlist -> {
val acl = Acl().fromId(Acl.GFWLIST)
if (acl.bypass) acl.subnets.asIterable().forEach { adapter.addSubnet(it) }
acl.proxyHostnames.asIterable().forEach { adapter.addHostname(it) }
acl.urls.asIterable().forEach { adapter.addURL(it) }
true
}
else -> false
}
override fun onDetach() {
undoManager.flush()
mode?.finish()
super.onDetach()
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val activity = requireActivity()
activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.black)
activity.menuInflater.inflate(R.menu.custom_rules_selection, menu)
toolbar.touchscreenBlocksFocus = true
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) {
R.id.action_select_all -> {
adapter.selectAll()
true
}
R.id.action_cut -> {
copySelected()
adapter.removeSelected()
true
}
R.id.action_copy -> {
copySelected()
true
}
R.id.action_delete -> {
adapter.removeSelected()
true
}
else -> false
}
override fun onDestroyActionMode(mode: ActionMode) {
val activity = requireActivity()
activity.window.statusBarColor = ContextCompat.getColor(activity,
activity.theme.resolveResourceId(android.R.attr.statusBarColor))
toolbar.touchscreenBlocksFocus = false
selectedItems.clear()
onSelectedItemsUpdated()
adapter.notifyDataSetChanged()
this.mode = null
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.bg
import android.app.KeyguardManager
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import com.github.shadowsocks.Core
import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.preference.DataStore
import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(24)
class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) }
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
private val keyguard by lazy { getSystemService<KeyguardManager>()!! }
private var tapPending = false
private val connection = ShadowsocksConnection()
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
updateTile(state) { profileName }
override fun onServiceConnected(service: IShadowsocksService) {
updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) {
tapPending = false
toggle()
}
}
override fun onStartListening() {
super.onStartListening()
connection.connect(this, this)
}
override fun onStopListening() {
connection.disconnect(this)
super.onStopListening()
}
override fun onClick() {
if (isLocked && !DataStore.canToggleLocked) unlockAndRun(this::toggle) else toggle()
}
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
qsTile?.apply {
label = null
when (serviceState) {
BaseService.State.Idle -> error("serviceState")
BaseService.State.Connecting -> {
icon = iconBusy
state = Tile.STATE_ACTIVE
}
BaseService.State.Connected -> {
icon = iconConnected
if (!keyguard.isDeviceLocked) label = profileName()
state = Tile.STATE_ACTIVE
}
BaseService.State.Stopping -> {
icon = iconBusy
state = Tile.STATE_UNAVAILABLE
}
BaseService.State.Stopped -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
}
}
label = label ?: getString(R.string.app_name)
updateTile()
}
}
private fun toggle() {
val service = connection.service
if (service == null) tapPending = true else BaseService.State.values()[service.state].let { state ->
when {
state.canStop -> Core.stopService()
state == BaseService.State.Stopped -> Core.startService()
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.PreferenceDialogFragmentCompat
import com.github.shadowsocks.ProfileConfigActivity
import com.github.shadowsocks.plugin.PluginContract
import com.github.shadowsocks.plugin.PluginManager
class PluginConfigurationDialogFragment : EditTextPreferenceDialogFragmentCompat() {
companion object {
private const val PLUGIN_ID_FRAGMENT_TAG =
"com.github.shadowsocks.preference.PluginConfigurationDialogFragment.PLUGIN_ID"
}
fun setArg(key: String, plugin: String) {
arguments = bundleOf(PreferenceDialogFragmentCompat.ARG_KEY to key, PLUGIN_ID_FRAGMENT_TAG to plugin)
}
private lateinit var editText: EditText
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
val intent = PluginManager.buildIntent(arguments?.getString(PLUGIN_ID_FRAGMENT_TAG)!!,
PluginContract.ACTION_HELP)
val activity = activity as ProfileConfigActivity
if (intent.resolveActivity(activity.packageManager) != null) builder.setNeutralButton("?") { _, _ ->
activity.pluginHelp.launch(intent.putExtra(PluginContract.EXTRA_OPTIONS, editText.text.toString()))
}
}
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
editText = view.findViewById(android.R.id.edit)
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.preference.ListPreference
import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.PluginList
import com.github.shadowsocks.plugin.PluginManager
class PluginPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
companion object FallbackProvider : SummaryProvider<PluginPreference> {
override fun provideSummary(preference: PluginPreference) =
preference.selectedEntry?.label ?: preference.unknownValueSummary.format(preference.value)
}
lateinit var plugins: PluginList
val selectedEntry get() = plugins.lookup[value]
private val entryIcon: Drawable? get() = selectedEntry?.icon
private val unknownValueSummary = context.getString(R.string.plugin_unknown)
private var listener: OnPreferenceChangeListener? = null
override fun getOnPreferenceChangeListener(): OnPreferenceChangeListener? = listener
override fun setOnPreferenceChangeListener(listener: OnPreferenceChangeListener?) {
this.listener = listener
}
init {
super.setOnPreferenceChangeListener { preference, newValue ->
val listener = listener
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
value = newValue.toString()
icon = entryIcon
true
} else false
}
}
fun init() {
plugins = PluginManager.fetchPlugins()
entryValues = plugins.lookup.map { it.key }.toTypedArray()
icon = entryIcon
summaryProvider = FallbackProvider
}
override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue)
init()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.preference
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.TooltipCompat
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.setFragmentResult
import androidx.preference.PreferenceDialogFragmentCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.R
import com.github.shadowsocks.plugin.Plugin
import com.google.android.material.bottomsheet.BottomSheetDialog
class PluginPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
companion object {
const val KEY_SELECTED_ID = "id"
}
private inner class IconListViewHolder(val dialog: BottomSheetDialog, view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener, View.OnLongClickListener {
private lateinit var plugin: Plugin
private val text1 = view.findViewById<TextView>(android.R.id.text1)
private val text2 = view.findViewById<TextView>(android.R.id.text2)
private val icon = view.findViewById<ImageView>(android.R.id.icon)
private val unlock = view.findViewById<View>(R.id.unlock).apply {
TooltipCompat.setTooltipText(this, getText(R.string.plugin_auto_connect_unlock_only))
}
init {
view.setOnClickListener(this)
view.setOnLongClickListener(this)
}
fun bind(plugin: Plugin, selected: Boolean = false) {
this.plugin = plugin
val label = plugin.label
text1.text = label
text2.text = plugin.id
val typeface = if (selected) Typeface.BOLD else Typeface.NORMAL
text1.setTypeface(null, typeface)
text2.setTypeface(null, typeface)
text2.isVisible = plugin.id.isNotEmpty() && label != plugin.id
icon.setImageDrawable(plugin.icon)
unlock.isGone = plugin.directBootAware || !DataStore.persistAcrossReboot
}
override fun onClick(v: View?) {
clicked = plugin
dialog.dismiss()
}
override fun onLongClick(v: View?) = try {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.Builder()
.scheme("package")
.opaquePart(plugin.packageName)
.build()))
true
} catch (_: ActivityNotFoundException) {
false
}
}
private inner class IconListAdapter(private val dialog: BottomSheetDialog) :
RecyclerView.Adapter<IconListViewHolder>() {
override fun getItemCount(): Int = preference.plugins.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = IconListViewHolder(dialog,
LayoutInflater.from(parent.context).inflate(R.layout.icon_list_item_2, parent, false))
override fun onBindViewHolder(holder: IconListViewHolder, position: Int) {
if (selected < 0) holder.bind(preference.plugins[position]) else when (position) {
0 -> holder.bind(preference.selectedEntry!!, true)
in selected + 1..Int.MAX_VALUE -> holder.bind(preference.plugins[position])
else -> holder.bind(preference.plugins[position - 1])
}
}
}
fun setArg(key: String) {
arguments = bundleOf(ARG_KEY to key)
}
private val preference by lazy { getPreference() as PluginPreference }
private val selected by lazy { preference.plugins.indexOf(preference.selectedEntry) }
private var clicked: Plugin? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity()
val dialog = BottomSheetDialog(activity, theme)
val recycler = RecyclerView(activity)
val padding = resources.getDimensionPixelOffset(R.dimen.bottom_sheet_padding)
recycler.setPadding(0, padding, 0, padding)
recycler.setHasFixedSize(true)
recycler.layoutManager = LinearLayoutManager(activity)
recycler.itemAnimator = DefaultItemAnimator()
recycler.adapter = IconListAdapter(dialog)
recycler.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.setContentView(recycler)
return dialog
}
override fun onDialogClosed(positiveResult: Boolean) {
val clicked = clicked
if (clicked != null && clicked != preference.selectedEntry) {
setFragmentResult(javaClass.name, bundleOf(KEY_SELECTED_ID to clicked.id))
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2020 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2020 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.subscription
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.*
import android.widget.AdapterView
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.ToolbarFragment
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.github.shadowsocks.utils.readableMessage
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.MainListListener
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.material.textfield.TextInputLayout
import kotlinx.parcelize.Parcelize
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.net.MalformedURLException
import java.net.URL
class SubscriptionFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
@Parcelize
data class SubItem(val item: String? = null) : Parcelable
@Parcelize
data class SubEditResult(val edited: String?, val replacing: String?) : Parcelable
class SubDialogFragment : AlertDialogFragment<SubItem, SubEditResult>(),
TextWatcher, AdapterView.OnItemSelectedListener {
private lateinit var editText: EditText
private lateinit var inputLayout: TextInputLayout
private val positive by lazy { (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) }
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
val activity = requireActivity()
@SuppressLint("InflateParams")
val view = activity.layoutInflater.inflate(R.layout.dialog_subscription, null)
editText = view.findViewById(R.id.content)
inputLayout = view.findViewById(R.id.content_layout)
editText.setText(arg.item)
editText.addTextChangedListener(this@SubDialogFragment)
setTitle(R.string.edit_subscription)
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
if (!arg.item.isNullOrEmpty()) setNeutralButton(R.string.delete, listener)
setView(view)
}
override fun onStart() {
super.onStart()
validate()
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) = validate(value = s)
override fun onNothingSelected(parent: AdapterView<*>?) = check(false)
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate()
private fun validate(value: Editable = editText.text) {
var message = ""
positive.isEnabled = try {
val url = URL(value.toString())
if ("http".equals(url.protocol, true)) message = getString(R.string.cleartext_http_warning)
true
} catch (e: MalformedURLException) {
message = e.readableMessage
false
}
inputLayout.error = message
}
override fun ret(which: Int) = when (which) {
DialogInterface.BUTTON_POSITIVE -> SubEditResult(editText.text.toString(), arg.item)
DialogInterface.BUTTON_NEUTRAL -> SubEditResult(null, arg.item)
else -> null
}
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which != DialogInterface.BUTTON_NEGATIVE) super.onClick(dialog, which)
}
}
private inner class SubViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener {
lateinit var item: URL
private val text = view.findViewById<TextView>(android.R.id.text1)
init {
view.isFocusable = true
view.setOnClickListener(this)
view.setBackgroundResource(R.drawable.background_selectable)
}
fun bind(url: URL) {
item = url
text.text = url.toString()
}
override fun onClick(v: View?) {
SubDialogFragment().apply {
arg(SubItem(item.toString()))
key()
}.show(parentFragmentManager, null)
}
}
private inner class SubscriptionAdapter : RecyclerView.Adapter<SubViewHolder>() {
private val subscription = Subscription.instance
private var savePending = false
override fun onBindViewHolder(holder: SubViewHolder, i: Int) {
holder.bind(subscription.urls[i])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = SubViewHolder(LayoutInflater
.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false))
override fun getItemCount(): Int = subscription.urls.size()
private fun apply() {
if (!savePending) {
savePending = true
list.post {
Subscription.instance = subscription
savePending = false
}
}
}
fun add(url: URL): Int {
val old = subscription.urls.size()
val index = subscription.urls.add(url)
if (old != subscription.urls.size()) {
notifyItemInserted(index)
apply()
}
return index
}
fun remove(i: Int) {
undoManager.remove(Pair(i, subscription.urls[i]))
subscription.urls.removeItemAt(i)
notifyItemRemoved(i)
apply()
}
fun remove(item: URL) {
notifyItemRemoved(subscription.urls.indexOf(item))
subscription.urls.remove(item)
apply()
}
fun undo(actions: List<Pair<Int, Any>>) {
for ((_, item) in actions)
when (item) {
is URL -> {
add(item)
}
}
}
}
private val adapter by lazy { SubscriptionAdapter() }
private lateinit var list: RecyclerView
private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<URL>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_subscriptions, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, ListHolderListener)
AlertDialogFragment.setResultListener<SubDialogFragment, SubEditResult>(this) { which, ret ->
val (edited, replacing) = ret ?: return@setResultListener
replacing?.let { item ->
val url = URL(item)
adapter.remove(url)
if (which == DialogInterface.BUTTON_NEUTRAL) undoManager.remove(-1 to url)
}
if (edited != null) adapter.add(URL(edited)).also { list.post { list.scrollToPosition(it) } }
}
toolbar.setTitle(R.string.subscriptions)
toolbar.inflateMenu(R.menu.subscription_menu)
toolbar.setOnMenuItemClickListener(this)
SubscriptionService.idle.observe(viewLifecycleOwner) {
toolbar.menu.findItem(R.id.action_update_subscription).isEnabled = it
}
val activity = activity as MainActivity
list = view.findViewById(R.id.list)
ViewCompat.setOnApplyWindowInsetsListener(list, MainListListener)
list.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = adapter
FastScrollerBuilder(list).useMd2Style().build()
undoManager = UndoSnackbarManager(activity, adapter::undo)
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.START) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) =
adapter.remove(viewHolder.adapterPosition)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean = false
}).attachToRecyclerView(list)
}
override fun onBackPressed(): Boolean {
val mode = mode
return if (mode != null) {
mode.finish()
true
} else super.onBackPressed()
}
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_manual_settings -> {
SubDialogFragment().apply {
arg(SubItem())
key()
}.show(parentFragmentManager, null)
true
}
R.id.action_update_subscription -> {
val context = requireContext()
context.startService(Intent(context, SubscriptionService::class.java))
true
}
else -> false
}
override fun onDetach() {
undoManager.flush()
mode?.finish()
super.onDetach()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.ProfileManager
class ActionListener : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val settings = Settings.fromIntent(intent)
var changed = false
if (ProfileManager.getProfile(settings.profileId) != null) {
Core.switchProfile(settings.profileId)
changed = true
}
if (settings.switchOn) {
Core.startService()
if (changed) Core.reloadService()
} else Core.stopService()
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.app.Activity
import android.content.res.Resources
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView
import android.widget.Switch
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.shadowsocks.R
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.widget.ListHolderListener
import com.github.shadowsocks.widget.ListListener
class ConfigActivity : AppCompatActivity() {
inner class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private var item: Profile? = null
private val text = itemView.findViewById<CheckedTextView>(android.R.id.text1)
init {
view.setBackgroundResource(theme.resolveResourceId(android.R.attr.selectableItemBackground))
itemView.setOnClickListener(this)
}
fun bindDefault() {
item = null
text.setText(R.string.profile_default)
text.isChecked = taskerOption.profileId < 0
}
fun bind(item: Profile) {
this.item = item
text.text = item.formattedName
text.isChecked = taskerOption.profileId == item.id
}
override fun onClick(v: View?) {
taskerOption.switchOn = switch.isChecked
val item = item
taskerOption.profileId = item?.id ?: -1
setResult(Activity.RESULT_OK, taskerOption.toIntent(this@ConfigActivity))
finish()
}
}
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
internal val profiles = ProfileManager.getActiveProfiles()?.toMutableList() ?: mutableListOf()
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(Resources.getSystem()
.getIdentifier("select_dialog_singlechoice_material", "layout", "android"), parent, false))
override fun getItemCount(): Int = 1 + profiles.size
}
private lateinit var taskerOption: Settings
private lateinit var switch: Switch
private val profilesAdapter = ProfilesAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
if (intent == null) {
finish()
return
}
taskerOption = Settings.fromIntent(intent)
setContentView(R.layout.layout_tasker)
ListHolderListener.setup(this)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.app_name)
toolbar.setNavigationIcon(R.drawable.ic_navigation_close)
toolbar.setNavigationOnClickListener { finish() }
switch = findViewById(R.id.serviceSwitch)
switch.isChecked = taskerOption.switchOn
findViewById<RecyclerView>(R.id.list).apply {
ViewCompat.setOnApplyWindowInsetsListener(this, ListListener)
itemAnimator = DefaultItemAnimator()
adapter = profilesAdapter
layoutManager = LinearLayoutManager(this@ConfigActivity, RecyclerView.VERTICAL, false).apply {
if (taskerOption.profileId >= 0) {
scrollToPosition(profilesAdapter.profiles.indexOfFirst { it.id == taskerOption.profileId } + 1)
}
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.tasker
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import com.github.shadowsocks.R
import com.github.shadowsocks.database.ProfileManager
import com.twofortyfouram.locale.api.Intent as ApiIntent
class Settings(bundle: Bundle?) {
companion object {
private const val KEY_SWITCH_ON = "switch_on"
private const val KEY_PROFILE_ID = "profile_id"
fun fromIntent(intent: Intent) = Settings(intent.getBundleExtra(ApiIntent.EXTRA_BUNDLE))
}
var switchOn: Boolean = bundle?.getBoolean(KEY_SWITCH_ON, true) ?: true
var profileId: Long
init {
profileId = bundle?.getLong(KEY_PROFILE_ID, -1L) ?: -1L
if (profileId < 0) profileId = (bundle?.getInt(KEY_PROFILE_ID, -1) ?: -1).toLong()
}
fun toIntent(context: Context): Intent {
val profile = ProfileManager.getProfile(profileId)
return Intent()
.putExtra(ApiIntent.EXTRA_BUNDLE, bundleOf(Pair(KEY_SWITCH_ON, switchOn),
Pair(KEY_PROFILE_ID, profileId)))
.putExtra(ApiIntent.EXTRA_STRING_BLURB,
if (profile != null) context.getString(
if (switchOn) R.string.start_service else R.string.stop_service, profile.formattedName)
else context.getString(if (switchOn) R.string.start_service_default else R.string.stop))
}
}
/*******************************************************************************
* *
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.progressindicator.CircularProgressIndicator
class FabProgressBehavior(context: Context, attrs: AttributeSet?) :
CoordinatorLayout.Behavior<CircularProgressIndicator>(context, attrs) {
override fun layoutDependsOn(parent: CoordinatorLayout, child: CircularProgressIndicator, dependency: View) =
dependency.id == (child.layoutParams as CoordinatorLayout.LayoutParams).anchorId
override fun onLayoutChild(parent: CoordinatorLayout, child: CircularProgressIndicator,
layoutDirection: Int): Boolean {
val size = parent.getDependencies(child).single().measuredHeight + child.trackThickness
return if (child.indicatorSize != size) {
child.indicatorSize = size
true
} else false
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.PointerIcon
import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.TooltipCompat
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.shadowsocks.R
import com.github.shadowsocks.bg.BaseService
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.progressindicator.DeterminateDrawable
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import java.util.*
class ServiceButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FloatingActionButton(context, attrs, defStyleAttr), DynamicAnimation.OnAnimationEndListener {
companion object {
private val springAnimator by lazy {
DeterminateDrawable::class.java.getDeclaredField("springAnimator").apply { isAccessible = true }
}
}
private val callback = object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable) {
super.onAnimationEnd(drawable)
var next = animationQueue.peek() ?: return
if (next.icon.current == drawable) {
animationQueue.pop()
next = animationQueue.peek() ?: return
}
next.start()
}
}
private inner class AnimatedState(@DrawableRes resId: Int,
private val onStart: BaseProgressIndicator<*>.() -> Unit = { hideProgress() }) {
val icon: AnimatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(context, resId)!!.apply {
registerAnimationCallback(this@ServiceButton.callback)
}
fun start() {
setImageDrawable(icon)
icon.start()
progress.onStart()
}
fun stop() = icon.stop()
}
private val iconStopped by lazy { AnimatedState(R.drawable.ic_service_stopped) }
private val iconConnecting by lazy {
AnimatedState(R.drawable.ic_service_connecting) {
hideProgress()
delayedAnimation = (context as LifecycleOwner).lifecycleScope.launchWhenStarted {
delay(context.resources.getInteger(android.R.integer.config_mediumAnimTime) + 1000L)
isIndeterminate = true
show()
}
}
}
private val iconConnected by lazy {
AnimatedState(R.drawable.ic_service_connected) {
delayedAnimation?.cancel()
setProgressCompat(1, true)
}
}
private val iconStopping by lazy { AnimatedState(R.drawable.ic_service_stopping) }
private val animationQueue = ArrayDeque<AnimatedState>()
private var checked = false
private var delayedAnimation: Job? = null
private lateinit var progress: BaseProgressIndicator<*>
fun initProgress(progress: BaseProgressIndicator<*>) {
this.progress = progress
(springAnimator.get(progress.progressDrawable) as DynamicAnimation<*>).addEndListener(this)
}
override fun onAnimationEnd(animation: DynamicAnimation<out DynamicAnimation<*>>?, canceled: Boolean, value: Float,
velocity: Float) {
if (!canceled) progress.hide()
}
private fun hideProgress() {
delayedAnimation?.cancel()
progress.hide()
}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
if (checked) View.mergeDrawableStates(drawableState, intArrayOf(android.R.attr.state_checked))
return drawableState
}
fun changeState(state: BaseService.State, previousState: BaseService.State, animate: Boolean) {
when (state) {
BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, animate)
BaseService.State.Stopping -> {
changeState(iconStopping, animate && previousState == BaseService.State.Connected)
}
else -> changeState(iconStopped, animate)
}
checked = state == BaseService.State.Connected
refreshDrawableState()
val description = context.getText(if (state.canStop) R.string.stop else R.string.connect)
contentDescription = description
TooltipCompat.setTooltipText(this, description)
val enabled = state.canStop || state == BaseService.State.Stopped
isEnabled = enabled
if (Build.VERSION.SDK_INT >= 24) pointerIcon = PointerIcon.getSystemIcon(context,
if (enabled) PointerIcon.TYPE_HAND else PointerIcon.TYPE_WAIT)
}
private fun changeState(icon: AnimatedState, animate: Boolean) {
fun counters(a: AnimatedState, b: AnimatedState): Boolean =
a == iconStopped && b == iconConnecting ||
a == iconConnecting && b == iconStopped ||
a == iconConnected && b == iconStopping ||
a == iconStopping && b == iconConnected
if (animate) {
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
animationQueue.add(icon)
if (animationQueue.size == 1) icon.start()
} else animationQueue.removeLast()
} else {
animationQueue.peekFirst()?.stop()
animationQueue.clear()
icon.start() // force ensureAnimatorSet to be called so that stop() will work
icon.stop()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.text.format.Formatter
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.widget.TooltipCompat
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.net.HttpsTest
import com.google.android.material.bottomappbar.BottomAppBar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.bottomAppBarStyle) :
BottomAppBar(context, attrs, defStyleAttr) {
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView
private val tester by (context as MainActivity).viewModels<HttpsTest>()
private lateinit var behavior: Behavior
override fun getBehavior(): Behavior {
if (!this::behavior.isInitialized) behavior = object : Behavior() {
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
type: Int, consumed: IntArray) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed + dyUnconsumed,
dxUnconsumed, 0, type, consumed)
}
}
return behavior
}
override fun setOnClickListener(l: OnClickListener?) {
statusText = findViewById(R.id.status)
txText = findViewById(R.id.tx)
txRateText = findViewById(R.id.txRate)
rxText = findViewById(R.id.rx)
rxRateText = findViewById(R.id.rxRate)
super.setOnClickListener(l)
}
private fun setStatus(text: CharSequence) {
statusText.text = text
TooltipCompat.setTooltipText(this, text)
}
fun changeState(state: BaseService.State) {
val activity = context as MainActivity
fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {
activity.whenStarted { what() }
}
if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
postWhenStarted { performShow() }
tester.status.observe(activity) { it.retrieve(this::setStatus) { msg -> activity.snackbar(msg).show() } }
} else {
postWhenStarted { performHide() }
updateTraffic(0, 0, 0, 0)
tester.status.removeObservers(activity)
if (state != BaseService.State.Idle) tester.invalidate()
setStatus(context.getText(when (state) {
BaseService.State.Connecting -> R.string.connecting
BaseService.State.Stopping -> R.string.stopping
else -> R.string.not_connected
}))
}
}
fun updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = "▲ ${Formatter.formatFileSize(context, txTotal)}"
rxText.text = "▼ ${Formatter.formatFileSize(context, rxTotal)}"
txRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, txRate))
rxRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, rxRate))
}
fun testConnection() = tester.testConnection()
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.google.android.material.snackbar.Snackbar
/**
* @param activity MainActivity.
* //@param view The view to find a parent from.
* @param undo Callback for undoing removals.
* @param commit Callback for committing removals.
* @tparam T Item type.
*/
class UndoSnackbarManager<in T>(private val activity: MainActivity, private val undo: (List<Pair<Int, T>>) -> Unit,
commit: ((List<Pair<Int, T>>) -> Unit)? = null) {
private val recycleBin = ArrayList<Pair<Int, T>>()
private val removedCallback = object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
if (last === transientBottomBar && event != DISMISS_EVENT_ACTION) {
commit?.invoke(recycleBin)
recycleBin.clear()
last = null
}
}
}
private var last: Snackbar? = null
fun remove(items: Collection<Pair<Int, T>>) {
recycleBin.addAll(items)
val count = recycleBin.size
activity.snackbar(activity.resources.getQuantityString(R.plurals.removed, count, count)).apply {
addCallback(removedCallback)
setAction(R.string.undo) {
undo(recycleBin.reversed())
recycleBin.clear()
}
last = this
show()
}
}
fun remove(vararg items: Pair<Int, T>) = remove(items.toList())
fun flush() = last?.dismiss()
}
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.Insets
import androidx.core.view.*
import com.github.shadowsocks.R
object ListHolderListener : OnApplyWindowInsetsListener {
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
view.setPadding(statusBarInsets.left, statusBarInsets.top, statusBarInsets.right, statusBarInsets.bottom)
return WindowInsetsCompat.Builder(insets).apply {
setInsets(WindowInsetsCompat.Type.statusBars(), Insets.NONE)
}.build()
}
fun setup(activity: AppCompatActivity) = activity.findViewById<View>(android.R.id.content).let {
ViewCompat.setOnApplyWindowInsetsListener(it, ListHolderListener)
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
}
}
object MainListListener : OnApplyWindowInsetsListener {
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply {
view.updatePadding(bottom = view.resources.getDimensionPixelOffset(R.dimen.main_list_padding_bottom) +
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom)
}
}
object ListListener : OnApplyWindowInsetsListener {
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat) = insets.apply {
view.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom)
}
}
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/material_green_700" android:state_checked="true"/>
<item android:color="@color/material_primary_500"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/color_primary_text" android:state_checked="true"/>
<item android:color="?android:attr/textColorPrimary"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/material_grey_100"/>
<foreground>
<!-- 44dp icon scaled to 52dp in 72dp, padding = (1-52/44*24/72)/2 -->
<inset
android:drawable="@drawable/ic_image_camera_alt"
android:inset="30.303%"/>
</foreground>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/material_grey_100"/>
<foreground>
<!-- 44dp icon scaled to 52dp in 72dp, padding = (1-52/44*24/72)/2 -->
<inset
android:drawable="@drawable/ic_qu_shadowsocks_foreground"
android:inset="30.303%"/>
</foreground>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/background_stat" />
</shape>
</item>
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/material_green_700" />
</shape>
</item>
</selector>
</item>
<!-- android:start not available until API 23 -->
<item android:left="8dp">
<shape android:shape="rectangle">
<solid android:color="?android:colorBackground" />
</shape>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_selected="true">
<color android:color="@color/background_selected"/>
</item>
</selector>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10.08,10.86c0.05,-0.33 0.16,-0.62 0.3,-0.87s0.34,-0.46 0.59,-0.62c0.24,-0.15 0.54,-0.22 0.91,-0.23 0.23,0.01 0.44,0.05 0.63,0.13 0.2,0.09 0.38,0.21 0.52,0.36s0.25,0.33 0.34,0.53 0.13,0.42 0.14,0.64h1.79c-0.02,-0.47 -0.11,-0.9 -0.28,-1.29s-0.4,-0.73 -0.7,-1.01 -0.66,-0.5 -1.08,-0.66 -0.88,-0.23 -1.39,-0.23c-0.65,0 -1.22,0.11 -1.7,0.34s-0.88,0.53 -1.2,0.92 -0.56,0.84 -0.71,1.36S8,11.29 8,11.87v0.27c0,0.58 0.08,1.12 0.23,1.64s0.39,0.97 0.71,1.35 0.72,0.69 1.2,0.91 1.05,0.34 1.7,0.34c0.47,0 0.91,-0.08 1.32,-0.23s0.77,-0.36 1.08,-0.63 0.56,-0.58 0.74,-0.94 0.29,-0.74 0.3,-1.15h-1.79c-0.01,0.21 -0.06,0.4 -0.15,0.58s-0.21,0.33 -0.36,0.46 -0.32,0.23 -0.52,0.3c-0.19,0.07 -0.39,0.09 -0.6,0.1 -0.36,-0.01 -0.66,-0.08 -0.89,-0.23 -0.25,-0.16 -0.45,-0.37 -0.59,-0.62s-0.25,-0.55 -0.3,-0.88 -0.08,-0.67 -0.08,-1v-0.27c0,-0.35 0.03,-0.68 0.08,-1.01zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M20,13H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-6c0,-0.55 -0.45,-1 -1,-1zM7,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM20,3H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1V4c0,-0.55 -0.45,-1 -1,-1zM7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,16h-3v3h-2v-3L8,16v-2h3v-3h2v3h3v2zM13,9L13,3.5L18.5,9L13,9z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations under
the License.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/material_grey_100" />
<size android:width="44dp" android:height="44dp" />
</shape>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M20.1,7.7l-1,1c1.8,1.8 1.8,4.6 0,6.5l1,1c2.5,-2.3 2.5,-6.1 0,-8.5zM18,9.8l-1,1c0.5,0.7 0.5,1.6 0,2.3l1,1c1.2,-1.2 1.2,-3 0,-4.3zM14,1L4,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L16,3c0,-1.1 -0.9,-2 -2,-2zM14,20L4,20L4,4h10v16z"/>
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M13,2.05v3.03c3.39,0.49 6,3.39 6,6.92 0,0.9 -0.18,1.75 -0.48,2.54l2.6,1.53c0.56,-1.24 0.88,-2.62 0.88,-4.07 0,-5.18 -3.95,-9.45 -9,-9.95zM12,19c-3.87,0 -7,-3.13 -7,-7 0,-3.53 2.61,-6.43 6,-6.92V2.05c-5.06,0.5 -9,4.76 -9,9.95 0,5.52 4.47,10 9.99,10 3.31,0 6.24,-1.61 8.06,-4.09l-2.6,-1.53C16.17,17.98 14.21,19 12,19z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M7,5h10v2h2L19,3c0,-1.1 -0.9,-1.99 -2,-1.99L7,1c-1.1,0 -2,0.9 -2,2v4h2L7,5zM15.41,16.59L20,12l-4.59,-4.59L14,8.83 17.17,12 14,15.17l1.41,1.42zM10,15.17L6.83,12 10,8.83 8.59,7.41 4,12l4.59,4.59L10,15.17zM17,19L7,19v-2L5,17v4c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-4h-2v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h0.71C7.37,7.69 9.48,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3s-1.34,3 -3,3z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M20.2,5.9l0.8,-0.8C19.6,3.7 17.8,3 16,3s-3.6,0.7 -5,2.1l0.8,0.8C13,4.8 14.5,4.2 16,4.2s3,0.6 4.2,1.7zM19.3,6.7c-0.9,-0.9 -2.1,-1.4 -3.3,-1.4s-2.4,0.5 -3.3,1.4l0.8,0.8c0.7,-0.7 1.6,-1 2.5,-1 0.9,0 1.8,0.3 2.5,1l0.8,-0.8zM19,13h-2L17,9h-2v4L5,13c-1.1,0 -2,0.9 -2,2v4c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-4c0,-1.1 -0.9,-2 -2,-2zM8,18L6,18v-2h2v2zM11.5,18h-2v-2h2v2zM15,18h-2v-2h2v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/material_primary_600"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path
android:fillColor="@color/material_primary_600"
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M11,15h2v-2h-2v2zM19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM15,9h-4v2h2c1.1,0 2,0.89 2,2v2c0,1.11 -0.9,2 -2,2h-2c-1.1,0 -2,-0.89 -2,-2L9,9c0,-1.11 0.9,-2 2,-2h4v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M12,7C6.48,7 2,9.24 2,12c0,2.24 2.94,4.13 7,4.77V20l4,-4 -4,-4v2.73c-3.15,-0.56 -5,-1.9 -5,-2.73 0,-1.06 3.04,-3 8,-3s8,1.94 8,3c0,0.73 -1.46,1.89 -4,2.53v2.05c3.53,-0.77 6,-2.53 6,-4.58 0,-2.76 -4.48,-5 -10,-5z"/>
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39 -1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39 1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1 1,-1h5V7.5l3.5,3.5 -3.5,3.5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M20,21c-1.39,0 -2.78,-0.47 -4,-1.32 -2.44,1.71 -5.56,1.71 -8,0C6.78,20.53 5.39,21 4,21H2v2h2c1.38,0 2.74,-0.35 4,-0.99 2.52,1.29 5.48,1.29 8,0 1.26,0.65 2.62,0.99 4,0.99h2v-2h-2zM3.95,19H4c1.6,0 3.02,-0.88 4,-2 0.98,1.12 2.4,2 4,2s3.02,-0.88 4,-2c0.98,1.12 2.4,2 4,2h0.05l1.89,-6.68c0.08,-0.26 0.06,-0.54 -0.06,-0.78s-0.34,-0.42 -0.6,-0.5L20,10.62V6c0,-1.1 -0.9,-2 -2,-2h-3V1H9v3H6c-1.1,0 -2,0.9 -2,2v4.62l-1.29,0.42c-0.26,0.08 -0.48,0.26 -0.6,0.5s-0.15,0.52 -0.06,0.78L3.95,19zM6,6h12v3.97L12,8 6,9.97V6z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2H8.9V6zM16,16h-3v3h-2v-3H8v-2h3v-3h2v3h3v2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_app_shortcut_background"
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp" />
<item
android:drawable="@drawable/ic_image_camera_alt"
android:left="12dp"
android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</layer-list>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="@color/material_blue_grey_600"
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_app_shortcut_background"
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp" />
<item
android:drawable="@drawable/ic_qu_shadowsocks_foreground"
android:left="12dp"
android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</layer-list>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#fff"
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_service_busy">
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
android:valueTo="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_service_idle">
<target android:name="strike_thru_path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
android:valueTo="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="strike_thru_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="strike_thru_path"
android:pathData="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
android:fillColor="#fff"
android:strokeWidth="1"/>
<clip-path
android:name="strike_thru_mask"
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"/>
<path
android:name="holey_icon"
android:pathData="M17.68,9l-1.59,7L12.7,14.89l5-5.93M10,10.08l-3.57,3L5,12.55l5-2.47M21.25,2.28L0,12.8l6.83,2.57,9.76-8.21L9.26,15.89l8.29,2.67,3.7-16.27h0ZM 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
android:fillColor="#fff"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_service_idle">
<target android:name="strike_thru_path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 2 4.27 L 3.27 3 L 3.27 3 L 2 4.27 Z"
android:valueTo="M 19.73 22 L 21 20.73 L 3.27 3 L 2 4.27 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="strike_thru_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 3.27 3 L 4.54 1.73 Z"
android:valueTo="M 0 0 L 24 0 L 24 24 L 0 24 L 0 0 Z M 4.54 1.73 L 3.27 3 L 21 20.73 L 22.27 19.46 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>
<?xml version="1.0" encoding="utf-8"?>
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_service_busy">
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:duration="@android:integer/config_mediumAnimTime"
android:valueFrom="M 15.5 13.28 L 15.5 13.28 L 15.5 13.28 L 15.5 13.28 M 7.14 11.9 L 7.14 11.9 L 7.14 11.9 L 7.14 11.9 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
android:valueTo="M 17.68 9 L 16.09 16 L 12.7 14.89 L 17.7 8.96 M 10 10.08 L 6.43 13.08 L 5 12.55 L 10 10.08 M 21.25 2.28 L 0 12.8 L 6.83 15.37 L 16.59 7.16 L 9.26 15.89 L 17.55 18.56 L 21.25 2.29 L 21.25 2.29 Z M 9.45 17.56 L 9.46 22 L 12.09 18.41 L 9.45 17.56 L 9.45 17.56 Z"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>
<!-- https://android.googlesource.com/platform/packages/apps/Settings/+/7961482/res/drawable/ic_password.xml -->
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21.5,9.39l-1.63,0l0.81,-1.42l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M13.68,7.97l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0l0,-1l-1.63,0z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M6.68,7.97l-0.86,-0.5l-0.82,1.42l-0.82,-1.42l-0.86,0.5l0.81,1.42l-1.63,0l0,1l1.63,0l-0.81,1.41l0.86,0.5l0.82,-1.41l0.82,1.41l0.86,-0.5l-0.81,-1.41l1.63,0l0,-1l-1.63,0z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,17.89H3c-0.28,0 -0.5,-0.22 -0.5,-0.5v-1c0,-0.28 0.22,-0.5 0.5,-0.5h18c0.28,0 0.5,0.22 0.5,0.5v1C21.5,17.66 21.28,17.89 21,17.89z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M3,2h8v2h-8z"/>
<path
android:fillColor="#FF000000"
android:pathData="M6,11l2,0l0,-4l3,0l0,-2l-8,0l0,2l3,0z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12.4036,20.1819l7.7781,-7.7781l1.4142,1.4142l-7.7781,7.7781z"/>
<path
android:fillColor="#FF000000"
android:pathData="M14.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path
android:fillColor="#FF000000"
android:pathData="M19.5,19.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
<path
android:fillColor="#FF000000"
android:pathData="M15.5,11c1.38,0 2.5,-1.12 2.5,-2.5V4h3V2h-4v4.51C16.58,6.19 16.07,6 15.5,6C14.12,6 13,7.12 13,8.5C13,9.88 14.12,11 15.5,11z"/>
<path
android:fillColor="#FF000000"
android:pathData="M9.74,15.96l-1.41,1.41l-0.71,-0.71l0.35,-0.35c0.98,-0.98 0.98,-2.56 0,-3.54c-0.49,-0.49 -1.13,-0.73 -1.77,-0.73c-0.64,0 -1.28,0.24 -1.77,0.73c-0.98,0.98 -0.98,2.56 0,3.54l0.35,0.35l-1.06,1.06c-0.98,0.98 -0.98,2.56 0,3.54C4.22,21.76 4.86,22 5.5,22s1.28,-0.24 1.77,-0.73l1.06,-1.06l1.41,1.41l1.41,-1.41l-1.41,-1.41l1.41,-1.41L9.74,15.96zM5.85,14.2c0.12,-0.12 0.26,-0.15 0.35,-0.15s0.23,0.03 0.35,0.15c0.19,0.2 0.19,0.51 0,0.71l-0.35,0.35L5.85,14.9C5.66,14.71 5.66,14.39 5.85,14.2zM5.85,19.85C5.73,19.97 5.59,20 5.5,20s-0.23,-0.03 -0.35,-0.15c-0.19,-0.19 -0.19,-0.51 0,-0.71l1.06,-1.06l0.71,0.71L5.85,19.85z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Spinner android:id="@+id/template_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@+id/content"
android:entries="@array/acl_rule_templates"/>
<Space
android:layout_width="match_parent"
android:layout_height="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:inputType="textNoSuggestions|textMultiLine"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:inputType="textNoSuggestions|textMultiLine"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:focusable="true">
<ImageView android:id="@android:id/icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:importantForAccessibility="no"/>
<Space android:layout_width="4dp"
android:layout_height="wrap_content"/>
<LinearLayout android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical">
<TextView android:id="@android:id/text1"
android:textAppearance="?android:attr/textAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"/>
<TextView android:id="@android:id/text2"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"/>
</LinearLayout>
<ImageView
android:id="@+id/unlock"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:paddingStart="12dp"
android:src="@drawable/ic_action_lock_open"
android:tint="@color/material_amber_a700"
android:importantForAccessibility="no"
tools:ignore="RtlSymmetry" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/about_container"
android:fitsSystemWindows="true">
<include layout="@layout/toolbar_light_dark"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:scrollbars="vertical">
<com.github.shadowsocks.widget.AutoCollapseTextView
android:id="@+id/tv_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textIsSelectable="true"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:duplicateParentState="false">
<com.google.android.material.appbar.MaterialToolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:touchscreenBlocksFocus="false"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
android:id="@+id/toolbar">
<SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:iconifiedByDefault="false"
android:queryHint="@android:string/search_go"/>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:elevation="1dp">
<TextView
android:id="@+id/tv_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:minWidth="42dp"
android:text="@string/proxied_apps"
app:layout_constraintBottom_toBottomOf="@+id/bypassGroup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/bypassGroup" />
<RadioGroup
android:id="@+id/bypassGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_mode"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_off"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/off" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_on"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/on" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_bypass"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/bypass_apps"
tools:checked="true"
app:tooltipText="@string/bypass_apps_summary" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ProgressBar android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.recyclerview.widget.RecyclerView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/list"
android:clipToPadding="false"
android:visibility="gone"
tools:listitem="@layout/layout_apps_item"
tools:visibility="visible"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true">
<ImageView
android:id="@+id/itemicon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@android:drawable/sym_def_app_icon" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/itemcheck"
app:layout_constraintStart_toEndOf="@+id/itemicon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Package label" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:textColor="?android:attr/textColorTertiary"
android:textSize="12sp"
android:breakStrategy="high_quality"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="com.package.name (10000)" />
<Switch
android:id="@+id/itemcheck"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark"/>
<FrameLayout android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@android:layout/simple_list_item_1"/>
</FrameLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<include layout="@layout/toolbar_light_dark" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/content"
android:name="com.github.shadowsocks.GlobalSettingsPreferenceFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<include layout="@layout/toolbar_light_dark"/>
<androidx.recyclerview.widget.RecyclerView android:id="@+id/list"
tools:listitem="@layout/layout_profile"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical"
android:clipChildren="false"
android:clipToPadding="false"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragment_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"/>
<!-- We double trackThickness as half of it will be invisible -->
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/fabProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:max="1"
android:visibility="invisible"
app:layout_anchor="@+id/fab"
app:layout_anchorGravity="center"
app:layout_behavior=".widget.FabProgressBehavior"
app:indicatorColor="@color/material_accent_200"
app:trackThickness="8dp"
app:trackCornerRadius="@dimen/mtrl_progress_track_thickness"/>
<com.github.shadowsocks.widget.ServiceButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:nextFocusDown="@+id/stats"
app:layout_anchor="@id/stats"
app:backgroundTint="@color/background_service"
app:fabSize="normal"
app:pressedTranslationZ="6dp"
app:srcCompat="@drawable/ic_service_idle"/>
<com.github.shadowsocks.widget.StatsBar
android:id="@+id/stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:fitsSystemWindows="true"
android:nextFocusUp="@+id/fab"
app:backgroundTint="@color/background_stat"
app:contentInsetStart="0dp"
app:layout_scrollFlags="enterAlways|scroll"
app:hideOnScroll="true">
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dip">
<TextView
android:id="@+id/tx"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_gravity="fill_horizontal"
android:layout_columnWeight="1"/>
<TextView
android:id="@+id/txRate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:gravity="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_gravity="fill_horizontal"
android:layout_row="0"
android:layout_column="2"
android:layout_columnWeight="1"/>
<TextView
android:id="@+id/rx"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_gravity="fill_horizontal"
android:layout_column="0"
android:layout_columnWeight="1"
android:layout_row="1"/>
<TextView
android:id="@+id/rxRate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:gravity="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_gravity="fill_horizontal"
android:layout_column="2"
android:layout_columnWeight="1"
android:layout_row="1"/>
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:layout_gravity="fill_horizontal"
android:layout_row="2"
android:layout_column="0"
android:layout_columnSpan="3"
android:ellipsize="end"
tools:text="@string/connection_test_available"/>
</GridLayout>
</com.github.shadowsocks.widget.StatsBar>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:insetForeground="@null"
app:itemIconTint="@color/nav_item_tint"
app:itemTextColor="@color/nav_item_tint"
app:headerLayout="@layout/navigation_header"
app:menu="@menu/navigation_main" />
</androidx.drawerlayout.widget.DrawerLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://github.com/android/platform_frameworks_base/blob/505e3ab/core/res/res/layout/simple_list_item_2.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_profile"
android:focusable="true"
android:nextFocusRight="@+id/edit"
android:orientation="vertical">
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
>
<TextView
android:id="@android:id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
tools:text="@string/profile_name"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/subscription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/subscriptions"
android:focusable="true"
android:padding="12dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_file_cloud_queue"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit"
android:focusable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_image_edit"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/share"
android:focusable="true"
android:nextFocusLeft="@+id/container"
android:padding="12dp"
app:srcCompat="@drawable/ic_social_share"/>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
>
<com.github.shadowsocks.widget.AutoCollapseTextView
android:id="@android:id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginBottom="@dimen/profile_padding"
android:layout_marginEnd="12dp"
android:layout_toStartOf="@+id/traffic"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@string/profile_plugin"/>
<com.github.shadowsocks.widget.AutoCollapseTextView
android:id="@+id/traffic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/profile_padding"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="@string/traffic"/>
</RelativeLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/content"
android:name="com.github.shadowsocks.ProfileConfigFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<androidx.camera.view.PreviewView
android:id="@+id/barcode"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@android:layout/simple_list_item_1" />
</FrameLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="1dp"
android:background="@android:color/white">
<Switch android:id="@+id/serviceSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/toggle_service_state"
android:textSize="18sp"
android:padding="16dp"/>
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbars="vertical"
tools:itemview="@android:layout/select_dialog_singlechoice_material"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_light_dark" />
<androidx.recyclerview.widget.RecyclerView android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:itemview="@android:layout/select_dialog_singlechoice_material"
android:scrollbars="vertical"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
android:src="@drawable/background_header"/>
<TextView android:id="@+id/drawer_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:fontFamily="@font/iceland"
android:gravity="center"
android:text="shadowsocks"
android:textColor="@android:color/white"
android:textSize="32sp"
tools:ignore="HardcodedText"/>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Based on: https://android.googlesource.com/platform/frameworks/support/+/b4cd329/preference/preference/res/layout/preference_dialog_edittext.xml -->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="48dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"/>
<com.google.android.material.textfield.TextInputLayout
android:orientation="vertical"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:paddingTop="12dp"
android:singleLine="true"
android:inputType="textPassword"
android:typeface="monospace"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</ScrollView>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_apply_all"
android:title="@string/action_apply_all"
android:alphabeticShortcut="a"
app:alphabeticModifiers="CTRL|SHIFT"
app:showAsAction="never"/>
<item
android:id="@+id/action_export_clipboard"
android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable"
android:title="@string/action_export"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_import_clipboard"
android:alphabeticShortcut="v"
android:icon="?attr/actionModePasteDrawable"
android:title="@string/action_import"
app:showAsAction="ifRoom"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/action_add_rule"
android:icon="@drawable/ic_av_playlist_add"
android:alphabeticShortcut="n"
app:showAsAction="always">
<menu>
<item android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="n"
app:alphabeticModifiers="CTRL|SHIFT"/>
<item android:id="@+id/action_import_clipboard"
android:title="@string/action_import"
android:alphabeticShortcut="v"/>
<item android:id="@+id/action_import_gfwlist"
android:title="@string/route_entry_gfwlist"/>
</menu>
</item>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource">
<item
android:id="@+id/action_select_all"
android:alphabeticShortcut="a"
android:icon="?attr/actionModeSelectAllDrawable"
android:title="@android:string/selectAll"
android:showAsAction="always"/>
<item
android:id="@+id/action_cut"
android:alphabeticShortcut="x"
android:icon="?attr/actionModeCutDrawable"
android:title="@android:string/cut"
android:showAsAction="always"/>
<item
android:id="@+id/action_copy"
android:alphabeticShortcut="c"
android:icon="?attr/actionModeCopyDrawable"
android:title="@android:string/copy"
android:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:alphabeticShortcut="d"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete"
android:showAsAction="always"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/main"
android:checkableBehavior="single">
<item android:id="@+id/profiles"
android:title="@string/profiles"
android:icon="@drawable/ic_action_description"/>
<item android:id="@+id/subscriptions"
android:title="@string/subscriptions"
android:icon="@drawable/ic_file_cloud_download"/>
<item android:id="@+id/customRules"
android:title="@string/custom_rules"
android:icon="@drawable/ic_action_assignment"/>
<item android:id="@+id/globalSettings"
android:title="@string/settings"
android:icon="@drawable/ic_action_settings"/>
</group>
<group android:id="@+id/misc"
android:checkableBehavior="single">
<item android:id="@+id/faq"
android:checkable="false"
android:title="@string/faq"
android:icon="@drawable/ic_action_help_outline"/>
<item android:id="@+id/about"
android:title="@string/about"
android:icon="@drawable/ic_action_copyright"/>
</group>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_delete"
android:title="@string/delete"
android:icon="@drawable/ic_action_delete"
android:alphabeticShortcut="d"
app:showAsAction="always"/>
<item android:id="@+id/action_apply"
android:title="@string/apply"
android:icon="@drawable/ic_action_done"
android:alphabeticShortcut="s"
app:showAsAction="always"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:alphabeticShortcut="n"
android:icon="@drawable/ic_action_note_add"
android:title="@string/add_profile"
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_scan_qr_code"
android:title="@string/add_profile_methods_scan_qr_code"/>
<item
android:id="@+id/action_import_clipboard"
android:alphabeticShortcut="v"
android:title="@string/action_import"/>
<item
android:id="@+id/action_import_file"
android:alphabeticShortcut="o"
android:title="@string/action_import_file"/>
<item
android:id="@+id/action_replace_file"
android:alphabeticShortcut="o"
android:title="@string/action_replace_file"
app:alphabeticModifiers="CTRL|SHIFT"/>
<item
android:id="@+id/action_manual_settings"
android:title="@string/add_profile_methods_manual_settings"
android:alphabeticShortcut="n"
app:alphabeticModifiers="CTRL|SHIFT"/>
</menu>
</item>
<item
android:alphabeticShortcut="s"
android:icon="@drawable/ic_file_file_upload"
android:title="@string/action_export_more"
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_export_clipboard"
android:alphabeticShortcut="c"
android:title="@string/action_export"/>
<item
android:id="@+id/action_export_file"
android:title="@string/action_export_file"
android:alphabeticShortcut="s"
app:alphabeticModifiers="CTRL|SHIFT"/>
</menu>
</item>
<item android:id="@+id/action_search"
android:title="@android:string/search_go"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_qr_code"
android:title="@string/share_qr_nfc"/>
<item android:id="@+id/action_export_clipboard"
android:title="@string/action_export"/>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_import_clipboard"
android:title="@string/action_import_file"
android:icon="@drawable/ic_image_photo"
android:alphabeticShortcut="o"
app:showAsAction="always"/>
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_subscription"
android:id="@+id/action_manual_settings"
android:icon="@drawable/ic_av_playlist_add"
android:alphabeticShortcut="n"
app:showAsAction="always"/>
<item android:title="@string/update_subscription"
android:id="@+id/action_update_subscription"
android:icon="@drawable/ic_file_cloud_download"
android:alphabeticShortcut="r"
app:showAsAction="ifRoom"/>
</menu>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<p>A <a href="http://shadowsocks.org">Shadowsocks</a> client for Android, written in Kotlin.</p>
<p>Copyright (C) 2017 by Max Lv <a href="&#x6d;&#97;&#x69;&#108;&#116;&#111;&#x3a;&#x6d;&#97;x&#46;&#99;.&#108;&#118;&#x40;&#103;&#109;&#97;&#x69;&#108;&#46;&#x63;&#x6f;&#109;">&#x6d;&#97;&#x78;.&#x63;&#x2e;&#108;v&#64;&#x67;&#109;&#x61;&#x69;&#108;&#x2e;&#99;&#111;&#109;</a></p>
<p>Copyright (C) 2017 by Mygod Studio <a href="mailto:contact-shadowsocks-android@mygod.be">contact-shadowsocks-android@mygod.be</a></p>
<p>This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.</p>
<p>This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.</p>
<p>You should have received a copy of the GNU General Public License
along with this program. If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.</p>
<h3>Open Source Licenses</h3>
<ul>
<li>redsocks: <a href="https://github.com/shadowsocks/redsocks/blob/shadowsocks-android/README">APL 2.0</a>&nbsp;</li>
<li>libevent: <a href="https://github.com/shadowsocks/libevent/blob/master/LICENSE">BSD</a>&nbsp;</li>
<li>tun2socks: <a href="https://github.com/shadowsocks/badvpn/blob/shadowsocks-android/COPYING">BSD</a>&nbsp;</li>
<li>shadowsocks-rust: <a href="https://github.com/shadowsocks/shadowsocks-rust/blob/master/LICENSE">MIT</a>&nbsp;</li>
<li>libsodium: <a href="https://github.com/jedisct1/libsodium/blob/master/LICENSE">ISC</a>&nbsp;</li>
<li>OpenSSL: <a href="https://www.openssl.org/source/license-openssl-ssleay.txt">OpenSSL License</a>&nbsp;</li>
<li><a href="#other-licenses">Other licenses</a></li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_selected">@color/material_primary_800</color>
<color name="background_stat">@color/material_primary_600</color>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="route_entry">
<item>@string/route_entry_all</item>
<item>@string/route_entry_bypass_lan</item>
<item>@string/route_entry_bypass_chn</item>
<item>@string/route_entry_bypass_lan_chn</item>
<item>@string/route_entry_gfwlist</item>
<item>@string/route_entry_chinalist</item>
<item>@string/custom_rules</item>
</string-array>
<string-array name="route_value" translatable="false">
<item>all</item>
<item>bypass-lan</item>
<item>bypass-china</item>
<item>bypass-lan-china</item>
<item>gfwlist</item>
<item>china-list</item>
<item>custom-rules</item>
</string-array>
<string-array name="acl_rule_templates" translatable="false">
<item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item>
<item>@string/acl_rule_online_config</item>
</string-array>
<string-array name="service_modes">
<item>@string/service_mode_proxy</item>
<item>@string/service_mode_vpn</item>
<item>@string/service_mode_transproxy</item>
</string-array>
<string-array name="service_mode_values" translatable="false">
<item>proxy</item>
<item>vpn</item>
<item>transproxy</item>
</string-array>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_selected">@color/material_primary_100</color>
<color name="background_stat">@color/material_primary_300</color>
<color name="material_amber_a700">#ffab00</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="qrcode_size">264dp</dimen>
<dimen name="profile_padding">8dp</dimen>
<dimen name="main_list_padding_bottom">88dp</dimen>
<dimen name="bottom_sheet_padding">8dp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Shadowsocks.Immersive.Navigation">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
</domain-config>
</network-security-config>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
app:initialExpandedChildrenCount="2">
<SwitchPreference
app:key="isAutoConnect"
app:icon="@drawable/ic_communication_phonelink_ring"
app:summary="@string/auto_connect_summary"
app:title="@string/auto_connect"/>
<SwitchPreference
app:key="directBootAware"
app:icon="@drawable/ic_action_lock"
app:summary="@string/direct_boot_aware_summary"
app:title="@string/direct_boot_aware"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="serviceMode"
app:icon="@drawable/ic_device_developer_mode"
app:entries="@array/service_modes"
app:entryValues="@array/service_mode_values"
app:defaultValue="vpn"
app:title="@string/service_mode"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portProxy"
app:icon="@drawable/ic_maps_directions_boat"
app:title="@string/port_proxy"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portLocalDns"
app:icon="@drawable/ic_action_dns"
app:title="@string/port_local_dns"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="portTransproxy"
app:title="@string/port_transproxy"
app:useSimpleSummaryProvider="true"/>
</PreferenceScreen>
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<EditTextPreference
app:key="profileName"
app:icon="@drawable/ic_social_emoji_symbols"
app:title="@string/profile_name"
app:useSimpleSummaryProvider="true"/>
<PreferenceCategory
app:title="@string/proxy_cat">
<EditTextPreference
app:key="proxy"
app:icon="@drawable/ic_hardware_router"
app:title="@string/proxy"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="remotePortNum"
app:icon="@drawable/ic_maps_directions_boat"
app:title="@string/remote_port"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:dialogLayout="@layout/preference_dialog_password"
app:key="sitekey"
app:icon="@drawable/ic_settings_password"
app:title="@string/sitekey"/>
<com.takisoft.preferencex.SimpleMenuPreference
app:key="encMethod"
app:icon="@drawable/ic_notification_enhanced_encryption"
app:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value"
app:title="@string/enc_method"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/feature_cat">
<com.takisoft.preferencex.SimpleMenuPreference
app:key="route"
app:icon="@drawable/ic_maps_directions"
app:entries="@array/route_entry"
app:entryValues="@array/route_value"
app:title="@string/route_list"
app:useSimpleSummaryProvider="true"/>
<SwitchPreference
app:key="isIpv6"
app:icon="@drawable/ic_image_looks_6"
app:summary="@string/ipv6_summary"
app:title="@string/ipv6"/>
<SwitchPreference
app:key="isProxyApps"
app:icon="@drawable/ic_navigation_apps"
app:summary="@string/proxied_apps_summary"
app:title="@string/proxied_apps"/>
<SwitchPreference
app:key="metered"
app:icon="@drawable/ic_device_data_usage"
app:summary="@string/metered_summary"
app:title="@string/metered"/>
<EditTextPreference
app:key="remoteDns"
app:icon="@drawable/ic_action_dns"
app:title="@string/remote_dns"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/plugin">
<com.github.shadowsocks.preference.PluginPreference
app:key="plugin"
app:persistent="false"
app:title="@string/plugin"
app:useSimpleSummaryProvider="true"/>
<EditTextPreference
app:key="plugin.configure"
app:icon="@drawable/ic_action_settings"
app:persistent="false"
app:title="@string/plugin_configure"
app:useSimpleSummaryProvider="true"/>
<Preference
app:key="udpFallback"
app:icon="@drawable/ic_maps_360"
app:title="@string/udp_fallback"
app:summary="@string/plugin_disabled">
<intent android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.UdpFallbackProfileActivity"/>
</Preference>
</PreferenceCategory>
</PreferenceScreen>
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut android:shortcutId="toggle"
android:icon="@drawable/ic_qu_shadowsocks_launcher"
android:shortcutShortLabel="@string/quick_toggle"
android:shortcutLongLabel="@string/quick_toggle">
<intent android:action="android.intent.action.MAIN"
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.QuickToggleShortcut" />
</shortcut>
<shortcut android:shortcutId="scan"
android:icon="@drawable/ic_qu_camera_launcher"
android:shortcutShortLabel="@string/add_profile_methods_scan_qr_code"
android:shortcutLongLabel="@string/add_profile_methods_scan_qr_code">
<intent android:action="android.intent.action.MAIN"
android:targetPackage="com.github.shadowsocks"
android:targetClass="com.github.shadowsocks.ScannerActivity" />
</shortcut>
</shortcuts>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment