This commit is contained in:
Marcos Rodriguez Velez 2024-07-10 23:08:53 -04:00
parent 3e394373e5
commit c3418d0678
No known key found for this signature in database
GPG key ID: 6030B2F48CCE86D7
5 changed files with 169 additions and 174 deletions

View file

@ -7,33 +7,26 @@ import android.widget.RemoteViews
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.testing.WorkManagerTestInitHelper
import androidx.work.WorkManager
import com.google.common.util.concurrent.ListenableFuture
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.json.JSONObject
import com.squareup.okhttp.mockwebserver.MockResponse
import com.squareup.okhttp.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.text.NumberFormat
import java.util.*
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class MockServerTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
mockWebServer = MockWebServer()
mockWebServer.start()
val baseUrl = mockWebServer.url("/").toString()
MarketAPI.baseUrl = baseUrl
WorkManagerTestInitHelper.initializeTestWorkManager(context)
MarketAPI.baseUrl = mockWebServer.url("/").toString()
}
@After
@ -43,45 +36,39 @@ class MockServerTest {
@Test
fun testWidgetUpdate() {
// Mock API response
val mockResponse = MockResponse()
.setBody(createMockJsonResponse("USD", "61500"))
.setResponseCode(200)
mockWebServer.enqueue(mockResponse)
val context = ApplicationProvider.getApplicationContext<Context>()
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val prices = listOf("60000", "60500", "61000", "61500", "62000")
prices.forEach {
mockWebServer.enqueue(MockResponse().setBody("""{"USD": {"price": "$it"}}"""))
}
// Trigger the widget update
WidgetUpdateWorker.scheduleWork(context)
WorkManagerTestInitHelper.getTestDriver()?.setAllConstraintsMet(WidgetUpdateWorker.WORK_NAME)
// Wait for the worker to run
val workInfos = WorkManager.getInstance(context).getWorkInfosByTag(WidgetUpdateWorker.WORK_NAME).get()
val testDriver = WorkManagerTestInitHelper.getTestDriver()
for (workInfo in workInfos) {
testDriver?.setAllConstraintsMet(workInfo.id)
}
Thread.sleep(10000) // Wait for 10 seconds to simulate the update interval
// Validate the widget updates
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, BitcoinPriceWidget::class.java))
val thisWidget = ComponentName(context, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(context.packageName, R.layout.widget_layout)
for (widgetId in widgetIds) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
// Add your assertions here to validate the widget update
}
}
private fun createMockJsonResponse(currency: String, price: String): String {
return """
{
"$currency": {
"endPointKey": "$currency",
"locale": "en-US",
"source": "Kraken",
"symbol": "$",
"country": "United States (US Dollar)",
"price": "$price"
}
prices.forEachIndexed { index, price ->
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
maximumFractionDigits = 0
}
""".trimIndent()
val formattedPrice = currencyFormat.format(price.toDouble())
views.setTextViewText(R.id.price_value, formattedPrice)
views.setTextViewText(R.id.last_updated_time, SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date()))
if (index > 0) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
views.setTextViewText(R.id.previous_price, currencyFormat.format(prices[index - 1].toDouble()))
} else {
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
Thread.sleep(5000) // Wait for 5 seconds before updating to the next price
}
}
}

View file

@ -8,65 +8,77 @@ import android.view.View
import android.widget.RemoteViews
import androidx.work.*
import java.text.NumberFormat
import java.util.Locale
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
class WidgetUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
const val WORK_NAME = "widget_update_work"
const val REPEAT_INTERVAL_MINUTES = 15L
fun scheduleWork(context: Context) {
val workRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
REPEAT_INTERVAL_MINUTES, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
)
Log.d(TAG, "Scheduling work for widget updates, will run every $REPEAT_INTERVAL_MINUTES minutes")
}
}
override fun doWork(): Result {
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val widgetComponent = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val allWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
Log.d(TAG, "Widget update worker running")
val price = MarketAPI.fetchPrice(applicationContext, "USD")
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
if (price != null) {
val formattedPrice = NumberFormat.getCurrencyInstance(Locale.getDefault()).format(price.toDouble())
// Simulate fetching price data
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
val price = fetchPrice() // Simulated method to fetch the price
val previousPrice = getPreviousPrice() // Simulated method to fetch the previous price
views.setTextViewText(R.id.price_value, formattedPrice)
views.setViewVisibility(R.id.loading_indicator, View.GONE)
views.setViewVisibility(R.id.price_value, View.VISIBLE)
views.setViewVisibility(R.id.last_updated, View.VISIBLE)
// Log fetched data
Log.d(TAG, "Fetch completed with price: $price at $currentTime. Previous price: $previousPrice")
val currentTime = System.currentTimeMillis()
val formattedTime = java.text.DateFormat.getTimeInstance().format(currentTime)
views.setTextViewText(R.id.last_updated, "Last Updated: $formattedTime")
// Update views
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
maximumFractionDigits = 0
}
views.setTextViewText(R.id.price_value, currencyFormat.format(price.toDouble()))
views.setTextViewText(R.id.last_updated_time, currentTime)
if (previousPrice != null) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE)
views.setTextViewText(R.id.previous_price, currencyFormat.format(previousPrice.toDouble()))
if (price.toDouble() > previousPrice.toDouble()) {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float)
} else {
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float)
}
} else {
Log.d("WidgetUpdateWorker", "Failed to fetch price")
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
}
for (widgetId in allWidgetIds) {
appWidgetManager.updateAppWidget(widgetId, views)
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
scheduleNextUpdate()
return Result.success()
}
private fun scheduleNextUpdate() {
val request = OneTimeWorkRequestBuilder<WidgetUpdateWorker>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork(
"widget_update_work",
ExistingWorkPolicy.REPLACE,
request
)
Log.d("WidgetUpdateWorker", "Scheduled next update for widget")
private fun fetchPrice(): String {
// Simulate a network call to fetch the price
return (60000 + Random().nextInt(5000)).toString() // Replace with actual fetch logic
}
companion object {
fun createWorkRequest(): OneTimeWorkRequest {
return OneTimeWorkRequestBuilder<WidgetUpdateWorker>()
.setInitialDelay(0, TimeUnit.SECONDS)
.build()
}
fun scheduleWork(context: Context) {
WorkManager.getInstance(context).enqueue(createWorkRequest())
}
private fun getPreviousPrice(): String? {
// Simulate retrieving the previous price from shared preferences or a database
return (60000 + Random().nextInt(5000)).toString() // Replace with actual retrieval logic
}
}

View file

@ -1,57 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:background="@drawable/widget_background"
android:layout_margin="16dp"
android:orientation="vertical">
android:gravity="end">
<TextView
android:id="@+id/price_value"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textSize="18sp"
android:textColor="@color/text_primary"
android:layout_centerHorizontal="true"/>
android:orientation="vertical"
android:gravity="end">
<TextView
android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Last Updated"
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/last_updated"
android:layout_width="wrap_content"
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/last_updated"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_below="@id/price_value"
android:layout_centerHorizontal="true"
android:visibility="gone"
android:layout_marginTop="8dp"/>
android:orientation="vertical"
android:gravity="end">
<TextView
android:id="@+id/price_value"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price_label"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="From:"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:visibility="gone"/>
<ImageView
android:id="@+id/price_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/price_value"
android:layout_marginTop="4dp"
android:layout_centerHorizontal="true"
android:visibility="gone"/>
<TextView
android:id="@+id/previous_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_below="@id/price_arrow"
android:layout_centerHorizontal="true"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>

View file

@ -2,7 +2,7 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="130dp"
android:minHeight="40dp"
android:minHeight="80dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen"
android:previewImage="@drawable/widget_preview"

View file

@ -1,42 +0,0 @@
{
"originHash" : "52530e6b1e3a85c8854952ef703a6d1bbe1acd82713be2b3166476b9b277db23",
"pins" : [
{
"identity" : "bugsnag-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/bugsnag/bugsnag-cocoa",
"state" : {
"revision" : "16b9145fc66e5296f16e733f6feb5d0e450574e8",
"version" : "6.28.1"
}
},
{
"identity" : "efqrcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/EFPrefix/EFQRCode.git",
"state" : {
"revision" : "2991c2f318ad9529d93b2a73a382a3f9c72c64ce",
"version" : "6.2.2"
}
},
{
"identity" : "keychain-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/evgenyneu/keychain-swift.git",
"state" : {
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608",
"version" : "24.0.0"
}
},
{
"identity" : "swift_qrcodejs",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ApolloZhu/swift_qrcodejs.git",
"state" : {
"revision" : "374dc7f7b9e76c6aeb393f6a84590c6d387e1ecb",
"version" : "2.2.2"
}
}
],
"version" : 3
}