diff --git a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/settings/SettingsPreferences.kt b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/settings/SettingsPreferences.kt index 31d329ae4..50c556a8d 100644 --- a/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/settings/SettingsPreferences.kt +++ b/pluto-plugins/base/lib/src/main/java/com/pluto/utilities/settings/SettingsPreferences.kt @@ -29,9 +29,29 @@ object SettingsPreferences { get() = settingsPrefs.getInt(GRID_SIZE, 5) set(value) = settingsPrefs.edit().putInt(GRID_SIZE, value).apply() + var bandWidthLimitUploadMbps: Long + get() = settingsPrefs.getLong(BANDWIDTH_LIMIT_UPLOAD, Long.MAX_VALUE) + set(value) = settingsPrefs.edit().putLong(BANDWIDTH_LIMIT_UPLOAD, value).apply() + + var bandWidthLimitDownloadMbps: Long + get() = settingsPrefs.getLong(BANDWIDTH_LIMIT_DOWNLOAD, Long.MAX_VALUE) + set(value) = settingsPrefs.edit().putLong(BANDWIDTH_LIMIT_DOWNLOAD, value).apply() + + var bandWidthDnsResolutionDelay: Long + get() = settingsPrefs.getLong(BANDWIDTH_LIMIT_DNS_RESOLUTION_DELAY, 0) + set(value) = settingsPrefs.edit().putLong(BANDWIDTH_LIMIT_DNS_RESOLUTION_DELAY, value).apply() + + var isBandwidthLimitEnabled: Boolean + get() = settingsPrefs.getBoolean(IS_BANDWIDTH_LIMIT_ENABLED, false) + set(value) = settingsPrefs.edit().putBoolean(IS_BANDWIDTH_LIMIT_ENABLED, value).apply() + private const val IS_DARK_THEME_ENABLED = "is_dark_theme_enabled" private const val IS_RIGHT_HANDED_ACCESS_POPUP = "is_right_handed_access_popup" private const val GRID_SIZE = "grid_size" + private const val BANDWIDTH_LIMIT_UPLOAD = "bandwidth_limit_upload" + private const val BANDWIDTH_LIMIT_DOWNLOAD = "bandwidth_limit_download" + private const val BANDWIDTH_LIMIT_DNS_RESOLUTION_DELAY = "bandwidth_limit_dns_resolution_delay" + private const val IS_BANDWIDTH_LIMIT_ENABLED = "is_bandwidth_limit_enabled" } private fun Context.preferences(name: String, mode: Int = Context.MODE_PRIVATE) = getSharedPreferences(name, mode) diff --git a/pluto-plugins/plugins/network/lib/build.gradle b/pluto-plugins/plugins/network/lib/build.gradle index 66f53308b..39d62a428 100644 --- a/pluto-plugins/plugins/network/lib/build.gradle +++ b/pluto-plugins/plugins/network/lib/build.gradle @@ -23,6 +23,7 @@ android { buildFeatures { viewBinding true + dataBinding true } @@ -74,4 +75,5 @@ dependencies { implementation 'androidx.browser:browser:1.4.0' testImplementation 'junit:junit:4.13.2' + testImplementation("com.squareup.okhttp3:mockwebserver:5.0.0-alpha.10") } \ No newline at end of file diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/PlutoNetwork.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/PlutoNetwork.kt index aaca11626..bf3428c19 100644 --- a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/PlutoNetwork.kt +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/PlutoNetwork.kt @@ -1,12 +1,20 @@ package com.pluto.plugins.network import android.content.Context +import com.pluto.plugins.network.internal.bandwidth.core.BandwidthDefaults +import com.pluto.plugins.network.internal.bandwidth.core.BandwidthLimitSocketFactory +import com.pluto.plugins.network.internal.bandwidth.core.DnsDelay +import com.pluto.plugins.network.internal.bandwidth.core.ThrottledInputStream +import com.pluto.plugins.network.internal.bandwidth.core.ThrottledOutputStream import com.pluto.plugins.network.internal.interceptor.logic.ApiCallData import com.pluto.plugins.network.internal.interceptor.logic.NetworkCallsRepo import com.pluto.plugins.network.internal.interceptor.logic.asExceptionData import com.pluto.plugins.network.internal.interceptor.logic.core.CacheDirectoryProvider import com.pluto.utilities.DebugLog +import com.pluto.utilities.settings.SettingsPreferences +import java.math.BigInteger import java.util.UUID +import okhttp3.OkHttpClient object PlutoNetwork { internal var cacheDirectoryProvider: CacheDirectoryProvider? = null @@ -40,4 +48,30 @@ object PlutoNetwork { NetworkCallsRepo.set(apiCallData) } } + + fun OkHttpClient.Builder.enableBandwidthMonitor(): OkHttpClient.Builder { + updateBandwidthLimitValues() + return dns(dns) + .socketFactory(BandwidthLimitSocketFactory()) + } + + fun updateBandwidthLimitValues() { + if (SettingsPreferences.isBandwidthLimitEnabled) { + ThrottledInputStream.maxBytesPerSecond = + BigInteger.valueOf(SettingsPreferences.bandWidthLimitDownloadMbps) + .multiply(BigInteger.valueOf(MBPS_TO_BPS)).toLong() + ThrottledOutputStream.maxBytesPerSecond = + BigInteger.valueOf(SettingsPreferences.bandWidthLimitUploadMbps) + .multiply(BigInteger.valueOf(MBPS_TO_BPS)).toLong() + dns.timeoutMilliSeconds = SettingsPreferences.bandWidthDnsResolutionDelay + } else { + ThrottledInputStream.maxBytesPerSecond = BandwidthDefaults.FULL_NETWORK_SPEED_DOWNLOAD + ThrottledOutputStream.maxBytesPerSecond = BandwidthDefaults.FULL_NETWORK_SPEED_UPLOAD + dns.timeoutMilliSeconds = BandwidthDefaults.NO_DELAY + } + } + + private val dns = DnsDelay(0) + + private const val MBPS_TO_BPS: Long = 1_000_000L } diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthDefaults.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthDefaults.kt new file mode 100644 index 000000000..b5f1d9aa2 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthDefaults.kt @@ -0,0 +1,10 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +object BandwidthDefaults { + + const val NO_DELAY: Long = 0 + + const val FULL_NETWORK_SPEED_UPLOAD: Long = Long.MAX_VALUE + + const val FULL_NETWORK_SPEED_DOWNLOAD: Long = Long.MAX_VALUE +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactory.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactory.kt new file mode 100644 index 000000000..5d5314a00 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactory.kt @@ -0,0 +1,67 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.net.Socket +import javax.net.SocketFactory + +class BandwidthLimitSocketFactory : SocketFactory() { + + override fun createSocket(): Socket { + return DelaySocket() + } + + override fun createSocket(host: String?, port: Int): Socket { + return DelaySocket(host, port) + } + + override fun createSocket( + host: String?, + port: Int, + localHost: InetAddress?, + localPort: Int + ): Socket { + return DelaySocket(host, port, localHost, localPort) + } + + override fun createSocket(host: InetAddress?, port: Int): Socket { + return DelaySocket(host, port) + } + + override fun createSocket( + address: InetAddress?, + port: Int, + localAddress: InetAddress?, + localPort: Int + ): Socket { + return DelaySocket(address, port, localAddress, localPort) + } + + class DelaySocket : Socket { + constructor() : super() + constructor(host: String?, port: Int) : super(host, port) + constructor(address: InetAddress?, port: Int) : super(address, port) + constructor(host: String?, port: Int, localAddr: InetAddress?, localPort: Int) : super( + host, + port, + localAddr, + localPort + ) + + constructor( + address: InetAddress?, + port: Int, + localAddr: InetAddress?, + localPort: Int + ) : super(address, port, localAddr, localPort) + + override fun getInputStream(): InputStream { + return ThrottledInputStream(super.getInputStream()) + } + + override fun getOutputStream(): OutputStream { + return ThrottledOutputStream(super.getOutputStream()) + } + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/DnsDelay.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/DnsDelay.kt new file mode 100644 index 000000000..fc2013dbe --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/DnsDelay.kt @@ -0,0 +1,18 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +import java.net.InetAddress +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import okhttp3.Dns + +/** + * Custom okhttp dns that adds network delay while finding the dns, by blocking the thread + * */ +class DnsDelay(var timeoutMilliSeconds: Long) : Dns { + override fun lookup(hostname: String): List { + return runBlocking { + delay(timeoutMilliSeconds) + Dns.SYSTEM.lookup(hostname) + } + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledInputStream.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledInputStream.kt new file mode 100644 index 000000000..6b97faa04 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledInputStream.kt @@ -0,0 +1,95 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +import java.io.IOException +import java.io.InputStream + +class ThrottledInputStream constructor( + inputStream: InputStream +) : + InputStream() { + private val inputStream: InputStream + private val startTime = System.nanoTime() + private var totalBytesRead: Long = 0 + private var totalSleepTime: Long = 0 + + init { + this.inputStream = inputStream + } + + @Throws(IOException::class) + override fun close() { + inputStream.close() + } + + @Throws(IOException::class) + override fun read(): Int { + throttle() + val data = inputStream.read() + if (data != -1) { + totalBytesRead++ + } + return data + } + + @Throws(IOException::class) + override fun read(b: ByteArray): Int { + throttle() + val readLen = inputStream.read(b) + if (readLen != -1) { + totalBytesRead += readLen.toLong() + } + return readLen + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + throttle() + val readLen = inputStream.read(b, off, len) + if (readLen != -1) { + totalBytesRead += readLen.toLong() + } + return readLen + } + + @Throws(IOException::class) + private fun throttle() { + while (bytesPerSec > maxBytesPerSecond) { + totalSleepTime += try { + Thread.sleep(SLEEP_DURATION_MS) + SLEEP_DURATION_MS + } catch (e: InterruptedException) { + println("Thread interrupted" + e.message) + throw IOException("Thread interrupted", e) + } + } + } + + /** + * Return the number of bytes read per second + */ + private val bytesPerSec: Long + get() { + val elapsed = (System.nanoTime() - startTime) / SECOND_IN_NANOSECONDS + return if (elapsed == 0L) { + totalBytesRead + } else { + totalBytesRead / elapsed + } + } + + override fun toString(): String { + val totalSleepTimeInSecond = totalSleepTime / SECOND_IN_MILLISECONDS + return "ThrottledInputStream{bytesRead=$totalBytesRead, " + + "maxBytesPerSec=$maxBytesPerSecond, bytesPerSec=$bytesPerSec," + + " totalSleepTimeInSeconds=$totalSleepTimeInSecond}" + } + + companion object { + private const val SLEEP_DURATION_MS: Long = 30 + private const val SECOND_IN_NANOSECONDS = 1_000_000_000L + private const val SECOND_IN_MILLISECONDS = 1000L + + @JvmStatic + var maxBytesPerSecond: Long = Long.MAX_VALUE + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledOutputStream.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledOutputStream.kt new file mode 100644 index 000000000..ff06faee8 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/core/ThrottledOutputStream.kt @@ -0,0 +1,98 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +import java.io.IOException +import java.io.OutputStream + +class ThrottledOutputStream constructor( + outputStream: OutputStream +) : + OutputStream() { + private val outputStream: OutputStream + private val startTime = System.nanoTime() + private var bytesWrite: Long = 0 + private var totalSleepTime: Long = 0 + + init { + this.outputStream = outputStream + } + + @Throws(IOException::class) + override fun write(arg0: Int) { + throttle() + outputStream.write(arg0) + bytesWrite++ + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + if (len < maxBytesPerSecond) { + throttle() + bytesWrite += len + outputStream.write(b, off, len) + return + } + var currentOffSet = off.toLong() + var remainingBytesToWrite = len.toLong() + do { + throttle() + remainingBytesToWrite -= maxBytesPerSecond + bytesWrite += maxBytesPerSecond + outputStream.write(b, currentOffSet.toInt(), maxBytesPerSecond.toInt()) + currentOffSet += maxBytesPerSecond + } while (remainingBytesToWrite > maxBytesPerSecond) + throttle() + bytesWrite += remainingBytesToWrite + outputStream.write(b, currentOffSet.toInt(), remainingBytesToWrite.toInt()) + } + + @Throws(IOException::class) + override fun write(b: ByteArray) { + this.write(b, 0, b.size) + } + + @Throws(IOException::class) + fun throttle() { + while (bytesPerSec > maxBytesPerSecond) { + totalSleepTime += try { + Thread.sleep(SLEEP_DURATION_MS) + SLEEP_DURATION_MS + } catch (e: InterruptedException) { + println("Thread interrupted" + e.message) + throw IOException("Thread interrupted", e) + } + } + } + + /** + * Return the number of bytes read per second + */ + private val bytesPerSec: Long + get() { + val elapsed = (System.nanoTime() - startTime) / SECOND_IN_NANOSECONDS + return if (elapsed == 0L) { + bytesWrite + } else { + bytesWrite / elapsed + } + } + + override fun toString(): String { + val totalSleepTimeInSeconds = totalSleepTime / SECOND_IN_MILLISECONDS + return "ThrottledOutputStream{" + "bytesWrite=" + bytesWrite + ", maxBytesPerSecond=" + + maxBytesPerSecond + ", bytesPerSec=" + bytesPerSec + ", totalSleepTimeInSeconds=" + + totalSleepTimeInSeconds + '}' + } + + @Throws(IOException::class) + override fun close() { + outputStream.close() + } + + companion object { + private const val SLEEP_DURATION_MS: Long = 30 + private const val SECOND_IN_NANOSECONDS = 1_000_000_000L + private const val SECOND_IN_MILLISECONDS = 1000L + @JvmStatic + var maxBytesPerSecond: Long = Long.MAX_VALUE + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthFragment.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthFragment.kt new file mode 100644 index 000000000..44248e83b --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthFragment.kt @@ -0,0 +1,89 @@ +package com.pluto.plugins.network.internal.bandwidth.ui + +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.pluto.plugins.network.PlutoNetwork +import com.pluto.plugins.network.R +import com.pluto.plugins.network.databinding.PlutoNetworkFragmentBandwidthBinding +import com.pluto.utilities.setOnDebounceClickListener +import com.pluto.utilities.settings.SettingsPreferences + +class BandwidthFragment : BottomSheetDialogFragment() { + + private lateinit var binding: PlutoNetworkFragmentBandwidthBinding + + private val viewModel: BandwidthViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = PlutoNetworkFragmentBandwidthBinding.inflate(layoutInflater, container, false) + binding.viewModel = viewModel + setInfinityTextWatcher(binding.valueDownloadLimit, binding.valueUploadLimit) + return binding.root + } + + private fun setInfinityTextWatcher(vararg editTexts: EditText) { + for (editText in editTexts) { + InfinityTextWatcher(editText, Long.MAX_VALUE) + } + } + + override fun getTheme(): Int = R.style.PlutoNetworkBottomSheetDialog + + private fun enableInputFields(vararg editTexts: EditText) { + for (editText in editTexts) { + editText.inputType = InputType.TYPE_CLASS_NUMBER + } + } + + private fun disableInputFields(vararg editTexts: EditText) { + for (editText in editTexts) { + editText.inputType = InputType.TYPE_NULL + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewModel.dnsResolutionTimeout.observe(viewLifecycleOwner) { + updateSaveCtaState() + } + viewModel.downloadSpeedInMbps.observe(viewLifecycleOwner) { + updateSaveCtaState() + } + viewModel.uploadSpeedInMbps.observe(viewLifecycleOwner) { + updateSaveCtaState() + } + viewModel.isBandwidthLimitEnabled.observe(viewLifecycleOwner) { + if (it) { + enableInputFields(binding.valueDnsDelay, binding.valueDownloadLimit, binding.valueDownloadLimit) + } else { + disableInputFields(binding.valueDnsDelay, binding.valueDownloadLimit, binding.valueDownloadLimit) + viewModel.reset() + } + updateSaveCtaState() + } + binding.save.setOnDebounceClickListener { + // saving values + SettingsPreferences.isBandwidthLimitEnabled = viewModel.isBandwidthLimitEnabled.value!! + SettingsPreferences.bandWidthLimitDownloadMbps = viewModel.downloadSpeedInMbps.value!! + SettingsPreferences.bandWidthDnsResolutionDelay = viewModel.dnsResolutionTimeout.value!! + SettingsPreferences.bandWidthLimitUploadMbps = viewModel.uploadSpeedInMbps.value!! + // updating limits + PlutoNetwork.updateBandwidthLimitValues() + dismiss() + } + binding.cta.setOnDebounceClickListener { + dismiss() + } + } + + private fun updateSaveCtaState() { + binding.save.isVisible = viewModel.areValuesChanged() + binding.disabled.isVisible = !viewModel.areValuesChanged() + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthViewModel.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthViewModel.kt new file mode 100644 index 000000000..c418b79eb --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BandwidthViewModel.kt @@ -0,0 +1,26 @@ +package com.pluto.plugins.network.internal.bandwidth.ui + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.pluto.utilities.settings.SettingsPreferences + +class BandwidthViewModel : ViewModel() { + + val dnsResolutionTimeout: MutableLiveData = MutableLiveData(SettingsPreferences.bandWidthDnsResolutionDelay) + val uploadSpeedInMbps: MutableLiveData = MutableLiveData(SettingsPreferences.bandWidthLimitUploadMbps) + val downloadSpeedInMbps: MutableLiveData = MutableLiveData(SettingsPreferences.bandWidthLimitDownloadMbps) + val isBandwidthLimitEnabled: MutableLiveData = MutableLiveData(SettingsPreferences.isBandwidthLimitEnabled) + + fun areValuesChanged(): Boolean { + return SettingsPreferences.bandWidthDnsResolutionDelay != dnsResolutionTimeout.value || + SettingsPreferences.bandWidthLimitUploadMbps != uploadSpeedInMbps.value || + SettingsPreferences.bandWidthLimitDownloadMbps != downloadSpeedInMbps.value || + SettingsPreferences.isBandwidthLimitEnabled != isBandwidthLimitEnabled.value + } + + fun reset() { + uploadSpeedInMbps.postValue(SettingsPreferences.bandWidthLimitUploadMbps) + downloadSpeedInMbps.postValue(SettingsPreferences.bandWidthLimitDownloadMbps) + dnsResolutionTimeout.postValue(SettingsPreferences.bandWidthDnsResolutionDelay) + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BindingAdapter.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BindingAdapter.kt new file mode 100644 index 000000000..1fdf46a57 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/BindingAdapter.kt @@ -0,0 +1,41 @@ +package com.pluto.plugins.network.internal.bandwidth.ui + +import android.widget.TextView +import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import okhttp3.internal.toLongOrDefault + +@BindingAdapter("android:text") +fun setText(view: TextView, text: Long?) { + val oldText = view.text + if (text.toString() === oldText) { + return + } + if (!haveContentsChanged(text.toString(), oldText)) { + return // No content changes, so don't set anything. + } + view.text = text.toString() +} + +@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged") +fun getText(view: TextView): Long { + return view.text.toString().toLongOrDefault(0) +} + +private fun haveContentsChanged(str1: CharSequence?, str2: CharSequence?): Boolean { + if (str1 == null != (str2 == null)) { + return true + } else if (str1 == null) { + return false + } + val length = str1.length + if (length != str2!!.length) { + return true + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/InfinityTextWatcher.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/InfinityTextWatcher.kt new file mode 100644 index 000000000..1eff0bbb1 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/bandwidth/ui/InfinityTextWatcher.kt @@ -0,0 +1,25 @@ +package com.pluto.plugins.network.internal.bandwidth.ui + +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText + +class InfinityTextWatcher(private val editText: EditText, private val threshold: Long) : + TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} + override fun afterTextChanged(editable: Editable) { + val inputText = editable.toString() + try { + val value = inputText.toLong() + if (value > threshold) { + editText.setText(INFINITY) + } + } catch (e: NumberFormatException) { + // Handle parsing errors or non-numeric input + } + } + companion object { + const val INFINITY = "∞" + } +} diff --git a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt index f7e827bf6..e881da39e 100644 --- a/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt +++ b/pluto-plugins/plugins/network/lib/src/main/java/com/pluto/plugins/network/internal/interceptor/ui/ListFragment.kt @@ -65,7 +65,9 @@ internal class ListFragment : Fragment(R.layout.pluto_network___fragment_list) { } } } - + binding.bandwidthButton.setOnDebounceClickListener { + findNavController().navigate(R.id.bandwidth) + } viewModel.apiCalls.removeObserver(listObserver) viewModel.apiCalls.observe(viewLifecycleOwner, listObserver) } diff --git a/pluto-plugins/plugins/network/lib/src/main/res/drawable/pluto_network___ic_bandwidth.xml b/pluto-plugins/plugins/network/lib/src/main/res/drawable/pluto_network___ic_bandwidth.xml new file mode 100644 index 000000000..57e5e9f5d --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/res/drawable/pluto_network___ic_bandwidth.xml @@ -0,0 +1,10 @@ + + + diff --git a/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_bandwidth.xml b/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_bandwidth.xml new file mode 100644 index 000000000..67a700667 --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_bandwidth.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_list.xml b/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_list.xml index 06931c5bb..2c6f7c0c3 100644 --- a/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_list.xml +++ b/pluto-plugins/plugins/network/lib/src/main/res/layout/pluto_network___fragment_list.xml @@ -88,39 +88,52 @@ app:layout_constraintTop_toTopOf="parent" /> - - + app:layout_constraintEnd_toEndOf="parent"> - + + + android:layout_margin="@dimen/pluto___margin_small" + android:background="@color/pluto___white" + android:elevation="@dimen/pluto___margin_medium" + app:cardCornerRadius="@dimen/pluto___margin_xsmall" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toStartOf="@+id/bandwidthButton" + android:id="@+id/pluto_network___cardview"> + + diff --git a/pluto-plugins/plugins/network/lib/src/main/res/navigation/pluto_network___navigation.xml b/pluto-plugins/plugins/network/lib/src/main/res/navigation/pluto_network___navigation.xml index 3dd7ab5ca..c9e150433 100644 --- a/pluto-plugins/plugins/network/lib/src/main/res/navigation/pluto_network___navigation.xml +++ b/pluto-plugins/plugins/network/lib/src/main/res/navigation/pluto_network___navigation.xml @@ -16,6 +16,9 @@ + - \ No newline at end of file + + diff --git a/pluto-plugins/plugins/network/lib/src/main/res/values/strings.xml b/pluto-plugins/plugins/network/lib/src/main/res/values/strings.xml index be2332778..d46a52354 100644 --- a/pluto-plugins/plugins/network/lib/src/main/res/values/strings.xml +++ b/pluto-plugins/plugins/network/lib/src/main/res/values/strings.xml @@ -85,6 +85,15 @@ Custom Custom Network trace This network trace was logged by client app, not PlutoInterceptor + Dns Resolution Delay in ms + Limit Bandwidth + Done + Fetch Dns Timeout In Milli Seconds + Download Speed Limit In Mbps + Upload Speed Limit In Mbps + Enable Network Bandwidth Limit + Close + 1 param %d params diff --git a/pluto-plugins/plugins/network/lib/src/test/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactoryTest.kt b/pluto-plugins/plugins/network/lib/src/test/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactoryTest.kt new file mode 100644 index 000000000..697f76f0f --- /dev/null +++ b/pluto-plugins/plugins/network/lib/src/test/java/com/pluto/plugins/network/internal/bandwidth/core/BandwidthLimitSocketFactoryTest.kt @@ -0,0 +1,112 @@ +package com.pluto.plugins.network.internal.bandwidth.core + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.nio.charset.StandardCharsets +import kotlin.math.ceil +import kotlin.random.Random +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class BandwidthLimitSocketFactoryTest { + + private lateinit var mockWebServer: MockWebServer + private lateinit var okHttpClient: OkHttpClient + + @Before + fun setUp() { + mockWebServer = MockWebServer() + mockWebServer.start() + + val factory = BandwidthLimitSocketFactory() + okHttpClient = OkHttpClient.Builder() + .retryOnConnectionFailure(true) + .cache(null) + .socketFactory(factory) + .build() + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun testUploadSpeed() { + ThrottledOutputStream.maxBytesPerSecond = 1024 * 512 + ThrottledInputStream.maxBytesPerSecond = Long.MAX_VALUE + val dataSizeBytes = 1024 * 1024 + + mockWebServer.enqueue(MockResponse()) + + val request = Request.Builder() + .url(mockWebServer.url("/")) + .put(MockRequestBody(dataSizeBytes.toLong())) + .header("Connection", "close") + .build() + + val startTime = System.currentTimeMillis() + okHttpClient.newCall(request).execute() + val endTime = System.currentTimeMillis() + + val uploadTimeSeconds = (endTime - startTime) / 1000.0 + Assert.assertEquals( + (dataSizeBytes / ThrottledOutputStream.maxBytesPerSecond).toInt(), + uploadTimeSeconds.toInt() + ) + } + + @Test + fun testDownloadSpeed() { + ThrottledOutputStream.maxBytesPerSecond = Long.MAX_VALUE + ThrottledInputStream.maxBytesPerSecond = 1024 * 512 + val dataSizeBytes = 1024 * 1024 + mockWebServer.enqueue( + MockResponse().setHeader("Content-Length", dataSizeBytes).setResponseCode(200).setBody( + String( + Random.nextBytes(ByteArray(dataSizeBytes)), StandardCharsets.UTF_8 + ) + ) + ) + + val request = Request.Builder() + .url(mockWebServer.url("/")) + .put(MockRequestBody(0)) + .build() + + val startTime = System.currentTimeMillis() + val execute = okHttpClient.newCall(request).execute() + execute.body.string() + val byteCount = (execute.headers["Content-Length"])?.toInt() ?: 0 + val endTime = System.currentTimeMillis() + + val downloadTimeSeconds = (endTime - startTime) / 1000.0 + Assert.assertEquals( + ceil(byteCount * 1f / ThrottledInputStream.maxBytesPerSecond).toInt(), + downloadTimeSeconds.toInt() + ) + } + // Similar test for download speed using mockWebServer.enqueue +} + +// Placeholder for com.pluto.plugins.network.internal.bandwidth.core.MockRequestBody +class MockRequestBody(private val contentLength: Long) : okhttp3.RequestBody() { + override fun contentType() = null + + override fun contentLength() = contentLength + + @Throws(IOException::class) + override fun writeTo(sink: okio.BufferedSink) { + val buffer = Buffer() + val inputStream = ByteArrayInputStream(ByteArray(contentLength.toInt())) + buffer.readFrom(inputStream) + sink.write(buffer, contentLength) + } +} diff --git a/sample/build.gradle b/sample/build.gradle index 3b6a3c213..1c03fffd3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -13,6 +13,7 @@ android { buildFeatures { viewBinding true + dataBinding true } defaultConfig { diff --git a/sample/src/main/java/com/sampleapp/functions/network/internal/core/Network.kt b/sample/src/main/java/com/sampleapp/functions/network/internal/core/Network.kt index 38bb72ad1..073e3ebbb 100644 --- a/sample/src/main/java/com/sampleapp/functions/network/internal/core/Network.kt +++ b/sample/src/main/java/com/sampleapp/functions/network/internal/core/Network.kt @@ -1,6 +1,8 @@ package com.sampleapp.functions.network.internal.core import com.pluto.plugins.network.PlutoInterceptor +import com.pluto.plugins.network.PlutoNetwork.enableBandwidthMonitor +import com.pluto.plugins.network.internal.bandwidth.core.BandwidthLimitSocketFactory import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -12,17 +14,12 @@ object Network { private const val READ_TIMEOUT = 30L private val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl("https://api.mocklets.com/p68296/") - .addConverterFactory(MoshiConverterFactory.create()) - .client(okHttpClient) - .build() + Retrofit.Builder().baseUrl("https://api.mocklets.com/p68296/") + .addConverterFactory(MoshiConverterFactory.create()).client(okHttpClient).build() } - private val okHttpClient: OkHttpClient = OkHttpClient.Builder() - .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) - .addInterceptors() - .build() + private val okHttpClient: OkHttpClient = + OkHttpClient.Builder().readTimeout(READ_TIMEOUT, TimeUnit.SECONDS).addInterceptors().build() fun getService(cls: Class): T { return retrofit.create(cls) @@ -37,6 +34,8 @@ object Network { private fun OkHttpClient.Builder.addInterceptors(): OkHttpClient.Builder { // addInterceptor(GzipRequestInterceptor()) + enableBandwidthMonitor() + socketFactory(BandwidthLimitSocketFactory()) addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) addInterceptor(PlutoInterceptor()) return this