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
23 changed files with 146 additions and 44 deletions
+5 -4
View File
@@ -6,9 +6,10 @@ buildscript {
ext {
kotlinVersion = '1.2.30'
minSdkVersion = 21
sdkVersion = 27
buildToolsVersion = '27.0.3'
supportLibraryVersion = '27.1.0'
sdkVersion = 'P'
compileSdkVersion = 'android-P'
buildToolsVersion = '28-rc1'
supportLibraryVersion = '28.0.0-alpha1'
takisoftFixVersion = '27.1.0.0'
playServicesVersion = '11.8.0'
junitVersion = '4.12'
@@ -22,7 +23,7 @@ buildscript {
}
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.google.gms:google-services:3.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+1 -1
View File
@@ -4,7 +4,7 @@ apply plugin: 'com.android.library'
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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
+1 -1
View File
@@ -16,7 +16,7 @@ def getCurrentFlavor() {
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
applicationId "com.github.shadowsocks"
minSdkVersion rootProject.minSdkVersion
+1
View File
@@ -3,6 +3,7 @@
package="com.github.shadowsocks"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.NFC" />
@@ -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.Looper
import android.support.annotation.RequiresApi
import android.support.v4.os.BuildCompat
import android.support.v4.os.UserManagerCompat
import android.support.v7.app.AppCompatDelegate
import android.util.Log
@@ -73,8 +74,8 @@ class App : Application() {
private val tracker: Tracker by lazy { GoogleAnalytics.getInstance(deviceContext).newTracker(R.xml.tracker) }
val info: PackageInfo by lazy { getPackageInfo(packageName) }
fun getPackageInfo(packageName: String) =
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)!!
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, if (BuildCompat.isAtLeastP())
PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() {
val intent = Intent(this, BaseService.serviceClass.java)
@@ -27,7 +27,6 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
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.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.thread
import com.github.shadowsocks.utils.*
import java.util.concurrent.atomic.AtomicBoolean
class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
@@ -142,7 +138,7 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
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) {
proxiedApps = str.split('\n').toHashSet()
@@ -45,6 +45,7 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
@@ -305,7 +306,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
private var txTotal: 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(id: Int) = startActivity(Intent(context, ProfileConfigActivity::class.java)
.putExtra(Action.EXTRA_PROFILE_ID, id))
@@ -21,7 +21,6 @@
package com.github.shadowsocks
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutManager
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.ProfileManager
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.BarcodeGraphic
import com.google.android.gms.vision.Frame
@@ -65,7 +65,7 @@ class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, Ba
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
if (try {
(getSystemService(Context.CAMERA_SERVICE) as CameraManager).cameraIdList.isEmpty()
systemService<CameraManager>().cameraIdList.isEmpty()
} catch (_: CameraAccessException) {
true
}) {
@@ -22,7 +22,6 @@ package com.github.shadowsocks
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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.bg.BaseService
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
companion object {
@@ -48,8 +48,7 @@ class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface
finish()
return
}
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (km.inKeyguardRestrictedInputMode()) {
if (systemService<KeyguardManager>().inKeyguardRestrictedInputMode()) {
receiver = broadcastReceiver { _, _ -> connection.connect() }
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else connection.connect()
@@ -22,7 +22,6 @@ package com.github.shadowsocks.acl
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.Bundle
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.asIterable
import com.github.shadowsocks.utils.resolveResourceId
import com.github.shadowsocks.utils.systemService
import com.github.shadowsocks.widget.UndoSnackbarManager
import java.net.IDN
import java.net.MalformedURLException
@@ -360,7 +360,7 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener,
private lateinit var list: RecyclerView
private var mode: ActionMode? = null
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() {
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
@@ -354,7 +354,7 @@ object BaseService {
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(app.info.signatures[0].toByteArray())
mdg.update(app.info.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder()
.add("sig", String(Base64.encode(mdg.digest(), 0)))
.build()
@@ -36,6 +36,7 @@ import com.github.shadowsocks.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.systemService
import java.util.*
/**
@@ -46,7 +47,7 @@ import java.util.*
*/
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
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 {
(service as Context).getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
@@ -82,7 +83,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
service as Context
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))
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)
val screenFilter = IntentFilter()
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
@@ -21,7 +21,6 @@
package com.github.shadowsocks.bg
import android.app.KeyguardManager
import android.content.Context
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
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.IShadowsocksServiceCallback
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.systemService
import android.service.quicksettings.TileService as BaseTileService
@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 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(Context.KEYGUARD_SERVICE) as KeyguardManager }
private val keyguard by lazy { systemService<KeyguardManager>() }
override val serviceCallback: IShadowsocksServiceCallback.Stub by lazy {
@RequiresApi(24)
@@ -20,12 +20,15 @@
package com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.LocalSocket
import android.net.*
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.support.v4.os.BuildCompat
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.JniHelper
@@ -36,6 +39,7 @@ import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.systemService
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
@@ -50,6 +54,20 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private const val PRIVATE_VLAN6 = "fdfe:dcba:9876::%s"
private val getInt: Method = FileDescriptor::class.java.getDeclaredMethod("getInt$")
/**
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private val defaultNetworkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build()
}
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") {
@@ -90,6 +108,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var worker: ProtectWorker? = null
private var tun2socksProcess: GuardedProcess? = null
private val connectivity by lazy { systemService<ConnectivityManager>() }
@TargetApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
setUnderlyingNetworks(arrayOf(network))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
// it's a good idea to refresh capabilities
setUnderlyingNetworks(arrayOf(network))
}
override fun onLost(network: Network) {
setUnderlyingNetworks(null)
}
}
private var listeningForDefaultNetwork = false
override fun onBind(intent: Intent): IBinder? = when (intent.action) {
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
else -> super<LocalDnsService.Interface>.onBind(intent)
@@ -98,6 +132,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun onRevoke() = stopRunner(true)
override fun killProcesses() {
if (BuildCompat.isAtLeastP() && listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false
}
worker?.stopThread()
worker = null
super.killProcesses()
@@ -179,6 +217,13 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
this.conn = conn
val fd = conn.fd
// We only need to update since Android P: https://android.googlesource.com/platform/frameworks/base/+/72f9c42b9e59761a28d6b32c42f65de57c98daed
if (BuildCompat.isAtLeastP()) {
// we want REQUEST here instead of LISTEN
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
listeningForDefaultNetwork = true
}
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN.format(Locale.ENGLISH, "2"),
"--netif-netmask", "255.255.255.0",
@@ -32,6 +32,7 @@ import android.util.Base64
import android.util.Log
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.signaturesCompat
import eu.chainfire.libsuperuser.Shell
import java.io.File
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.
*/
val trustedSignatures by lazy {
app.info.signatures.toSet() +
app.info.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
@@ -24,6 +24,7 @@ import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Bundle
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.utils.signaturesCompat
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
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 packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy {
app.getPackageInfo(packageName).signatures.any(PluginManager.trustedSignatures::contains)
app.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
}
}
@@ -3,11 +3,14 @@ package com.github.shadowsocks.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.res.Resources
import android.os.Build
import android.support.annotation.AttrRes
import android.support.v4.app.Fragment
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.util.TypedValue
import com.github.shadowsocks.App.Companion.app
@@ -50,6 +53,12 @@ fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader:
val URLConnection.responseLength: Long
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
*/
@@ -22,18 +22,20 @@ package com.github.shadowsocks.widget
import android.animation.ValueAnimator
import android.content.Context
import android.support.design.animation.AnimationConsts
import android.support.design.widget.CoordinatorLayout
import android.support.design.widget.Snackbar
import android.support.design.widget.SnackbarAnimation
import android.support.design.widget.SnackbarConsts
import android.util.AttributeSet
import android.view.View
import android.view.accessibility.AccessibilityManager
import com.github.shadowsocks.utils.systemService
/**
* Full credits go to: https://stackoverflow.com/a/35904421/2245107
*/
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 =
dependency is Snackbar.SnackbarLayout
@@ -52,8 +54,8 @@ class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorL
val animator = ValueAnimator()
val start = child.height
animator.setIntValues(start, parent.height)
animator.interpolator = SnackbarAnimation.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarAnimation.ANIMATION_DURATION
animator.interpolator = AnimationConsts.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarConsts.ANIMATION_DURATION
@Suppress("NAME_SHADOWING")
animator.addUpdateListener { animator ->
child.layoutParams.height = animator.animatedValue as Int
+1 -1
View File
@@ -5,7 +5,7 @@ apply from: 'gradle-mvn-push.gradle'
android {
buildToolsVersion rootProject.buildToolsVersion
compileSdkVersion rootProject.sdkVersion
compileSdkVersion rootProject.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.minSdkVersion