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.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.testing.WorkManagerTestInitHelper import androidx.work.testing.WorkManagerTestInitHelper
import androidx.work.WorkManager import com.squareup.okhttp.mockwebserver.MockResponse
import com.google.common.util.concurrent.ListenableFuture import com.squareup.okhttp.mockwebserver.MockWebServer
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.json.JSONObject
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.text.NumberFormat
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MockServerTest { class MockServerTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private lateinit var context: Context
@Before @Before
fun setUp() { fun setUp() {
context = ApplicationProvider.getApplicationContext()
mockWebServer = MockWebServer() mockWebServer = MockWebServer()
mockWebServer.start() mockWebServer.start()
MarketAPI.baseUrl = mockWebServer.url("/").toString()
val baseUrl = mockWebServer.url("/").toString()
MarketAPI.baseUrl = baseUrl
WorkManagerTestInitHelper.initializeTestWorkManager(context)
} }
@After @After
@ -43,45 +36,39 @@ class MockServerTest {
@Test @Test
fun testWidgetUpdate() { fun testWidgetUpdate() {
// Mock API response val context = ApplicationProvider.getApplicationContext<Context>()
val mockResponse = MockResponse() WorkManagerTestInitHelper.initializeTestWorkManager(context)
.setBody(createMockJsonResponse("USD", "61500"))
.setResponseCode(200) val prices = listOf("60000", "60500", "61000", "61500", "62000")
mockWebServer.enqueue(mockResponse) prices.forEach {
mockWebServer.enqueue(MockResponse().setBody("""{"USD": {"price": "$it"}}"""))
}
// Trigger the widget update
WidgetUpdateWorker.scheduleWork(context) 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 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) { prices.forEachIndexed { index, price ->
val views = RemoteViews(context.packageName, R.layout.widget_layout) val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
// Add your assertions here to validate the widget update maximumFractionDigits = 0
}
}
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"
}
} }
""".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 android.widget.RemoteViews
import androidx.work.* import androidx.work.*
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit 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 { override fun doWork(): Result {
val appWidgetManager = AppWidgetManager.getInstance(applicationContext) Log.d(TAG, "Widget update worker running")
val widgetComponent = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
val allWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
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) val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
if (price != null) { // Simulate fetching price data
val formattedPrice = NumberFormat.getCurrencyInstance(Locale.getDefault()).format(price.toDouble()) 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) // Log fetched data
views.setViewVisibility(R.id.loading_indicator, View.GONE) Log.d(TAG, "Fetch completed with price: $price at $currentTime. Previous price: $previousPrice")
views.setViewVisibility(R.id.price_value, View.VISIBLE)
views.setViewVisibility(R.id.last_updated, View.VISIBLE)
val currentTime = System.currentTimeMillis() // Update views
val formattedTime = java.text.DateFormat.getTimeInstance().format(currentTime) val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault()).apply {
views.setTextViewText(R.id.last_updated, "Last Updated: $formattedTime") 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 { } else {
Log.d("WidgetUpdateWorker", "Failed to fetch price") views.setViewVisibility(R.id.price_arrow_container, View.GONE)
} }
for (widgetId in allWidgetIds) { appWidgetManager.updateAppWidget(appWidgetIds, views)
appWidgetManager.updateAppWidget(widgetId, views)
}
scheduleNextUpdate()
return Result.success() return Result.success()
} }
private fun scheduleNextUpdate() { private fun fetchPrice(): String {
val request = OneTimeWorkRequestBuilder<WidgetUpdateWorker>() // Simulate a network call to fetch the price
.setInitialDelay(10, TimeUnit.MINUTES) return (60000 + Random().nextInt(5000)).toString() // Replace with actual fetch logic
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork(
"widget_update_work",
ExistingWorkPolicy.REPLACE,
request
)
Log.d("WidgetUpdateWorker", "Scheduled next update for widget")
} }
companion object { private fun getPreviousPrice(): String? {
fun createWorkRequest(): OneTimeWorkRequest { // Simulate retrieving the previous price from shared preferences or a database
return OneTimeWorkRequestBuilder<WidgetUpdateWorker>() return (60000 + Random().nextInt(5000)).toString() // Replace with actual retrieval logic
.setInitialDelay(0, TimeUnit.SECONDS)
.build()
}
fun scheduleWork(context: Context) {
WorkManager.getInstance(context).enqueue(createWorkRequest())
}
} }
} }

View file

@ -1,57 +1,95 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:background="@drawable/widget_background" android:background="@drawable/widget_background"
android:layout_margin="16dp" android:gravity="end">
android:orientation="vertical">
<TextView <LinearLayout
android:id="@+id/price_value" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/loading" android:orientation="vertical"
android:textSize="18sp" android:gravity="end">
android:textColor="@color/text_primary" <TextView
android:layout_centerHorizontal="true"/> 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 <TextView
android:id="@+id/last_updated" android:id="@+id/last_updated_time"
android:layout_width="wrap_content" 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:layout_height="wrap_content"
android:text="@string/last_updated" android:orientation="vertical"
android:textSize="12sp" android:gravity="end">
android:textColor="@color/text_secondary"
android:layout_below="@id/price_value" <TextView
android:layout_centerHorizontal="true" android:id="@+id/price_value"
android:visibility="gone" style="@style/WidgetTextPrimary"
android:layout_marginTop="8dp"/> 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 <ProgressBar
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_gravity="center"
android:layout_centerVertical="true"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout>
<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>

View file

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