____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.ktx
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.*
import android.os.Build
import android.system.Os
import android.system.OsConstants
import android.util.TypedValue
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.AttrRes
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.ui.MainActivity
import io.nekohasekai.sagernet.ui.ThemedActivity
import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.Continuation
import kotlin.reflect.KMutableProperty0
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
import androidx.core.view.isVisible
import androidx.core.view.isGone
inline fun Iterable.forEachTry(action: (T) -> Unit) {
var result: Exception? = null
for (element in this) try {
action(element)
} catch (e: Exception) {
if (result == null) result = e else result.addSuppressed(e)
}
if (result != null) {
throw result
}
}
val Throwable.readableMessage: String
get() = localizedMessage.takeIf { !it.isNullOrBlank() } ?: javaClass.simpleName
fun parsePort(str: String?, default: Int, min: Int = 0): Int {
val value = str?.toIntOrNull() ?: default
return if (value !in min..65535) default else value
}
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
}
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
callback()
if (onetime) context.unregisterReceiver(this)
}
}.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
})
}
}
fun Preference.remove() = parent!!.removePreference(this)
/**
* A slightly more performant variant of parseNumericAddress.
*
* Bug in Android 9.0 and lower: https://issuetracker.google.com/issues/123456213
*/
private val parseNumericAddress by lazy {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true
}
}
fun String?.parseNumericAddress(): InetAddress? =
Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let {
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(
null, this
) as InetAddress
}
@JvmOverloads
fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String? = null) {
if (!fragmentManager.isStateSaved) show(fragmentManager, tag)
}
fun RecyclerView.scrollTo(index: Int, force: Boolean = false) {
if (force) post {
scrollToPosition(index)
}
postDelayed({
try {
layoutManager?.startSmoothScroll(object : LinearSmoothScroller(context) {
init {
targetPosition = index
}
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
})
} catch (_: IllegalArgumentException) {
}
}, 300L)
}
val app get() = SagerNet.application
val shortAnimTime by lazy {
app.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
}
fun View.crossFadeFrom(other: View) {
clearAnimation()
other.clearAnimation()
if (isVisible && other.isGone) return
alpha = 0F
visibility = View.VISIBLE
animate().alpha(1F).duration = shortAnimTime
other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
other.visibility = View.GONE
}
}).duration = shortAnimTime
}
fun Fragment.snackbar(textId: Int) = (requireActivity() as MainActivity).snackbar(textId)
fun Fragment.snackbar(text: CharSequence) = (requireActivity() as MainActivity).snackbar(text)
fun ThemedActivity.startFilesForResult(
launcher: ActivityResultLauncher, input: String
) {
try {
return launcher.launch(input)
} catch (_: ActivityNotFoundException) {
} catch (_: SecurityException) {
}
snackbar(getString(R.string.file_manager_missing)).show()
}
fun Fragment.startFilesForResult(
launcher: ActivityResultLauncher, input: String
) {
try {
return launcher.launch(input)
} catch (_: ActivityNotFoundException) {
} catch (_: SecurityException) {
}
(requireActivity() as ThemedActivity).snackbar(getString(R.string.file_manager_missing)).show()
}
fun Fragment.needReload() {
if (SagerNet.started) {
snackbar(getString(R.string.restart)).setAction(R.string.apply) {
SagerNet.reloadService()
}.show()
}
}
fun Context.getColour(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
fun Context.getColorAttr(@AttrRes resId: Int): Int {
return ContextCompat.getColor(this, TypedValue().also {
theme.resolveAttribute(resId, it, true)
}.resourceId)
}
fun Continuation.tryResume(value: T) {
try {
resumeWith(Result.success(value))
} catch (_: IllegalStateException) {
}
}
fun Continuation.tryResumeWithException(exception: Throwable) {
try {
resumeWith(Result.failure(exception))
} catch (_: IllegalStateException) {
}
}
operator fun KProperty0.getValue(thisRef: Any?, property: KProperty<*>): F = get()
operator fun KMutableProperty0.setValue(
thisRef: Any?, property: KProperty<*>, value: F
) = set(value)
operator fun AtomicBoolean.getValue(thisRef: Any?, property: KProperty<*>): Boolean = get()
operator fun AtomicBoolean.setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) =
set(value)
operator fun AtomicInteger.getValue(thisRef: Any?, property: KProperty<*>): Int = get()
operator fun AtomicInteger.setValue(thisRef: Any?, property: KProperty<*>, value: Int) = set(value)
operator fun AtomicLong.getValue(thisRef: Any?, property: KProperty<*>): Long = get()
operator fun AtomicLong.setValue(thisRef: Any?, property: KProperty<*>, value: Long) = set(value)
operator fun AtomicReference.getValue(thisRef: Any?, property: KProperty<*>): T = get()
operator fun AtomicReference.setValue(thisRef: Any?, property: KProperty<*>, value: T) =
set(value)
operator fun Map.getValue(thisRef: K, property: KProperty<*>) = get(thisRef)
operator fun MutableMap.setValue(thisRef: K, property: KProperty<*>, value: V?) {
if (value != null) {
put(thisRef, value)
} else {
remove(thisRef)
}
}