Commit 8885f6de authored by sheteng's avatar sheteng

加速修改模式切换

parent db25102b
......@@ -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 {
......@@ -154,9 +156,8 @@ class MainActivity() : XPageActivity(), TencentLocationListener {
mSensor,
SensorManager.SENSOR_DELAY_NORMAL
)
Handler().postDelayed({
AcceleratorUtils.checkVpn()
}, 1000)
AcceleratorUtils.checkVpn(this)
//QQ官方sdk授权
Tencent.setIsPermissionGranted(true)
}
......
......@@ -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 {
}
}
}
}
}, 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()
}
}, 1, 1, TimeUnit.MINUTES)
}
},
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.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.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.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.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) 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>
This diff is collapsed.
This diff is collapsed.
<?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>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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