mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-01-17 07:12:32 -08:00
Reorganize app source code
This commit is contained in:
@@ -1,267 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.InverseBindingAdapter
|
||||
import androidx.databinding.InverseBindingListener
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.*
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ktx.replaceRandomWithSpecial
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@BindingAdapter("onNavigationClick")
|
||||
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
|
||||
view.setNavigationOnClickListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
|
||||
view.setImageResource(resId)
|
||||
}
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
fun setImageResource(view: ImageView, drawable: Drawable) {
|
||||
view.setImageDrawable(drawable)
|
||||
}
|
||||
|
||||
@BindingAdapter("movieBehavior", "movieBehaviorText")
|
||||
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
|
||||
(view.tag as? Job)?.cancel()
|
||||
view.tag = null
|
||||
if (isMovieBehavior) {
|
||||
view.tag = GlobalScope.launch(Dispatchers.Main.immediate) {
|
||||
while (true) {
|
||||
delay(150)
|
||||
view.text = text.replaceRandomWithSpecial()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view.text = text
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("onTouch")
|
||||
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
|
||||
view.setOnTouchListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("scrollToLast")
|
||||
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
|
||||
|
||||
fun scrollToLast() = UiThreadHandler.handler.postDelayed({
|
||||
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
|
||||
}, 30)
|
||||
|
||||
fun wait(callback: () -> Unit) {
|
||||
UiThreadHandler.handler.postDelayed(callback, 1000)
|
||||
}
|
||||
|
||||
fun RecyclerView.Adapter<*>.setListener() {
|
||||
val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
scrollToLast()
|
||||
}
|
||||
}
|
||||
registerAdapterDataObserver(observer)
|
||||
view.setTag(R.id.recyclerScrollListener, observer)
|
||||
}
|
||||
|
||||
fun RecyclerView.Adapter<*>.removeListener() {
|
||||
val observer =
|
||||
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
|
||||
unregisterAdapterDataObserver(observer)
|
||||
}
|
||||
|
||||
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
|
||||
|
||||
if (shouldScrollToLast) {
|
||||
trySetListener()
|
||||
} else {
|
||||
view.adapter?.removeListener()
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("isEnabled")
|
||||
fun setEnabled(view: View, isEnabled: Boolean) {
|
||||
view.isEnabled = isEnabled
|
||||
}
|
||||
|
||||
@BindingAdapter("error")
|
||||
fun TextInputLayout.setErrorString(error: String) {
|
||||
val newError = error.let { if (it.isEmpty()) null else it }
|
||||
if (this.error == null && newError == null) return
|
||||
this.error = newError
|
||||
}
|
||||
|
||||
// md2
|
||||
|
||||
@BindingAdapter(
|
||||
"android:layout_marginLeft",
|
||||
"android:layout_marginTop",
|
||||
"android:layout_marginRight",
|
||||
"android:layout_marginBottom",
|
||||
"android:layout_marginStart",
|
||||
"android:layout_marginEnd",
|
||||
requireAll = false
|
||||
)
|
||||
fun View.setMargins(
|
||||
marginLeft: Int?,
|
||||
marginTop: Int?,
|
||||
marginRight: Int?,
|
||||
marginBottom: Int?,
|
||||
marginStart: Int?,
|
||||
marginEnd: Int?
|
||||
) = updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
marginLeft?.let { leftMargin = it }
|
||||
marginTop?.let { topMargin = it }
|
||||
marginRight?.let { rightMargin = it }
|
||||
marginBottom?.let { bottomMargin = it }
|
||||
marginStart?.let { this.marginStart = it }
|
||||
marginEnd?.let { this.marginEnd = it }
|
||||
}
|
||||
|
||||
@BindingAdapter("nestedScrollingEnabled")
|
||||
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
|
||||
isNestedScrollingEnabled = enabled
|
||||
}
|
||||
|
||||
@BindingAdapter("isSelected")
|
||||
fun View.isSelected(isSelected: Boolean) {
|
||||
this.isSelected = isSelected
|
||||
}
|
||||
|
||||
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
|
||||
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
|
||||
if (dividerHorizontal != null) {
|
||||
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
|
||||
setDrawable(dividerHorizontal)
|
||||
}.let { addItemDecoration(it) }
|
||||
}
|
||||
if (dividerVertical != null) {
|
||||
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
|
||||
setDrawable(dividerVertical)
|
||||
}.let { addItemDecoration(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("app:icon")
|
||||
fun Button.setIconRes(res: Int) {
|
||||
(this as MaterialButton).setIconResource(res)
|
||||
}
|
||||
|
||||
@BindingAdapter("strokeWidth")
|
||||
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
|
||||
strokeWidth = stroke.roundToInt()
|
||||
}
|
||||
|
||||
@BindingAdapter("onMenuClick")
|
||||
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
|
||||
setOnMenuItemClickListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("onCloseClicked")
|
||||
fun Chip.setOnCloseClickedListenerBinding(listener: View.OnClickListener) {
|
||||
setOnCloseIconClickListener(listener)
|
||||
}
|
||||
|
||||
@BindingAdapter("progressAnimated")
|
||||
fun ProgressBar.setProgressAnimated(newProgress: Int) {
|
||||
val animator = tag as? ValueAnimator
|
||||
animator?.cancel()
|
||||
|
||||
ValueAnimator.ofInt(progress, newProgress).apply {
|
||||
interpolator = FastOutSlowInInterpolator()
|
||||
addUpdateListener { progress = it.animatedValue as Int }
|
||||
tag = this
|
||||
}.start()
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setTextSafe(text: Int) {
|
||||
if (text == 0) this.text = null else setText(text)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:onLongClick")
|
||||
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
|
||||
setOnLongClickListener {
|
||||
listener()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("strikeThrough")
|
||||
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
|
||||
paintFlags = if (useStrikeThrough) {
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
} else {
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
||||
}
|
||||
}
|
||||
|
||||
interface OnPopupMenuItemClickListener {
|
||||
fun onMenuItemClick(itemId: Int)
|
||||
}
|
||||
|
||||
@BindingAdapter("popupMenu", "popupMenuOnClickListener", requireAll = false)
|
||||
fun View.setPopupMenu(popupMenu: Int, listener: OnPopupMenuItemClickListener) {
|
||||
val menu = tag as? PopupMenu ?: let {
|
||||
val themeWrapper = ContextThemeWrapper(context, R.style.Foundation_PopupMenu)
|
||||
PopupMenu(themeWrapper, this)
|
||||
}
|
||||
tag = menu.apply {
|
||||
this.menu.clear()
|
||||
menuInflater.inflate(popupMenu, this.menu)
|
||||
setOnMenuItemClickListener {
|
||||
listener.onMenuItemClick(it.itemId)
|
||||
true
|
||||
}
|
||||
}
|
||||
setOnClickListener {
|
||||
(tag as PopupMenu).show()
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("spanCount")
|
||||
fun RecyclerView.setSpanCount(count: Int) {
|
||||
when (val lama = layoutManager) {
|
||||
is GridLayoutManager -> lama.spanCount = count
|
||||
is StaggeredGridLayoutManager -> lama.spanCount = count
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("state")
|
||||
fun setState(view: IndeterminateCheckBox, state: Boolean?) {
|
||||
if (view.state != state)
|
||||
view.state = state
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "state")
|
||||
fun getState(view: IndeterminateCheckBox) = view.state
|
||||
|
||||
@BindingAdapter("stateAttrChanged")
|
||||
fun setListeners(
|
||||
view: IndeterminateCheckBox,
|
||||
attrChange: InverseBindingListener
|
||||
) {
|
||||
view.setOnStateChangedListener { _, _ ->
|
||||
attrChange.onChange()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.topjohnwu.magisk.ui.base.ViewEvent
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
|
||||
class EndlessRecyclerScrollListener(
|
||||
private val layoutManager: RecyclerView.LayoutManager,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.net.Uri
|
||||
import android.text.Spanned
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.caverock.androidsvg.SVG
|
||||
import com.caverock.androidsvg.SVGParseException
|
||||
import com.topjohnwu.magisk.core.ResMgr
|
||||
import com.topjohnwu.superuser.internal.WaitRunnable
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.MarkwonSpansFactory
|
||||
import io.noties.markwon.image.*
|
||||
import io.noties.markwon.image.data.DataUriSchemeHandler
|
||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.OkHttpClient
|
||||
import org.commonmark.node.Image
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
// Differences with Markwon stock ImagePlugin:
|
||||
//
|
||||
// We assume beforeSetText() will be run in a background thread, and in that method
|
||||
// we download/decode all drawables before sending the spanned markdown CharSequence
|
||||
// to the next stage. We also get our surrounding TextView width to properly
|
||||
// resize our images.
|
||||
//
|
||||
// This is required for PrecomputedText to properly take the images into account
|
||||
// when precomputing the metrics of TextView
|
||||
//
|
||||
// Basically, we want nothing to do with AsyncDrawable
|
||||
class MarkwonImagePlugin(okHttp: OkHttpClient) : AbstractMarkwonPlugin() {
|
||||
|
||||
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
||||
builder.setFactory(Image::class.java) { _, props ->
|
||||
val dest = ImageProps.DESTINATION.require(props)
|
||||
val size = ImageProps.IMAGE_SIZE.get(props)
|
||||
ImageSpan(dest, size)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun beforeSetText(tv: TextView, markdown: Spanned) {
|
||||
if (markdown.isEmpty())
|
||||
return
|
||||
|
||||
val spans = markdown.getSpans(0, markdown.length, ImageSpan::class.java)
|
||||
if (spans == null || spans.isEmpty())
|
||||
return
|
||||
|
||||
// Get TextView sizes before setText() to resize all images
|
||||
val wr = WaitRunnable {
|
||||
val width = tv.width - tv.paddingLeft - tv.paddingRight
|
||||
spans.forEach { it.canvasWidth = width }
|
||||
}
|
||||
tv.post(wr)
|
||||
|
||||
runBlocking {
|
||||
// Wait for drawable to be set
|
||||
spans.forEach { it.await() }
|
||||
// Wait for canvasWidth to be set
|
||||
wr.waitUntilDone()
|
||||
}
|
||||
}
|
||||
|
||||
private val schemeHandlers = HashMap<String, SchemeHandler>(3)
|
||||
private val mediaDecoders = HashMap<String, MediaDecoder>(0)
|
||||
private val defaultMediaDecoder = DefaultMediaDecoder.create()
|
||||
|
||||
init {
|
||||
addSchemeHandler(DataUriSchemeHandler.create())
|
||||
addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttp))
|
||||
addMediaDecoder(SVGDecoder())
|
||||
}
|
||||
|
||||
private fun addSchemeHandler(schemeHandler: SchemeHandler) {
|
||||
for (scheme in schemeHandler.supportedSchemes()) {
|
||||
schemeHandlers[scheme] = schemeHandler
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMediaDecoder(mediaDecoder: MediaDecoder) {
|
||||
for (type in mediaDecoder.supportedTypes()) {
|
||||
mediaDecoders[type] = mediaDecoder
|
||||
}
|
||||
}
|
||||
|
||||
// Modified from AsyncDrawableLoaderImpl.execute(asyncDrawable)
|
||||
fun loadDrawable(destination: String): Drawable? {
|
||||
val uri = Uri.parse(destination)
|
||||
var drawable: Drawable? = null
|
||||
|
||||
try {
|
||||
val scheme = uri.scheme
|
||||
check(scheme != null && scheme.isNotEmpty()) {
|
||||
"No scheme is found: $destination"
|
||||
}
|
||||
|
||||
// obtain scheme handler
|
||||
val schemeHandler = schemeHandlers[scheme]
|
||||
?: throw IllegalStateException("No scheme-handler is found: $destination")
|
||||
|
||||
// handle scheme
|
||||
val imageItem = schemeHandler.handle(destination, uri)
|
||||
|
||||
// if resulting imageItem needs further decoding -> proceed
|
||||
drawable = if (imageItem.hasDecodingNeeded()) {
|
||||
val withDecodingNeeded = imageItem.asWithDecodingNeeded
|
||||
val mediaDecoder = mediaDecoders[withDecodingNeeded.contentType()]
|
||||
?: defaultMediaDecoder
|
||||
mediaDecoder.decode(
|
||||
withDecodingNeeded.contentType(),
|
||||
withDecodingNeeded.inputStream()
|
||||
)
|
||||
} else {
|
||||
imageItem.asWithResult.result()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Error loading image: $destination")
|
||||
}
|
||||
|
||||
// apply intrinsic bounds (but only if they are empty)
|
||||
if (drawable != null && drawable.bounds.isEmpty)
|
||||
DrawableUtils.applyIntrinsicBounds(drawable)
|
||||
|
||||
return drawable
|
||||
}
|
||||
|
||||
inner class ImageSpan(
|
||||
dest: String,
|
||||
private val size: ImageSize?
|
||||
) : DynamicDrawableSpan(ALIGN_BOTTOM) {
|
||||
|
||||
var canvasWidth = 0
|
||||
private var measured = false
|
||||
private lateinit var draw: Drawable
|
||||
private val job: Job
|
||||
|
||||
init {
|
||||
// Asynchronously download/decode images in the background
|
||||
job = GlobalScope.launch(Dispatchers.IO) {
|
||||
draw = loadDrawable(dest) ?: ShapeDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await() = job.join()
|
||||
|
||||
override fun getDrawable() = draw
|
||||
|
||||
private fun defaultBounds(): Rect {
|
||||
val bounds: Rect = draw.bounds
|
||||
if (!bounds.isEmpty) {
|
||||
return bounds
|
||||
}
|
||||
val intrinsicBounds = DrawableUtils.intrinsicBounds(draw)
|
||||
if (!intrinsicBounds.isEmpty) {
|
||||
return intrinsicBounds
|
||||
}
|
||||
return Rect(0, 0, 1, 1)
|
||||
}
|
||||
|
||||
private fun measure(paint: Paint) {
|
||||
if (measured || canvasWidth == 0)
|
||||
return
|
||||
measured = true
|
||||
val bound =
|
||||
SizeResolver.resolveImageSize(size, defaultBounds(), canvasWidth, paint.textSize)
|
||||
draw.bounds = bound
|
||||
}
|
||||
|
||||
override fun getSize(
|
||||
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
|
||||
): Int {
|
||||
measure(paint)
|
||||
return super.getSize(paint, text, start, end, fm)
|
||||
}
|
||||
}
|
||||
|
||||
object SizeResolver : ImageSizeResolverDef() {
|
||||
// Expose protected API
|
||||
public override fun resolveImageSize(
|
||||
imageSize: ImageSize?,
|
||||
imageBounds: Rect,
|
||||
canvasWidth: Int,
|
||||
textSize: Float
|
||||
): Rect {
|
||||
return super.resolveImageSize(imageSize, imageBounds, canvasWidth, textSize)
|
||||
}
|
||||
}
|
||||
|
||||
class SVGDecoder: MediaDecoder() {
|
||||
override fun supportedTypes() = listOf("image/svg+xml")
|
||||
|
||||
override fun decode(contentType: String?, inputStream: InputStream): Drawable {
|
||||
val svg = try {
|
||||
SVG.getFromInputStream(inputStream)
|
||||
} catch (e: SVGParseException) {
|
||||
throw IllegalStateException("Exception decoding SVG", e)
|
||||
}
|
||||
|
||||
val w = svg.documentWidth
|
||||
val h = svg.documentHeight
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
val picture = svg.renderToPicture()
|
||||
return PictureDrawable(picture)
|
||||
}
|
||||
|
||||
val density: Float = ResMgr.resource.displayMetrics.density
|
||||
|
||||
val width = (w * density + .5f).toInt()
|
||||
val height = (h * density + .5f).toInt()
|
||||
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.scale(density, density)
|
||||
svg.renderToCanvas(canvas)
|
||||
|
||||
return BitmapDrawable(ResMgr.resource, bitmap)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt
Normal file
49
app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
|
||||
object Utils {
|
||||
|
||||
fun toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), msg, duration).show() }
|
||||
}
|
||||
|
||||
fun toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Info.env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
|
||||
}
|
||||
|
||||
fun openLink(context: Context, link: Uri) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
toast(
|
||||
R.string.open_link_failed_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDownloadPath(path: String) =
|
||||
File(Environment.getExternalStorageDirectory(), path).run {
|
||||
if ((exists() && isDirectory) || mkdirs()) this else null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils.net
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkInfo
|
||||
import android.net.NetworkInfo.DetailedState
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
// Recreate NetworkInfo with Kotlin data class
|
||||
data class Connectivity(
|
||||
val state : NetworkInfo.State = NetworkInfo.State.DISCONNECTED,
|
||||
val detailedState : DetailedState = DetailedState.IDLE,
|
||||
val type : Int = -1,
|
||||
val subType : Int = -1,
|
||||
val isAvailable : Boolean = false,
|
||||
val isFailover : Boolean = false,
|
||||
val isRoaming : Boolean = false,
|
||||
val typeName : String = "NONE",
|
||||
val subTypeName : String = "NONE",
|
||||
val reason : String? = null,
|
||||
val extraInfo : String? = null
|
||||
) {
|
||||
private constructor(info: NetworkInfo) : this(
|
||||
info.state,
|
||||
info.detailedState,
|
||||
info.type,
|
||||
info.subtype,
|
||||
info.isAvailable,
|
||||
info.isFailover,
|
||||
info.isRoaming,
|
||||
info.typeName,
|
||||
info.subtypeName,
|
||||
info.reason,
|
||||
info.extraInfo
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(manager: ConnectivityManager): Connectivity {
|
||||
return manager.activeNetworkInfo?.let { Connectivity(it) } ?: Connectivity()
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
fun create(manager: ConnectivityManager, network: Network): Connectivity {
|
||||
return manager.getNetworkInfo(network)?.let { Connectivity(it) } ?: Connectivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.topjohnwu.magisk.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
|
||||
@TargetApi(21)
|
||||
open class LollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: NetInfoCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val networkCallback = NetCallback()
|
||||
|
||||
init {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
manager.registerNetworkCallback(request, networkCallback)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
manager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
emit(Connectivity.create(manager, network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
emit(Connectivity.create(manager, network))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.NetworkInfo
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
@TargetApi(23)
|
||||
class MarshmallowNetworkObserver(
|
||||
context: Context,
|
||||
callback: NetInfoCallback
|
||||
): LollipopNetworkObserver(context, callback) {
|
||||
|
||||
private val idleReceiver = IdleBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
context.registerReceiver(idleReceiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
super.stopObserving()
|
||||
context.unregisterReceiver(idleReceiver)
|
||||
}
|
||||
|
||||
override fun emit(current: Connectivity) {
|
||||
val typeChanged = last.type != current.type
|
||||
val wasConnected = last.state == NetworkInfo.State.CONNECTED
|
||||
val isDisconnected = current.state == NetworkInfo.State.DISCONNECTED
|
||||
val isNotIdle = current.detailedState != NetworkInfo.DetailedState.IDLE
|
||||
if (typeChanged && wasConnected && isDisconnected && isNotIdle) {
|
||||
super.emit(current)
|
||||
super.emit(last)
|
||||
last = current
|
||||
} else {
|
||||
super.emit(current)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
||||
|
||||
private fun isIdleMode(context: Context): Boolean {
|
||||
val packageName = context.packageName
|
||||
val manager = context.getSystemService<PowerManager>()!!
|
||||
val isIgnoringOptimizations = manager.isIgnoringBatteryOptimizations(packageName)
|
||||
return manager.isDeviceIdleMode && !isIgnoringOptimizations
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (isIdleMode(context)) {
|
||||
emit(Connectivity())
|
||||
} else {
|
||||
emit(Connectivity.create(manager))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.topjohnwu.magisk.utils.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
typealias NetInfoCallback = (Connectivity) -> Unit
|
||||
|
||||
abstract class NetworkObserver protected constructor(
|
||||
context: Context,
|
||||
private val callback: NetInfoCallback
|
||||
) {
|
||||
|
||||
protected val context = context.applicationContext
|
||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
protected var last = Connectivity.create(manager)
|
||||
|
||||
init {
|
||||
callback(last)
|
||||
}
|
||||
|
||||
protected open fun emit(current: Connectivity) {
|
||||
if (last != current)
|
||||
callback(current)
|
||||
last = current
|
||||
}
|
||||
|
||||
protected abstract fun stopObserving()
|
||||
|
||||
companion object {
|
||||
fun observe(context: Context, callback: NetInfoCallback): NetworkObserver {
|
||||
return when (Build.VERSION.SDK_INT) {
|
||||
in 23 until Int.MAX_VALUE -> MarshmallowNetworkObserver(context, callback)
|
||||
in 21 until 23 -> LollipopNetworkObserver(context, callback)
|
||||
else -> PreLollipopNetworkObserver(context, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.utils.net
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
|
||||
class PreLollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: NetInfoCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val receiver = ConnectivityBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
context.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
private inner class ConnectivityBroadcastReceiver: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
emit(Connectivity.create(manager))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user