Compare commits

..
5 Commits
Author SHA1 Message Date
Mygod 7922be5653 Fix signatures are being deprecated 2018-03-08 23:50:49 -08:00
Mygod a134cba08a Merge branch 'master' into beta 2018-03-08 23:44:44 -08:00
Mygod da2490b1b8 Use ContextCompat.getSystemService 2018-03-08 23:31:29 -08:00
Mygod 3a8f863bd1 Implement network capabilities reporting
This commit should fix the problem where VPN connection is viewed as metered connections on Android P DP1. However as I only have an emulator to test, I can't tell if it really works but it should.

An interesting observation is that the developer can call setUnderlyingNetworks(arrayOfNulls(1)) to make the VPN connection always unmetered. This is more of a quick hack instead of a real fix. Source: https://android.googlesource.com/platform/frameworks/base/+/adbf1d0/services/core/java/com/android/server/connectivity/Vpn.java#312

One may ask whether it's possible to fix this issue for Android 8.1. The short answer is not that I know of.
After some intensive source code reading, I found out that this is the commit that "breaks" whether VPN is metered for Android 8.1: https://android.googlesource.com/platform/frameworks/base/+/43d2a1700b6eb1d804924c6a1e5e0161a13a5348%5E%21/
The key is that isActiveNetworkMetered() used to return getActiveNetworkInfo().isMetered() and now it uses the more up-to-date API getActiveNetwork(). However, getActiveNetworkInfo() ignores VPN connections while getActiveNetwork() prefers VPN connection. And since NetworkCapabilities for VPN Network is always metered by default, this ultimately causes the bug. One might be able to get around this bug by somehow calling NetworkAgent.sendNetworkCapabilities in the correct context.

Fix #1666.
2018-03-08 22:47:58 -08:00
Mygod 8a273ab762 Update to Android P DP1 2018-03-08 22:45:23 -08:00
36 changed files with 5326 additions and 280 deletions
+1 -2
View File
@@ -29,8 +29,7 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support; 3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support;
4. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either. 4. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) * Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836) * Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414) * Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410) * Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
+6 -2
View File
@@ -12,6 +12,11 @@
Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/). Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/).
### Как добавить QR код из локальной галереи?
Отсканируйте его при помощи стороннего сканера, например [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) и нажмите на `ss` ссылку.
### Почему режим NAT более не поддерживается? ### Почему режим NAT более не поддерживается?
1. Требуются ROOT права; 1. Требуются ROOT права;
@@ -29,8 +34,7 @@
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки"; 3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними. 4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
* Исправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) * Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Исправления для EMUI: [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
* Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836) * Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414) * Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
* Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410) * Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
+5 -4
View File
@@ -6,9 +6,10 @@ buildscript {
ext { ext {
kotlinVersion = '1.2.30' kotlinVersion = '1.2.30'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 27 sdkVersion = 'P'
buildToolsVersion = '27.0.3' compileSdkVersion = 'android-P'
supportLibraryVersion = '27.1.0' buildToolsVersion = '28-rc1'
supportLibraryVersion = '28.0.0-alpha1'
takisoftFixVersion = '27.1.0.0' takisoftFixVersion = '27.1.0.0'
playServicesVersion = '11.8.0' playServicesVersion = '11.8.0'
junitVersion = '4.12' junitVersion = '4.12'
@@ -22,7 +23,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.android.tools.build:gradle:3.2.0-alpha05'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:3.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+1 -1
View File
@@ -4,7 +4,7 @@ apply plugin: 'com.android.library'
android { android {
buildToolsVersion rootProject.buildToolsVersion buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion compileSdkVersion rootProject.compileSdkVersion
defaultConfig { defaultConfig {
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+3 -3
View File
@@ -16,13 +16,13 @@ def getCurrentFlavor() {
android { android {
buildToolsVersion rootProject.buildToolsVersion buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion compileSdkVersion rootProject.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "com.github.shadowsocks" applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode 4050200 versionCode 4050100
versionName "4.5.2" versionName "4.5.1"
testApplicationId "com.github.shadowsocks.test" testApplicationId "com.github.shadowsocks.test"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW" resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
/*******************************************************************************
* *
* 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 android.support.design.animation
import android.animation.TimeInterpolator
object AnimationConsts {
val FAST_OUT_SLOW_IN_INTERPOLATOR: TimeInterpolator get() = AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR
}
@@ -1,8 +0,0 @@
package android.support.design.widget
import android.view.animation.Interpolator
object SnackbarAnimation {
val FAST_OUT_SLOW_IN_INTERPOLATOR: Interpolator get() = AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR
const val ANIMATION_DURATION = BaseTransientBottomBar.ANIMATION_DURATION.toLong()
}
@@ -0,0 +1,25 @@
/*******************************************************************************
* *
* 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 android.support.design.widget
object SnackbarConsts {
const val ANIMATION_DURATION = BaseTransientBottomBar.ANIMATION_DURATION.toLong()
}
@@ -34,6 +34,7 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.support.annotation.RequiresApi import android.support.annotation.RequiresApi
import android.support.v4.os.BuildCompat
import android.support.v4.os.UserManagerCompat import android.support.v4.os.UserManagerCompat
import android.support.v7.app.AppCompatDelegate import android.support.v7.app.AppCompatDelegate
import android.util.Log import android.util.Log
@@ -73,8 +74,8 @@ class App : Application() {
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) } private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
val info: PackageInfo by lazy { getPackageInfo(packageName) } val info: PackageInfo by lazy { getPackageInfo(packageName) }
fun getPackageInfo(packageName: String) = fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, if (BuildCompat.isAtLeastP())
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)!! PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() { fun startService() {
val intent = Intent(this, BaseService.serviceClass.java) val intent = Intent(this, BaseService.serviceClass.java)
@@ -27,7 +27,6 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -49,10 +48,7 @@ import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.*
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.thread
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener { class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
@@ -142,7 +138,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private lateinit var loadingView: View private lateinit var loadingView: View
private val appsLoading = AtomicBoolean() private val appsLoading = AtomicBoolean()
private val handler = Handler() private val handler = Handler()
private val clipboard by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } private val clipboard by lazy { systemService<ClipboardManager>() }
private fun initProxiedApps(str: String = DataStore.individual) { private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet() proxiedApps = str.split('\n').toHashSet()
@@ -56,7 +56,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1 private const val REQUEST_CODE_PLUGIN_CONFIGURE = 1
} }
private var profileId = -1 private lateinit var profile: Profile
private lateinit var isProxyApps: SwitchPreference private lateinit var isProxyApps: SwitchPreference
private lateinit var plugin: IconListPreference private lateinit var plugin: IconListPreference
private lateinit var pluginConfigure: EditTextPreference private lateinit var pluginConfigure: EditTextPreference
@@ -66,7 +66,13 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.privateStore preferenceManager.preferenceDataStore = DataStore.privateStore
val activity = requireActivity() val activity = requireActivity()
profileId = activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1) val profile = ProfileManager.getProfile(activity.intent.getIntExtra(Action.EXTRA_PROFILE_ID, -1))
if (profile == null) {
activity.finish()
return
}
this.profile = profile
profile.serialize()
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) { if (Build.VERSION.SDK_INT >= 25 && activity.getSystemService(UserManager::class.java).isDemoUser) {
findPreference(Key.host).summary = "shadowsocks.example.org" findPreference(Key.host).summary = "shadowsocks.example.org"
@@ -124,12 +130,10 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
} }
fun saveAndExit() { fun saveAndExit() {
val profile = ProfileManager.getProfile(profileId) ?: Profile()
profile.id = profileId
profile.deserialize() profile.deserialize()
ProfileManager.updateProfile(profile) ProfileManager.updateProfile(profile)
ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profileId) ProfilesFragment.instance?.profilesAdapter?.deepRefreshId(profile.id)
if (DataStore.profileId == profileId && DataStore.directBootAware) DirectBoot.update() if (DataStore.profileId == profile.id && DataStore.directBootAware) DirectBoot.update()
requireActivity().finish() requireActivity().finish()
} }
@@ -183,7 +187,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.delete_confirm_prompt) .setTitle(R.string.delete_confirm_prompt)
.setPositiveButton(R.string.yes, { _, _ -> .setPositiveButton(R.string.yes, { _, _ ->
ProfileManager.delProfile(profileId) ProfileManager.delProfile(profile.id)
activity.finish() activity.finish()
}) })
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
@@ -45,6 +45,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize import com.google.android.gms.ads.AdSize
@@ -123,7 +124,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var adView: AdView? = null private var adView: AdView? = null
init { init {
edit.setOnClickListener { startConfig(item) } edit.setOnClickListener { startConfig(item.id) }
TooltipCompat.setTooltipText(edit, edit.contentDescription) TooltipCompat.setTooltipText(edit, edit.contentDescription)
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
val share = itemView.findViewById<View>(R.id.share) val share = itemView.findViewById<View>(R.id.share)
@@ -305,12 +306,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var txTotal: Long = 0L private var txTotal: Long = 0L
private var rxTotal: Long = 0L private var rxTotal: Long = 0L
private val clipboard by lazy { requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun startConfig(profile: Profile) { private fun startConfig(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
profile.serialize() .putExtra(Action.EXTRA_PROFILE_ID, id))
startActivity(Intent(context, ProfileConfigActivity::class.java).putExtra(Action.EXTRA_PROFILE_ID, profile.id))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.layout_list, container, false) inflater.inflate(R.layout.layout_list, container, false)
@@ -368,20 +367,20 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
} }
R.id.action_import -> { R.id.action_import -> {
try { try {
val profiles = Profile.findAll(clipboard.primaryClip!!.getItemAt(0).text).toList() val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
if (profiles.isNotEmpty()) { if (profiles.isNotEmpty()) {
profiles.forEach { ProfileManager.createProfile(it) } profiles.forEach { ProfileManager.createProfile(it) }
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_msg, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_msg,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
return true return true
} }
} catch (_: Exception) { } } catch (_: IndexOutOfBoundsException) { }
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
true true
} }
R.id.action_manual_settings -> { R.id.action_manual_settings -> {
startConfig(ProfileManager.createProfile()) startConfig(ProfileManager.createProfile().id)
true true
} }
R.id.action_export -> { R.id.action_export -> {
@@ -21,7 +21,6 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@@ -40,6 +39,7 @@ import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic
import com.google.android.gms.vision.Frame import com.google.android.gms.vision.Frame
@@ -65,7 +65,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan") if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
if (try { if (try {
(getSystemService(Context.CAMERA_SERVICE) as CameraManager).cameraIdList.isEmpty() systemService<CameraManager>().cameraIdList.isEmpty()
} catch (_: CameraAccessException) { } catch (_: CameraAccessException) {
true true
}) { }) {
@@ -22,7 +22,6 @@ package com.github.shadowsocks
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.VpnService import android.net.VpnService
@@ -33,6 +32,7 @@ import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.bg.BaseService import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface { class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
companion object { companion object {
@@ -48,8 +48,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
finish() finish()
return return
} }
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager if (systemService<KeyguardManager>().inKeyguardRestrictedInputMode()) {
if (km.inKeyguardRestrictedInputMode()) {
receiver = broadcastReceiver { _, _ -> connection.connect() } receiver = broadcastReceiver { _, _ -> connection.connect() }
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT)) registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else connection.connect() } else connection.connect()
@@ -22,7 +22,6 @@ package com.github.shadowsocks.acl
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
@@ -48,6 +47,7 @@ import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.asIterable import com.github.shadowsocks.utils.asIterable
import com.github.shadowsocks.utils.resolveResourceId import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager import com.github.shadowsocks.widget.UndoSnackbarManager
import java.net.IDN import java.net.IDN
import java.net.MalformedURLException import java.net.MalformedURLException
@@ -360,7 +360,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
private lateinit var list: RecyclerView private lateinit var list: RecyclerView
private var mode: ActionMode? = null private var mode: ActionMode? = null
private lateinit var undoManager: UndoSnackbarManager<Any> private lateinit var undoManager: UndoSnackbarManager<Any>
private val clipboard by lazy { requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } private val clipboard by lazy { requireContext().systemService<ClipboardManager>() }
private fun onSelectedItemsUpdated() { private fun onSelectedItemsUpdated() {
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this) if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
@@ -438,7 +438,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
} }
R.id.action_import -> { R.id.action_import -> {
try { try {
check(adapter.addToProxy(clipboard.primaryClip!!.getItemAt(0).text.toString()) != null) adapter.addToProxy(clipboard.primaryClip.getItemAt(0).text.toString()) != null
} catch (exc: Exception) { } catch (exc: Exception) {
Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.make(requireActivity().findViewById(R.id.snackbar), R.string.action_import_err,
Snackbar.LENGTH_LONG).show() Snackbar.LENGTH_LONG).show()
@@ -354,7 +354,7 @@ object BaseService {
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.build() .build()
val mdg = MessageDigest.getInstance("SHA-1") val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures[0].toByteArray()) mdg.update(app.info.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder() val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0))) .add("sig", String(Base64.encode(mdg.digest(), 0)))
.build() .build()
@@ -84,6 +84,7 @@ object LocalDnsService {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config
.put("PrimaryDNS", localDns) .put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns) .put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", data.aclFile!!.absolutePath) .put("DomainFile", data.aclFile!!.absolutePath)
Acl.CHINALIST -> config Acl.CHINALIST -> config
.put("PrimaryDNS", localDns) .put("PrimaryDNS", localDns)
@@ -36,6 +36,7 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
import java.util.* import java.util.*
/** /**
@@ -46,7 +47,7 @@ import java.util.*
*/ */
class ServiceNotification(private val service: BaseService.Interface, profileName: String, class ServiceNotification(private val service: BaseService.Interface, profileName: String,
channel: String, private val visible: Boolean = false) { channel: String, private val visible: Boolean = false) {
private val keyGuard = (service as Context).getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager private val keyGuard = (service as Context).systemService<KeyguardManager>()
private val nm by lazy { private val nm by lazy {
(service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager (service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
} }
@@ -82,7 +83,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
service as Context service as Context
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close, if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0)) service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager val power = service.systemService<PowerManager>()
update(if (power.isInteractive) Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true) update(if (power.isInteractive) Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
val screenFilter = IntentFilter() val screenFilter = IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON) screenFilter.addAction(Intent.ACTION_SCREEN_ON)
@@ -21,7 +21,6 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.support.annotation.RequiresApi import android.support.annotation.RequiresApi
@@ -31,6 +30,7 @@ import com.github.shadowsocks.ShadowsocksConnection
import com.github.shadowsocks.aidl.IShadowsocksService import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.systemService
import android.service.quicksettings.TileService as BaseTileService import android.service.quicksettings.TileService as BaseTileService
@RequiresApi(24) @RequiresApi(24)
@@ -38,7 +38,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) } 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 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 iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
private val keyguard by lazy { getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager } private val keyguard by lazy { systemService<KeyguardManager>() }
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy { override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
@RequiresApi(24) @RequiresApi(24)
@@ -20,14 +20,15 @@
package com.github.shadowsocks.bg package com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.* import android.net.*
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.support.v4.os.BuildCompat
import android.util.Log import android.util.Log
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper import com.github.shadowsocks.JniHelper
@@ -38,6 +39,7 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.systemService
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.IOException import java.io.IOException
@@ -54,8 +56,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$") private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
/** /**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: * Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
* *
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that * This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately we need to have * satisfies default network capabilities but only THE default network. Unfortunately we need to have
@@ -76,22 +77,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
var success = false var success = false
try { try {
socket.inputStream.read() socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!! val fds = socket.ancillaryFileDescriptors
val fdInt = getInt.invoke(fd) as Int if (fds.isEmpty()) return
try { val fd = getInt.invoke(fds.first()) as Int
val network = underlyingNetwork success = protect(fd)
success = if (network != null && Build.VERSION.SDK_INT >= 23) { JniHelper.close(fd) // Trick to close file decriptor
network.bindSocket(fd)
true
} else protect(fdInt)
} catch (e: Exception) {
Log.e(tag, "Error when protect socket", e)
app.track(e)
} finally {
JniHelper.close(fdInt) // Trick to close file decriptor
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Error when receiving ancillary fd", e) Log.e(tag, "Error when protect socket", e)
app.track(e) app.track(e)
} }
try { try {
@@ -115,23 +107,19 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null private var worker: ProtectWorker? = null
private var tun2socksProcess: GuardedProcess? = null private var tun2socksProcess: GuardedProcess? = null
private var underlyingNetwork: Network? = null
set(value) {
if (Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(if (value == null) null else arrayOf(value))
field = value
}
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } private val connectivity by lazy { systemService<ConnectivityManager>() }
@TargetApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() { private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
underlyingNetwork = network setUnderlyingNetworks(arrayOf(network))
} }
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) { override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
// it's a good idea to refresh capabilities // it's a good idea to refresh capabilities
underlyingNetwork = network setUnderlyingNetworks(arrayOf(network))
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
underlyingNetwork = null setUnderlyingNetworks(null)
} }
} }
private var listeningForDefaultNetwork = false private var listeningForDefaultNetwork = false
@@ -144,7 +132,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner(true) override fun onRevoke() = stopRunner(true)
override fun killProcesses() { override fun killProcesses() {
if (listeningForDefaultNetwork) { if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false listeningForDefaultNetwork = false
} }
@@ -229,9 +217,12 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
this.conn = conn this.conn = conn
val fd = conn.fd val fd = conn.fd
// we want REQUEST here instead of LISTEN // We only need to update since Android P: https://android.googlesource.com/platform/frameworks/base/+/72f9c42b9e59761a28d6b32c42f65de57c98daed
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) if (BuildCompat.isAtLeastP()) {
listeningForDefaultNetwork = true // we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
listeningForDefaultNetwork = true
}
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath, val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"), "--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"),
@@ -1,61 +0,0 @@
/*******************************************************************************
* *
* 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.database
import android.database.sqlite.SQLiteDatabaseLockedException
import com.j256.ormlite.dao.Dao
import com.j256.ormlite.support.ConnectionSource
import com.j256.ormlite.table.TableUtils
import java.sql.SQLException
private val Throwable.ultimateCause: Throwable get() {
var result = this
while (true) {
val cause = result.cause ?: return result
result = cause
}
}
@Throws(SQLException::class)
fun <T> safeWrapper(func: () -> T): T {
while (true) {
try {
return func()
} catch (e: SQLException) {
if (e.ultimateCause !is SQLiteDatabaseLockedException) throw e
}
}
}
@Throws(SQLException::class)
inline fun <reified T> ConnectionSource.createTableSafe() = safeWrapper { TableUtils.createTable(this, T::class.java) }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.queryAllSafe(): MutableList<T> = safeWrapper { queryForAll() }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.queryByIdSafe(id: ID?): T? = safeWrapper { queryForId(id) }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.updateSafe(data: T?): Int = safeWrapper { update(data) }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.replaceSafe(data: T?): Dao.CreateOrUpdateStatus? = safeWrapper { createOrUpdate(data) }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.deleteByIdSafe(id: ID?) = safeWrapper { deleteById(id) }
@Throws(SQLException::class)
fun <T, ID> Dao<T, ID>.executeSafe(statement: String?) = safeWrapper { executeRawNoArgs(statement) }
@@ -37,18 +37,18 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> } val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource) { override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
connectionSource.createTableSafe<Profile>() TableUtils.createTable(connectionSource, Profile::class.java)
connectionSource.createTableSafe<KeyValuePair>() TableUtils.createTable(connectionSource, KeyValuePair::class.java)
} }
private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource) { private fun recreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
TableUtils.dropTable<Profile, Int>(connectionSource, Profile::class.java, true) TableUtils.dropTable<Profile, Int>(connectionSource, Profile::class.java, true)
TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true) TableUtils.dropTable<KeyValuePair, String?>(connectionSource, KeyValuePair::class.java, true)
onCreate(database, connectionSource) onCreate(database, connectionSource)
} }
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource, override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
oldVersion: Int, newVersion: Int) { oldVersion: Int, newVersion: Int) {
if (oldVersion < 7) { if (oldVersion < 7) {
recreate(database, connectionSource) recreate(database, connectionSource)
@@ -57,38 +57,38 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
try { try {
if (oldVersion < 8) { if (oldVersion < 8) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN udpdns SMALLINT;")
} }
if (oldVersion < 9) { if (oldVersion < 9) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN route VARCHAR DEFAULT 'all';")
} else if (oldVersion < 19) { } else if (oldVersion < 19) {
profileDao.executeSafe("UPDATE `profile` SET route = 'all' WHERE route IS NULL;") profileDao.executeRawNoArgs("UPDATE `profile` SET route = 'all' WHERE route IS NULL;")
} }
if (oldVersion < 11) { if (oldVersion < 11) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN ipv6 SMALLINT;")
} }
if (oldVersion < 12) { if (oldVersion < 12) {
profileDao.executeSafe("BEGIN TRANSACTION;") profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeSafe("ALTER TABLE `profile` RENAME TO `tmp`;") profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
connectionSource.createTableSafe<Profile>() TableUtils.createTable(connectionSource, Profile::class.java)
profileDao.executeSafe( profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," + "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," +
" proxyApps, bypass, udpdns, ipv6, individual) " + " proxyApps, bypass, udpdns, ipv6, individual) " +
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," + "SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," +
" bypass, udpdns, ipv6, individual FROM `tmp`;") " bypass, udpdns, ipv6, individual FROM `tmp`;")
profileDao.executeSafe("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeSafe("COMMIT;") profileDao.executeRawNoArgs("COMMIT;")
} else if (oldVersion < 13) { } else if (oldVersion < 13) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN tx LONG;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;")
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN rx LONG;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;")
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN date VARCHAR;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;")
} }
if (oldVersion < 15) { if (oldVersion < 15) {
if (oldVersion >= 12) profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN userOrder LONG;") if (oldVersion >= 12) profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN userOrder LONG;")
var i = 0L var i = 0L
val apps by lazy { app.packageManager.getInstalledApplications(0) } val apps by lazy { app.packageManager.getInstalledApplications(0) }
for (profile in profileDao.queryAllSafe()) { for (profile in profileDao.queryForAll()) {
if (oldVersion < 14) { if (oldVersion < 14) {
val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly) val uidSet = profile.individual.split('|').filter(TextUtils::isDigitsOnly)
.map(String::toInt).toSet() .map(String::toInt).toSet()
@@ -96,28 +96,28 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
.joinToString("\n") { it.packageName } .joinToString("\n") { it.packageName }
} }
profile.userOrder = i profile.userOrder = i
profileDao.updateSafe(profile) profileDao.update(profile)
i += 1 i += 1
} }
} }
if (oldVersion < 16) { if (oldVersion < 16) {
profileDao.executeSafe( profileDao.executeRawNoArgs(
"UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'") "UPDATE `profile` SET route = 'bypass-lan-china' WHERE route = 'bypass-china'")
} }
if (oldVersion < 21) { if (oldVersion < 21) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN remoteDns VARCHAR DEFAULT '8.8.8.8';")
} }
if (oldVersion < 17) { if (oldVersion < 17) {
profileDao.executeSafe("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN plugin VARCHAR;")
} else if (oldVersion < 22) { } else if (oldVersion < 22) {
// upgrade kcptun to SIP003 plugin // upgrade kcptun to SIP003 plugin
profileDao.executeSafe("BEGIN TRANSACTION;") profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
profileDao.executeSafe("ALTER TABLE `profile` RENAME TO `tmp`;") profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
connectionSource.createTableSafe<Profile>() TableUtils.createTable(connectionSource, Profile::class.java)
profileDao.executeSafe( profileDao.executeRawNoArgs(
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, " + "INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " + "remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"plugin) " + "plugin) " +
@@ -125,17 +125,17 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
"CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, route, " + "CASE WHEN kcp = 1 THEN kcpPort ELSE remotePort END, password, method, route, " +
"remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " + "remoteDns, proxyApps, bypass, udpdns, ipv6, individual, tx, rx, date, userOrder, " +
"CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;") "CASE WHEN kcp = 1 THEN 'kcptun ' || kcpcli ELSE NULL END FROM `tmp`;")
profileDao.executeSafe("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
profileDao.executeSafe("COMMIT;") profileDao.executeRawNoArgs("COMMIT;")
} }
if (oldVersion < 23) { if (oldVersion < 23) {
profileDao.executeSafe("BEGIN TRANSACTION;") profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
connectionSource.createTableSafe<KeyValuePair>() TableUtils.createTable(connectionSource, KeyValuePair::class.java)
profileDao.executeSafe("COMMIT;") profileDao.executeRawNoArgs("COMMIT;")
val old = PreferenceManager.getDefaultSharedPreferences(app) val old = PreferenceManager.getDefaultSharedPreferences(app)
PublicDatabase.kvPairDao.replaceSafe(KeyValuePair(Key.id).put(old.getInt(Key.id, 0))) PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.id).put(old.getInt(Key.id, 0)))
PublicDatabase.kvPairDao.replaceSafe(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false))) PublicDatabase.kvPairDao.createOrUpdate(KeyValuePair(Key.tfo).put(old.getBoolean(Key.tfo, false)))
} }
if (oldVersion < 25) { if (oldVersion < 25) {
@@ -31,7 +31,6 @@ import com.j256.ormlite.field.DataType
import com.j256.ormlite.field.DatabaseField import com.j256.ormlite.field.DatabaseField
import java.io.Serializable import java.io.Serializable
import java.net.URI import java.net.URI
import java.net.URISyntaxException
import java.util.* import java.util.*
class Profile : Serializable { class Profile : Serializable {
@@ -44,51 +43,41 @@ class Profile : Serializable {
fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map { fun findAll(data: CharSequence?) = pattern.findAll(data ?: "").map {
val uri = Uri.parse(it.value) val uri = Uri.parse(it.value)
try { if (uri.userInfo == null) {
if (uri.userInfo == null) { val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING))) if (match != null) {
if (match != null) { val profile = Profile()
val profile = Profile() profile.method = match.groupValues[1].toLowerCase()
profile.method = match.groupValues[1].toLowerCase() profile.password = match.groupValues[2]
profile.password = match.groupValues[2] profile.host = match.groupValues[3]
profile.host = match.groupValues[3] profile.remotePort = match.groupValues[4].toInt()
profile.remotePort = match.groupValues[4].toInt() profile.plugin = uri.getQueryParameter(Key.plugin)
profile.plugin = uri.getQueryParameter(Key.plugin) profile.name = uri.fragment
profile.name = uri.fragment profile
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else { } else {
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo, Log.e(TAG, "Unrecognized URI: ${it.value}")
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE))) null
if (match != null) { }
val profile = Profile() } else {
profile.method = match.groupValues[1] val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
profile.password = match.groupValues[2] Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855 if (match != null) {
try { val profile = Profile()
val javaURI = URI(it.value) profile.method = match.groupValues[1]
profile.host = javaURI.host ?: "" profile.password = match.groupValues[2]
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']') // bug in Android: https://code.google.com/p/android/issues/detail?id=192855
profile.host = profile.host.substring(1, profile.host.length - 1) val javaURI = URI(it.value)
profile.remotePort = javaURI.port profile.host = javaURI.host
profile.plugin = uri.getQueryParameter(Key.plugin) if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
profile.name = uri.fragment ?: "" profile.host = profile.host.substring(1, profile.host.length - 1)
profile profile.remotePort = javaURI.port
} catch (e: URISyntaxException) { profile.plugin = uri.getQueryParameter(Key.plugin)
Log.e(TAG, "Invalid URI: ${it.value}") profile.name = uri.fragment ?: ""
null profile
} } else {
} else { Log.e(TAG, "Unknown user info: ${it.value}")
Log.e(TAG, "Unknown user info: ${it.value}") null
null
}
} }
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
} }
}.filterNotNull() }.filterNotNull()
} }
@@ -48,12 +48,10 @@ object ProfileManager {
profile.individual = oldProfile.individual profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns profile.udpdns = oldProfile.udpdns
} }
val last = safeWrapper { val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult .selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
}
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1 if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
PrivateDatabase.profileDao.replaceSafe(profile) PrivateDatabase.profileDao.createOrUpdate(profile)
ProfilesFragment.instance?.profilesAdapter?.add(profile) ProfilesFragment.instance?.profilesAdapter?.add(profile)
return profile return profile
} }
@@ -62,10 +60,10 @@ object ProfileManager {
* Note: It's caller's responsibility to update DirectBoot profile if necessary. * Note: It's caller's responsibility to update DirectBoot profile if necessary.
*/ */
@Throws(SQLException::class) @Throws(SQLException::class)
fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.updateSafe(profile) fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile)
fun getProfile(id: Int): Profile? = try { fun getProfile(id: Int): Profile? = try {
PrivateDatabase.profileDao.queryByIdSafe(id) PrivateDatabase.profileDao.queryForId(id)
} catch (ex: SQLException) { } catch (ex: SQLException) {
Log.e(TAG, "getProfile", ex) Log.e(TAG, "getProfile", ex)
app.track(ex) app.track(ex)
@@ -74,16 +72,13 @@ object ProfileManager {
@Throws(SQLException::class) @Throws(SQLException::class)
fun delProfile(id: Int) { fun delProfile(id: Int) {
PrivateDatabase.profileDao.deleteByIdSafe(id) PrivateDatabase.profileDao.deleteById(id)
ProfilesFragment.instance?.profilesAdapter?.removeId(id) ProfilesFragment.instance?.profilesAdapter?.removeId(id)
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean() if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
} }
fun getFirstProfile(): Profile? = try { fun getFirstProfile(): Profile? = try {
safeWrapper { PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
PrivateDatabase.profileDao.query(
PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
}
} catch (ex: SQLException) { } catch (ex: SQLException) {
Log.e(TAG, "getFirstProfile", ex) Log.e(TAG, "getFirstProfile", ex)
app.track(ex) app.track(ex)
@@ -91,10 +86,7 @@ object ProfileManager {
} }
fun getAllProfiles(): List<Profile>? = try { fun getAllProfiles(): List<Profile>? = try {
safeWrapper { PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
PrivateDatabase.profileDao.query(
PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
}
} catch (ex: SQLException) { } catch (ex: SQLException) {
Log.e(TAG, "getAllProfiles", ex) Log.e(TAG, "getAllProfiles", ex)
app.track(ex) app.track(ex)
@@ -34,21 +34,20 @@ object PublicDatabase : OrmLiteSqliteOpenHelper(app.deviceContext, Key.DB_PUBLIC
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> } val kvPairDao: Dao<KeyValuePair, String?> by lazy { getDao(KeyValuePair::class.java) as Dao<KeyValuePair, String?> }
override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource) { override fun onCreate(database: SQLiteDatabase?, connectionSource: ConnectionSource?) {
connectionSource.createTableSafe<KeyValuePair>() TableUtils.createTable(connectionSource, KeyValuePair::class.java)
} }
override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?, override fun onUpgrade(database: SQLiteDatabase?, connectionSource: ConnectionSource?,
oldVersion: Int, newVersion: Int) { oldVersion: Int, newVersion: Int) {
if (oldVersion < 1) { if (oldVersion < 1) {
safeWrapper { PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key",
PrivateDatabase.kvPairDao.queryBuilder().where().`in`("key",
Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query() Key.id, Key.tfo, Key.serviceMode, Key.portProxy, Key.portLocalDns, Key.portTransproxy).query()
}.forEach { kvPairDao.replaceSafe(it) } .forEach { kvPairDao.createOrUpdate(it) }
} }
if (oldVersion < 2) { if (oldVersion < 2) {
kvPairDao.replaceSafe(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString())) kvPairDao.createOrUpdate(KeyValuePair(Acl.CUSTOM_RULES).put(Acl().fromId(Acl.CUSTOM_RULES).toString()))
} }
} }
@@ -25,7 +25,7 @@ import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init { init {
check(resolveInfo.providerInfo != null) assert(resolveInfo.providerInfo != null)
} }
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
@@ -32,6 +32,7 @@ import android.util.Base64
import android.util.Log import android.util.Log
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.signaturesCompat
import eu.chainfire.libsuperuser.Shell import eu.chainfire.libsuperuser.Shell
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -47,7 +48,7 @@ object PluginManager {
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys. * public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/ */
val trustedSignatures by lazy { val trustedSignatures by lazy {
app.info.signatures.toSet() + app.info.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod Signature(Base64.decode( // @Mygod
""" """
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD |MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
@@ -134,7 +135,7 @@ object PluginManager {
private fun initNative(options: PluginOptions): String? { private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders( val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0) Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
check(providers.size == 1) assert(providers.size == 1)
val uri = Uri.Builder() val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT) .scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers[0].providerInfo.authority) .authority(providers[0].providerInfo.authority)
@@ -154,7 +155,7 @@ object PluginManager {
out.putString(PluginContract.EXTRA_OPTIONS, options.id) out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out) val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out)
.getString(PluginContract.EXTRA_ENTRY) .getString(PluginContract.EXTRA_ENTRY)
check(File(result).canExecute()) assert(File(result).canExecute())
return result return result
} }
@@ -173,7 +174,7 @@ object PluginManager {
do { do {
val path = cursor.getString(0) val path = cursor.getString(0)
val file = File(pluginDir, path) val file = File(pluginDir, path)
check(file.absolutePath.startsWith(pluginDirPath)) assert(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream -> cr.openInputStream(uri.buildUpon().path(path).build()).use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) } file.outputStream().use { outStream -> inStream.copyTo(outStream) }
} }
@@ -24,6 +24,7 @@ import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.signaturesCompat
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val metaData: Bundle protected abstract val metaData: Bundle
@@ -34,6 +35,6 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) } override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG) }
override val packageName: String get() = resolveInfo.resolvePackageName override val packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy { override val trusted by lazy {
app.getPackageInfo(packageName).signatures.any(PluginManager.trustedSignatures::contains) app.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
} }
} }
@@ -22,20 +22,17 @@ package com.github.shadowsocks.preference
import android.support.v7.preference.PreferenceDataStore import android.support.v7.preference.PreferenceDataStore
import com.github.shadowsocks.database.KeyValuePair import com.github.shadowsocks.database.KeyValuePair
import com.github.shadowsocks.database.deleteByIdSafe
import com.github.shadowsocks.database.queryByIdSafe
import com.github.shadowsocks.database.replaceSafe
import com.j256.ormlite.dao.Dao import com.j256.ormlite.dao.Dao
import java.util.HashSet import java.util.HashSet
@Suppress("MemberVisibilityCanPrivate", "unused") @Suppress("MemberVisibilityCanPrivate", "unused")
open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() { open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, String?>) : PreferenceDataStore() {
fun getBoolean(key: String?) = kvPairDao.queryByIdSafe(key)?.boolean fun getBoolean(key: String?) = kvPairDao.queryForId(key)?.boolean
fun getFloat(key: String?) = kvPairDao.queryByIdSafe(key)?.float fun getFloat(key: String?) = kvPairDao.queryForId(key)?.float
fun getInt(key: String?) = kvPairDao.queryByIdSafe(key)?.int fun getInt(key: String?) = kvPairDao.queryForId(key)?.int
fun getLong(key: String?) = kvPairDao.queryByIdSafe(key)?.long fun getLong(key: String?) = kvPairDao.queryForId(key)?.long
fun getString(key: String?) = kvPairDao.queryByIdSafe(key)?.string fun getString(key: String?) = kvPairDao.queryForId(key)?.string
fun getStringSet(key: String?) = kvPairDao.queryByIdSafe(key)?.stringSet fun getStringSet(key: String?) = kvPairDao.queryForId(key)?.stringSet
override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue override fun getBoolean(key: String?, defValue: Boolean) = getBoolean(key) ?: defValue
override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue override fun getFloat(key: String?, defValue: Float) = getFloat(key) ?: defValue
@@ -49,32 +46,32 @@ open class OrmLitePreferenceDataStore(private val kvPairDao: Dao<KeyValuePair, S
fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value) fun putInt(key: String?, value: Int?) = if (value == null) remove(key) else putInt(key, value)
fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value) fun putLong(key: String?, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
kvPairDao.replaceSafe(KeyValuePair(key).put(value)) kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putFloat(key: String?, value: Float) { override fun putFloat(key: String?, value: Float) {
kvPairDao.replaceSafe(KeyValuePair(key).put(value)) kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putInt(key: String?, value: Int) { override fun putInt(key: String?, value: Int) {
kvPairDao.replaceSafe(KeyValuePair(key).put(value)) kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putLong(key: String?, value: Long) { override fun putLong(key: String?, value: Long) {
kvPairDao.replaceSafe(KeyValuePair(key).put(value)) kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putString(key: String?, value: String?) = if (value == null) remove(key) else { override fun putString(key: String?, value: String?) = if (value == null) remove(key) else {
kvPairDao.replaceSafe(KeyValuePair(key).put(value)) kvPairDao.createOrUpdate(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else { override fun putStringSet(key: String?, values: MutableSet<String>?) = if (values == null) remove(key) else {
kvPairDao.replaceSafe(KeyValuePair(key).put(values)) kvPairDao.createOrUpdate(KeyValuePair(key).put(values))
fireChangeListener(key) fireChangeListener(key)
} }
fun remove(key: String?) { fun remove(key: String?) {
kvPairDao.deleteByIdSafe(key) kvPairDao.deleteById(key)
fireChangeListener(key) fireChangeListener(key)
} }
@@ -3,11 +3,14 @@ package com.github.shadowsocks.utils
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.support.annotation.AttrRes import android.support.annotation.AttrRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat
import android.support.v4.os.BuildCompat
import android.support.v7.util.SortedList import android.support.v7.util.SortedList
import android.util.TypedValue import android.util.TypedValue
import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.App.Companion.app
@@ -50,6 +53,12 @@ fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader:
val URLConnection.responseLength: Long val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong() get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
inline fun <reified T> Context.systemService() = ContextCompat.getSystemService(this, T::class.java)!!
val PackageInfo.signaturesCompat get() =
if (BuildCompat.isAtLeastP()) signingCertificateHistory.flatten()
else @Suppress("DEPRECATION") signatures.asIterable()
/** /**
* Based on: https://stackoverflow.com/a/15656428/2245107 * Based on: https://stackoverflow.com/a/15656428/2245107
*/ */
@@ -22,18 +22,20 @@ package com.github.shadowsocks.widget
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.support.design.animation.AnimationConsts
import android.support.design.widget.CoordinatorLayout import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.design.widget.SnackbarAnimation import android.support.design.widget.SnackbarConsts
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import com.github.shadowsocks.utils.systemService
/** /**
* Full credits go to: https://stackoverflow.com/a/35904421/2245107 * Full credits go to: https://stackoverflow.com/a/35904421/2245107
*/ */
class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) { class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val accessibility = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager private val accessibility = context.systemService<AccessibilityManager>()
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean = override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
dependency is Snackbar.SnackbarLayout dependency is Snackbar.SnackbarLayout
@@ -52,8 +54,8 @@ class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorL
val animator = ValueAnimator() val animator = ValueAnimator()
val start = child.height val start = child.height
animator.setIntValues(start, parent.height) animator.setIntValues(start, parent.height)
animator.interpolator = SnackbarAnimation.FAST_OUT_SLOW_IN_INTERPOLATOR animator.interpolator = AnimationConsts.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarAnimation.ANIMATION_DURATION animator.duration = SnackbarConsts.ANIMATION_DURATION
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
animator.addUpdateListener { animator -> animator.addUpdateListener { animator ->
child.layoutParams.height = animator.animatedValue as Int child.layoutParams.height = animator.animatedValue as Int
+1 -1
View File
@@ -5,7 +5,7 @@ apply from: 'gradle-mvn-push.gradle'
android { android {
buildToolsVersion rootProject.buildToolsVersion buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion compileSdkVersion rootProject.compileSdkVersion
defaultConfig { defaultConfig {
minSdkVersion rootProject.minSdkVersion minSdkVersion rootProject.minSdkVersion
@@ -61,7 +61,7 @@ abstract class NativePluginProvider : ContentProvider() {
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
sortOrder: String?): Cursor { sortOrder: String?): Cursor {
check(selection == null && selectionArgs == null && sortOrder == null) assert(selection == null && selectionArgs == null && sortOrder == null)
val result = MatrixCursor(projection) val result = MatrixCursor(projection)
populateFiles(PathProvider(uri, result)) populateFiles(PathProvider(uri, result))
return result return result
@@ -79,7 +79,7 @@ abstract class NativePluginProvider : ContentProvider() {
abstract fun openFile(uri: Uri?): ParcelFileDescriptor abstract fun openFile(uri: Uri?): ParcelFileDescriptor
override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor { override fun openFile(uri: Uri?, mode: String?): ParcelFileDescriptor {
check(mode == "r") assert(mode == "r")
return openFile(uri) return openFile(uri)
} }