Skip to content

Enhancement | Bandwidth limiter for network plugin #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions pluto-plugins/plugins/network/lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ android {

buildFeatures {
viewBinding true
dataBinding true
}


Expand Down Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
Original file line number Diff line number Diff line change
@@ -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<InetAddress> {
return runBlocking {
delay(timeoutMilliSeconds)
Dns.SYSTEM.lookup(hostname)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading