Reorganize app source code

This commit is contained in:
topjohnwu
2020-08-18 06:31:15 -07:00
parent d7a26dbf27
commit 846bbb4da1
77 changed files with 158 additions and 191 deletions

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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)
}
}
}

View 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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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))
}
}
}