Skip to content

Commit f13d15e

Browse files
committed
Reduce the amount of calls to exchange APIs to cut down on rate limiting.
Add a new indicator for when an exchange has rate limited a widget. Space out updates for widgets when refreshing to avoid rate limiting. Renamed BitClude to Egera. Fixed a bug where widget prices were not updating in certain scenarios. Stop using Coingecko as the default for newly created widgets.
1 parent b3490ba commit f13d15e

29 files changed

+248
-190
lines changed

.idea/kotlinc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bitcoin/build.gradle

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ plugins {
33
id 'kotlin-android'
44
id 'com.google.devtools.ksp'
55
id 'org.jetbrains.kotlin.android'
6-
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.20'
6+
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22'
77
}
88

99
android {
@@ -13,8 +13,8 @@ android {
1313
applicationId "com.brentpanther.bitcoinwidget"
1414
minSdk 23
1515
targetSdk 34
16-
versionCode 324
17-
versionName "8.4.5"
16+
versionCode 325
17+
versionName "8.5.0"
1818

1919
}
2020

@@ -65,17 +65,17 @@ dependencies {
6565
implementation platform('androidx.compose:compose-bom:2023.10.01')
6666

6767
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
68-
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.1"
68+
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
6969
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
7070
implementation 'androidx.preference:preference-ktx:1.2.1'
7171
implementation 'androidx.work:work-runtime:2.9.0'
72-
implementation 'androidx.activity:activity-ktx:1.8.1'
73-
implementation 'androidx.activity:activity-compose:1.8.1'
72+
implementation 'androidx.activity:activity-ktx:1.8.2'
73+
implementation 'androidx.activity:activity-compose:1.8.2'
7474
implementation "androidx.compose.ui:ui"
7575
implementation "androidx.compose.ui:ui-tooling-preview"
7676
implementation 'androidx.compose.material:material'
77-
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
78-
implementation 'androidx.navigation:navigation-compose:2.7.5'
77+
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
78+
implementation 'androidx.navigation:navigation-compose:2.7.6'
7979
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
8080
implementation 'io.coil-kt:coil-compose:2.5.0'
8181
implementation 'androidx.core:core-ktx:1.12.0'

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/Enums.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ enum class NightMode {
4242
}
4343

4444
enum class WidgetState {
45-
DRAFT, CURRENT, STALE, ERROR
45+
DRAFT, CURRENT, STALE, RATE_LIMITED, ERROR
4646
}
4747

4848
enum class WidgetType(@StringRes val widgetName: Int, @StringRes val widgetSummary: Int) {

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/Repository.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,25 +90,25 @@ object Repository {
9090
}
9191
}
9292

93-
fun getExchangeData(widget: Widget): ExchangeData {
93+
fun getExchangeData(coin: Coin, coinName: String?): ExchangeData {
9494
val context = WidgetApplication.instance
9595
return try {
96-
if (widget.coin == Coin.CUSTOM) {
97-
CustomExchangeData(widget.coinName(), widget.coin, getJson(context))
96+
if (coin == Coin.CUSTOM) {
97+
CustomExchangeData(coinName ?: coin.coinName, coin, getJson(context))
9898
} else {
99-
val data = ExchangeData(widget.coin, getJson(context))
99+
val data = ExchangeData(coin, getJson(context))
100100
if (data.numberExchanges == 0) {
101101
throw SerializationException("No exchanges found.")
102102
}
103103
data
104104
}
105105
} catch(e: SerializationException) {
106-
Log.e("SettingsViewModel", "Error parsing JSON file, falling back to original.", e)
106+
Log.e(TAG, "Error parsing JSON file, falling back to original.", e)
107107
context.deleteFile(CURRENCY_FILE_NAME)
108108
PreferenceManager.getDefaultSharedPreferences(context).edit {
109109
remove(LAST_MODIFIED)
110110
}
111-
ExchangeData(widget.coin, getJson(context))
111+
ExchangeData(coin, getJson(context))
112112
}
113113
}
114114

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/WidgetApplication.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class WidgetApplication : Application() {
3535
super.onCreate()
3636
instance = this
3737
registerReceiver(WidgetBroadcastReceiver(), IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED))
38+
WidgetUpdater.updateDisplays(this)
3839
}
3940

4041
companion object {

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/WidgetProvider.kt

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,9 @@ open class WidgetProvider : AppWidgetProvider() {
3131
}
3232
}
3333

34-
override fun onEnabled(context: Context) {
35-
refreshWidgets(context)
36-
}
37-
38-
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, widgetIds: IntArray) {
39-
refreshWidgets(context)
40-
}
41-
4234
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager,
4335
appWidgetId: Int, newOptions: Bundle) {
44-
refreshWidgets(context)
36+
refreshWidgets(context, appWidgetId)
4537
}
4638

4739
override fun onDeleted(context: Context, widgetIds: IntArray) {
@@ -53,7 +45,7 @@ open class WidgetProvider : AppWidgetProvider() {
5345
workManager.cancelUniqueWork(ONETIMEWORKNAME)
5446
cancelWork(workManager)
5547
} else if (widgetDao.configWithSizes().consistentSize) {
56-
refreshWidgets(context)
48+
WidgetUpdater.updateDisplays(context)
5749
}
5850
}
5951
}
@@ -63,37 +55,42 @@ open class WidgetProvider : AppWidgetProvider() {
6355
private const val WORKNAME = "widgetRefresh"
6456
const val ONETIMEWORKNAME = "115575872"
6557

66-
fun refreshWidgets(context: Context, restart: Boolean = false) = CoroutineScope(Dispatchers.IO).launch {
58+
fun refreshWidgets(context: Context, widgetId: Int) = refreshWidgets(context, intArrayOf(widgetId))
59+
60+
fun refreshWidgets(context: Context, widgetIds: IntArray? = null, restart: Boolean = false) = CoroutineScope(Dispatchers.IO).launch {
6761
val dao = WidgetDatabase.getInstance(context).widgetDao()
68-
val widgetIds = dao.getAll().map { it.widgetId }.toIntArray()
69-
if (widgetIds.isEmpty()) return@launch
62+
val widgetIdsToRefresh = widgetIds ?: dao.getAll().map { it.widgetId }.toIntArray()
63+
if (widgetIdsToRefresh.isEmpty()) return@launch
7064

71-
WidgetUpdater.update(context, widgetIds, false)
65+
WidgetUpdater.update(context, widgetIdsToRefresh, false)
7266
val workManager = WorkManager.getInstance(context)
7367
val refresh = dao.configWithSizes().refresh
7468

7569
// https://issuetracker.google.com/issues/115575872
76-
val immediateWork = OneTimeWorkRequestBuilder<WidgetUpdateWorker>().setInitialDelay(3650L, TimeUnit.DAYS).build()
70+
val immediateWork = OneTimeWorkRequestBuilder<WidgetUpdateWorker>()
71+
.setInitialDelay(3650L, TimeUnit.DAYS).build()
7772
workManager.enqueueUniqueWork(ONETIMEWORKNAME, ExistingWorkPolicy.KEEP, immediateWork)
7873

79-
val workPolicy = if (restart) ExistingPeriodicWorkPolicy.UPDATE else ExistingPeriodicWorkPolicy.KEEP
74+
if (restart) {
75+
workManager.cancelAllWorkByTag(WORKNAME)
76+
}
8077
when (refresh) {
81-
5 -> (0..10 step 5).forEachIndexed { i, it -> scheduleWork(workManager, 15, it, i, workPolicy) }
82-
10 -> (0..10 step 10).forEachIndexed { i, it -> scheduleWork(workManager, 20, it, i, workPolicy) }
83-
else -> scheduleWork(workManager, refresh, 0, 0, workPolicy)
78+
5 -> (5..15 step 5).forEachIndexed { i, it -> scheduleWork(workManager, 15, it, i) }
79+
10 -> (10..20 step 10).forEachIndexed { i, it -> scheduleWork(workManager, 20, it, i) }
80+
else -> scheduleWork(workManager, refresh, refresh, 0)
8481
}
8582
}
8683

8784
fun cancelWork(workManager: WorkManager) = workManager.cancelAllWorkByTag(WORKNAME)
8885

89-
private fun scheduleWork(workManager: WorkManager, refresh: Int, delay: Int, index: Int, policy: ExistingPeriodicWorkPolicy) {
86+
private fun scheduleWork(workManager: WorkManager, refresh: Int, delay: Int, index: Int) {
9087
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
9188
val work = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(refresh.toLong(), TimeUnit.MINUTES)
9289
.setConstraints(constraints)
9390
.addTag(WORKNAME)
9491
.setInitialDelay(delay.toLong(), TimeUnit.MINUTES)
9592
.build()
96-
workManager.enqueueUniquePeriodicWork("$WORKNAME$index", policy, work)
93+
workManager.enqueueUniquePeriodicWork("$WORKNAME$index", ExistingPeriodicWorkPolicy.KEEP, work)
9794
}
9895

9996
}
Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
package com.brentpanther.bitcoinwidget
22

33
import android.content.Context
4+
import com.brentpanther.bitcoinwidget.db.WidgetDatabase
45
import com.brentpanther.bitcoinwidget.strategy.data.WidgetDataStrategy
56
import com.brentpanther.bitcoinwidget.strategy.display.WidgetDisplayStrategy
67
import com.brentpanther.bitcoinwidget.strategy.presenter.RemoteWidgetPresenter
7-
import kotlinx.coroutines.coroutineScope
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.delay
11+
import kotlinx.coroutines.launch
12+
import kotlin.time.Duration.Companion.seconds
813

914
object WidgetUpdater {
1015

11-
suspend fun update(context: Context, widgetIds: IntArray, manual: Boolean) = coroutineScope {
12-
16+
fun update(context: Context, widgetIds: IntArray, manual: Boolean) = CoroutineScope(Dispatchers.IO).launch {
1317
val dataStrategies = widgetIds.map { WidgetDataStrategy.getStrategy(it) }
1418

15-
// update display immediately to avoid looking bad
16-
for (strategy in dataStrategies) {
17-
val widget = strategy.widget ?: continue
19+
dataStrategies.forEachIndexed { index, strategy ->
20+
val widget = strategy.widget ?: return@forEachIndexed
1821
val widgetPresenter = RemoteWidgetPresenter(context, widget)
19-
val displayStrategy = WidgetDisplayStrategy.getStrategy(context, widget, widgetPresenter)
20-
displayStrategy.refresh()
21-
}
22-
23-
// update data
24-
for (strategy in dataStrategies) {
25-
strategy.loadData(manual)
22+
val success = strategy.loadData(manual)
2623
strategy.save()
24+
if (success) {
25+
val displayStrategy = WidgetDisplayStrategy.getStrategy(context, widget, widgetPresenter)
26+
displayStrategy.refresh()
27+
displayStrategy.save()
28+
// wait before refreshing the next widget to avoid rate limiting issues
29+
if (index != dataStrategies.lastIndex) {
30+
delay(20.seconds)
31+
}
32+
}
2733
}
2834

29-
// data may cause display to need refreshed
30-
for (strategy in dataStrategies) {
31-
val widget = strategy.widget ?: continue
32-
val widgetPresenter = RemoteWidgetPresenter(context, widget)
33-
val displayStrategy = WidgetDisplayStrategy.getStrategy(context, widget, widgetPresenter)
34-
displayStrategy.refresh()
35-
displayStrategy.save()
35+
updateDisplays(context)
36+
}
37+
38+
fun updateDisplays(context: Context) = CoroutineScope(Dispatchers.IO).launch {
39+
// change of data for different widgets may cause us to need to refresh all widgets
40+
val dao = WidgetDatabase.getInstance(context).widgetDao()
41+
val widgetIds = dao.getAll().map { it.widgetId }.toIntArray()
42+
val dataStrategies = widgetIds.map { WidgetDataStrategy.getStrategy(it) }
43+
(1..2).forEachIndexed { _, _ ->
44+
// refresh twice in case the size changed on the last widget
45+
dataStrategies.forEach { strategy ->
46+
val widget = strategy.widget ?: return@forEach
47+
val widgetPresenter = RemoteWidgetPresenter(context, widget)
48+
val displayStrategy = WidgetDisplayStrategy.getStrategy(context, widget, widgetPresenter)
49+
displayStrategy.refresh()
50+
displayStrategy.save()
51+
}
3652
}
53+
3754
}
3855

3956
}

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/db/DataMigration.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ object DataMigration {
1313
migrateOkexToOkx(db)
1414
migrateBithumbProToBitGlobal(db)
1515
migrateKaspaToKas(db)
16+
migrateBitcludeToEgera(db)
1617
fixRemovedExchanges(db)
1718
}
1819

@@ -32,6 +33,10 @@ object DataMigration {
3233
db.execSQL("UPDATE Widget SET exchange = 'ZONDA' WHERE exchange = 'BITBAY'")
3334
}
3435

36+
private fun migrateBitcludeToEgera(db: SupportSQLiteDatabase) {
37+
db.execSQL("UPDATE Widget SET exchange = 'EGERA' WHERE exchange = 'BITCLUDE'")
38+
}
39+
3540
private fun fixRemovedExchanges(db: SupportSQLiteDatabase) {
3641
val cursor = db.query("SELECT id, exchange FROM Widget ORDER BY id")
3742
val allExchanges = Exchange.entries.map { it.name }

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/exchange/Exchange.kt

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,6 @@ enum class Exchange(val exchangeName: String, shortName: String? = null) {
7070
return getJsonObject(url)["data"]?.jsonObject?.get("last").asString
7171
}
7272
},
73-
BITCLUDE("BitClude") {
74-
75-
override fun getValue(coin: String, currency: String): String? {
76-
val url = "https://api.bitclude.com/stats/ticker.json"
77-
val obj = getJsonObject(url)
78-
return obj["${coin.lowercase()}_${currency.lowercase()}"]?.jsonObject?.get("last").asString
79-
}
80-
},
8173
BITCOINDE("Bitcoin.de") {
8274

8375
override fun getValue(coin: String, currency: String): String? {
@@ -310,6 +302,13 @@ enum class Exchange(val exchangeName: String, shortName: String? = null) {
310302
return getJsonObject(url)["ticker"]?.jsonArray?.get(0)?.jsonObject?.get("last").asString
311303
}
312304
},
305+
EGERA("Egera") {
306+
override fun getValue(coin: String, currency: String): String? {
307+
val url = "https://api.egera.com/stats/ticker.json"
308+
val obj = getJsonObject(url)
309+
return obj["${coin.lowercase()}_${currency.lowercase()}"]?.jsonObject?.get("last").asString
310+
}
311+
},
313312
EXMO("Exmo") {
314313

315314
override fun getValue(coin: String, currency: String): String? {
@@ -433,19 +432,6 @@ enum class Exchange(val exchangeName: String, shortName: String? = null) {
433432
?.jsonObject?.get("latest").asString
434433
}
435434
},
436-
LIQUID("Liquid") {
437-
override fun getValue(coin: String, currency: String): String? {
438-
val array = getJsonArray("https://api.liquid.com/products")
439-
val pair = "$coin$currency"
440-
for (jsonElement in array) {
441-
val obj = jsonElement as JsonObject
442-
if (pair == obj["currency_pair_code"].asString) {
443-
return obj["last_traded_price"].asString
444-
}
445-
}
446-
return null
447-
}
448-
},
449435
LUNO("Luno") {
450436

451437
override fun getValue(coin: String, currency: String): String? {
@@ -525,18 +511,6 @@ enum class Exchange(val exchangeName: String, shortName: String? = null) {
525511
return (value.toDouble() / 10.0.pow(8)).toString()
526512
}
527513
},
528-
POCKETBITS("Pocketbits") {
529-
530-
override fun getValue(coin: String, currency: String): String? {
531-
val url = "https://ticker.pocketbits.in/api/v1/ticker"
532-
val obj = getJsonArray(url).firstOrNull {
533-
it.jsonObject["symbol"].asString == "$coin$currency"
534-
}?.jsonObject ?: return null
535-
val buy = obj["buy"].asString?.toDoubleOrNull() ?: return null
536-
val sell = obj["sell"].asString?.toDoubleOrNull() ?: return null
537-
return ((buy + sell) / 2).toString()
538-
}
539-
},
540514
POLONIEX("Poloniex") {
541515

542516
override fun getValue(coin: String, currency: String): String? {

bitcoin/src/main/java/com/brentpanther/bitcoinwidget/exchange/ExchangeData.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import kotlinx.serialization.SerialName
88
import kotlinx.serialization.json.Json
99
import kotlinx.serialization.json.decodeFromStream
1010
import java.io.InputStream
11-
import java.util.*
11+
import java.util.Currency
1212

1313

1414
open class ExchangeData(val coin: Coin, json: InputStream) {
@@ -93,10 +93,7 @@ open class ExchangeData(val coin: Coin, json: InputStream) {
9393
//TODO: change to use exchange
9494
fun getDefaultExchange(currency: String): String {
9595
val exchanges = currencyExchange[currency]
96-
exchanges?.let {
97-
if (!exchanges.contains(Exchange.COINGECKO.name)) return exchanges[0]
98-
}
99-
return Exchange.COINGECKO.name
96+
return exchanges?.get(0) ?: Exchange.COINGECKO.name
10097
}
10198

10299
open fun getExchangeCoinName(exchange: String): String? {

0 commit comments

Comments
 (0)