Skip to content

Commit 5d5b628

Browse files
v4.2.2
1 parent d52012e commit 5d5b628

File tree

23 files changed

+348
-132
lines changed

23 files changed

+348
-132
lines changed

.github/workflows/package-and-publish.yml

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ jobs:
112112
env:
113113
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114114
steps:
115+
- name: Install DigiCert Client tools
116+
uses: digicert/[email protected]
117+
115118
- name: Setup certificate
116119
shell: bash
117120
run: echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
@@ -130,18 +133,6 @@ jobs:
130133
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
131134
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
132135
133-
- name: Setup SSM KSP
134-
env:
135-
SM_API_KEY: ${{ secrets.SM_API_KEY }}
136-
shell: cmd
137-
run: |
138-
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools.msi
139-
msiexec /i smtools.msi /quiet /qn
140-
smksp_registrar.exe list
141-
smctl.exe keypair ls
142-
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
143-
smksp_cert_sync.exe
144-
145136
- name: Download Windows package
146137
id: download-artifact
147138
uses: actions/download-artifact@v4

changelogs/4.2.2.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
· Handy Widgets · Track balances and view NFTs right from your home screen.
2+
· Live Fiat Previews · See the total balance in multiple fiat currencies at once.
3+
· New Welcome Flow · Easier onboarding with confetti.

mobile/android/air/SubModules/UIWidgets/src/main/java/org/mytonwallet/app_air/widgets/priceWidget/PriceWidget.kt

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ class PriceWidget : AppWidgetProvider() {
5050
var appWidgetMaxWidth: Int? = null,
5151
var appWidgetMaxHeight: Int? = null,
5252
// Cache chart to redraw on size changes
53-
var cachedChart: List<Array<Double>> = listOf()
53+
var cachedChart: List<Array<Double>> = listOf(),
54+
var cachedChartDt: Long = 0,
55+
var cachedChartCurrency: String? = null,
56+
var isShown: Boolean = false
5457
) {
5558
constructor(config: JSONObject?) : this(
5659
token = config?.optJSONObject("token"),
@@ -82,7 +85,10 @@ class PriceWidget : AppWidgetProvider() {
8285
}
8386
}
8487
chartList
85-
}()
88+
}(),
89+
cachedChartDt = config?.optLong("cachedChartDt") ?: 0,
90+
cachedChartCurrency = config?.optString("cachedChartCurrency"),
91+
isShown = config?.optBoolean("isShown") ?: false,
8692
)
8793

8894
fun toJson(): JSONObject =
@@ -99,6 +105,9 @@ class PriceWidget : AppWidgetProvider() {
99105
}
100106
}
101107
put("cachedChart", JSONArray(chartArray))
108+
put("cachedChartDt", cachedChartDt)
109+
put("cachedChartCurrency", cachedChartCurrency)
110+
put("isShown", isShown)
102111
}
103112

104113
val tokenChain: String?
@@ -177,37 +186,52 @@ class PriceWidget : AppWidgetProvider() {
177186
appWidgetManager,
178187
appWidgetId,
179188
config,
180-
WBaseStorage.getBaseCurrency()
181189
)
182190
}
183191
}
184192

185193
override fun onUpdate(
186194
context: Context,
187195
appWidgetManager: AppWidgetManager,
188-
appWidgetIds: IntArray
196+
appWidgetIds: IntArray,
197+
) {
198+
updateAppWidgets(context, appWidgetManager, appWidgetIds, null)
199+
}
200+
201+
fun updateAppWidgets(
202+
context: Context,
203+
appWidgetManager: AppWidgetManager,
204+
appWidgetIds: IntArray,
205+
onCompletion: (() -> Unit)?
189206
) {
190207
val cachedPriceChartData = mutableMapOf<String, Array<Array<Double>>>()
191208
ApplicationContextHolder.update(context.applicationContext)
192209
WBaseStorage.init(context)
193210
val baseCurrency = WBaseStorage.getBaseCurrency() ?: MBaseCurrency.USD
194211
val widgetQueue: Queue<Int> = LinkedList(appWidgetIds.toList())
195212

213+
// TODO:: Consider loading parallel since it's not common to have several widgets with same token and period.
214+
// Make sure to don't miss onCompletion?.invoke() if changed anything!
196215
fun processNextWidget() {
197-
val appWidgetId = widgetQueue.poll() ?: return
216+
val appWidgetId = widgetQueue.poll() ?: run {
217+
onCompletion?.invoke()
218+
return
219+
}
198220

199221
var config = Config(WBaseStorage.getWidgetConfigurations(appWidgetId) ?: JSONObject())
200222
if (config.appWidgetMinWidth == null || config.appWidgetMinHeight == null) {
201223
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
202-
config.appWidgetMinWidth =
203-
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
204-
config.appWidgetMinHeight =
205-
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
206-
config.appWidgetMaxWidth =
207-
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
208-
config.appWidgetMaxHeight =
209-
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
210-
WBaseStorage.setWidgetConfigurations(appWidgetId, config.toJson())
224+
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
225+
if (minWidth > 0) {
226+
config.appWidgetMinWidth = minWidth
227+
config.appWidgetMinHeight =
228+
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
229+
config.appWidgetMaxWidth =
230+
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
231+
config.appWidgetMaxHeight =
232+
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
233+
WBaseStorage.setWidgetConfigurations(appWidgetId, config.toJson())
234+
}
211235
}
212236
val period = config.period
213237
val cacheKey = "${config.assetId}_$period"
@@ -223,23 +247,28 @@ class PriceWidget : AppWidgetProvider() {
223247
appWidgetManager,
224248
appWidgetId,
225249
config,
226-
baseCurrency
227250
)
228251
processNextWidget()
229252
} ?: run {
230253
val period = config.period?.value ?: run {
231254
processNextWidget()
232255
return
233256
}
234-
if (config.cachedChart.isEmpty()) {
235-
// Show loading widget
257+
// Update with latest chart data available if base currencies match
258+
if (config.cachedChartCurrency == baseCurrency.currencyCode)
236259
updateAppWidget(
237260
context,
238261
appWidgetManager,
239262
appWidgetId,
240263
config,
241-
baseCurrency
242264
)
265+
// Ignore requesting again if it's updated less than a minute ago
266+
if (config.cachedChart.isNotEmpty() &&
267+
config.cachedChartDt > System.currentTimeMillis() - 60_000 &&
268+
config.cachedChartCurrency == baseCurrency.currencyCode
269+
) {
270+
processNextWidget()
271+
return
243272
}
244273
val assetId = config.assetId ?: DEFAULT_TOKEN
245274
SDKApiMethod.Token.PriceChart(
@@ -253,6 +282,8 @@ class PriceWidget : AppWidgetProvider() {
253282
config =
254283
Config(WBaseStorage.getWidgetConfigurations(appWidgetId)).apply {
255284
cachedChart = result.toList()
285+
cachedChartDt = System.currentTimeMillis()
286+
cachedChartCurrency = baseCurrency.currencyCode
256287
}
257288
if (config.assetId != assetId || config.period?.value != period) {
258289
processNextWidget()
@@ -264,7 +295,6 @@ class PriceWidget : AppWidgetProvider() {
264295
appWidgetManager,
265296
appWidgetId,
266297
config,
267-
baseCurrency
268298
)
269299
processNextWidget()
270300
}
@@ -284,10 +314,24 @@ class PriceWidget : AppWidgetProvider() {
284314
appWidgetManager: AppWidgetManager,
285315
appWidgetId: Int,
286316
config: Config,
287-
baseCurrency: MBaseCurrency? = null,
288317
) {
289318
val orientation = context.resources.configuration.orientation
290319
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE
320+
if (!config.isShown) {
321+
config.isShown = true
322+
WBaseStorage.setWidgetConfigurations(appWidgetId, config.toJson())
323+
appWidgetManager.updateAppWidget(
324+
appWidgetId,
325+
generateRemoteViews(
326+
context,
327+
config,
328+
if (isLandscape) config.appWidgetMaxWidth else config.appWidgetMinWidth,
329+
if (isLandscape) config.appWidgetMinHeight else config.appWidgetMaxHeight,
330+
null,
331+
appWidgetId
332+
)
333+
)
334+
}
291335
ImageUtils.loadBitmapFromUrl(
292336
context,
293337
config.token?.optString("image", ""),
@@ -297,7 +341,6 @@ class PriceWidget : AppWidgetProvider() {
297341
generateRemoteViews(
298342
context,
299343
config,
300-
baseCurrency,
301344
if (isLandscape) config.appWidgetMaxWidth else config.appWidgetMinWidth,
302345
if (isLandscape) config.appWidgetMinHeight else config.appWidgetMaxHeight,
303346
image,
@@ -311,7 +354,6 @@ class PriceWidget : AppWidgetProvider() {
311354
private fun generateRemoteViews(
312355
context: Context,
313356
config: Config,
314-
baseCurrency: MBaseCurrency?,
315357
width: Int?,
316358
height: Int?,
317359
image: Bitmap?,
@@ -328,6 +370,7 @@ class PriceWidget : AppWidgetProvider() {
328370
)
329371

330372
// PREPARE VALUES //////////////////////////////////////////////////////////////////////////
373+
val baseCurrency = config.cachedChartCurrency?.let { MBaseCurrency.parse(it) }
331374
val priceChartData = config.cachedChart.toTypedArray()
332375
val baseColor = config.token?.optString("color", DEFAULT_COLOR)?.toColorInt()
333376
?: DEFAULT_COLOR.toColorInt()
@@ -495,17 +538,18 @@ class PriceWidget : AppWidgetProvider() {
495538
height: Int?,
496539
isCompact: Boolean
497540
) {
498-
views.setImageViewBitmap(
499-
R.id.chart,
500-
ChartUtils.chartToBitmap(
501-
context,
502-
priceChartData,
503-
baseColor = baseColor,
504-
chartWidth = width?.dp ?: 200.dp,
505-
chartHeight = (height?.dp ?: 180.dp) - 130.dp,
506-
paddingBottom = if (isCompact) 77.dp else 68.dp
541+
if (width != null && height != null)
542+
views.setImageViewBitmap(
543+
R.id.chart,
544+
ChartUtils.chartToBitmap(
545+
context,
546+
priceChartData,
547+
baseColor = baseColor,
548+
chartWidth = width.dp,
549+
chartHeight = height.dp - 130.dp,
550+
paddingBottom = if (isCompact) 77.dp else 68.dp
551+
)
507552
)
508-
)
509553
}
510554

511555
override fun onDeleted(context: Context, appWidgetIds: IntArray?) {

mobile/android/air/SubModules/UIWidgetsConfigurations/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ dependencies {
3939
implementation(libs.androidx.core.ktx)
4040
implementation(libs.androidx.appcompat)
4141
implementation(libs.material)
42+
implementation(libs.androidx.work.runtime.ktx)
4243
implementation(project("$airSubModulePath:UIComponents"))
4344
implementation(project("$airSubModulePath:WalletCore"))
4445
implementation(project("$airSubModulePath:WalletContext"))
4546
implementation(project("$airSubModulePath:WalletBaseContext"))
47+
implementation(project("$airSubModulePath:WalletSDK"))
4648
implementation(project("$airSubModulePath:UIWidgets"))
4749
implementation(project("$airSubModulePath:Icons"))
4850
implementation(project("$airSubModulePath:vkryl:core"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.mytonwallet.app_air.uiwidgets.configurations
2+
3+
import android.content.Context
4+
import androidx.work.CoroutineWorker
5+
import androidx.work.WorkerParameters
6+
import kotlin.coroutines.resume
7+
import kotlin.coroutines.suspendCoroutine
8+
9+
class WidgetUpdateWorker(
10+
private val applicationContext: Context,
11+
workerParams: WorkerParameters
12+
) : CoroutineWorker(applicationContext, workerParams) {
13+
14+
override suspend fun doWork(): Result {
15+
return try {
16+
suspendCoroutine { cont ->
17+
WidgetsConfigurations.reloadPriceWidgets(applicationContext) {
18+
cont.resume(Result.success())
19+
}
20+
}
21+
} catch (_: Exception) {
22+
Result.failure()
23+
}
24+
}
25+
}

mobile/android/air/SubModules/UIWidgetsConfigurations/src/main/java/org/mytonwallet/app_air/uiwidgets/configurations/WidgetsConfigurations.kt

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,57 @@ package org.mytonwallet.app_air.uiwidgets.configurations
33
import android.appwidget.AppWidgetManager
44
import android.content.ComponentName
55
import android.content.Context
6+
import androidx.work.ExistingPeriodicWorkPolicy
7+
import androidx.work.PeriodicWorkRequest
8+
import androidx.work.PeriodicWorkRequestBuilder
9+
import androidx.work.WorkManager
10+
import org.mytonwallet.app_air.walletbasecontext.WBaseStorage
611
import org.mytonwallet.app_air.widgets.actionsWidget.ActionsWidget
712
import org.mytonwallet.app_air.widgets.priceWidget.PriceWidget
13+
import java.util.concurrent.TimeUnit
814

915
object WidgetsConfigurations {
16+
fun scheduleWidgetUpdates(context: Context) {
17+
val widgetUpdateRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
18+
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
19+
TimeUnit.MILLISECONDS
20+
).build()
21+
WorkManager.getInstance(context.applicationContext).enqueueUniquePeriodicWork(
22+
"widgetUpdateWork",
23+
ExistingPeriodicWorkPolicy.KEEP,
24+
widgetUpdateRequest
25+
)
26+
}
27+
1028
fun reloadWidgets(context: Context) {
29+
reloadActionsWidgets(context)
30+
reloadPriceWidgets(context)
31+
}
32+
33+
fun reloadActionsWidgets(context: Context) {
1134
val appWidgetManager = AppWidgetManager.getInstance(context)
1235
appWidgetManager
1336
.getAppWidgetIds(ComponentName(context, ActionsWidget::class.java))
1437
.let { appWidgetIds ->
1538
ActionsWidget().onUpdate(context, appWidgetManager, appWidgetIds)
1639
}
40+
}
41+
42+
fun reloadPriceWidgets(context: Context, onCompletion: (() -> Unit)? = null) {
43+
val appWidgetManager = AppWidgetManager.getInstance(context)
1744
appWidgetManager
1845
.getAppWidgetIds(ComponentName(context, PriceWidget::class.java))
1946
.let { appWidgetIds ->
20-
PriceWidget().onUpdate(context, appWidgetManager, appWidgetIds)
47+
PriceWidget().updateAppWidgets(
48+
context,
49+
appWidgetManager,
50+
appWidgetIds,
51+
onCompletion
52+
)
2153
}
2254
}
55+
56+
fun isWidgetConfigured(appWidgetId: Int): Boolean {
57+
return WBaseStorage.getWidgetConfigurations(appWidgetId) != null
58+
}
2359
}

0 commit comments

Comments
 (0)