Merge pull request #7440 from BlueWallet/androidwi

FIX: Android widget fixes. Allow other currencies
This commit is contained in:
GLaDOS 2025-01-02 13:09:26 +00:00 committed by GitHub
commit 636fc21f9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 167 additions and 138 deletions

View File

@ -8,28 +8,55 @@ import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
companion object {
private const val TAG = "BitcoinPriceWidget"
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
private const val WIDGET_COUNT_KEY = "widget_count"
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
Log.d("BitcoinPriceWidget", "onUpdate called")
WidgetUpdateWorker.scheduleWork(context)
for (widgetId in appWidgetIds) {
Log.d(TAG, "Updating widget with ID: $widgetId")
WidgetUpdateWorker.scheduleWork(context)
}
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
Log.d("BitcoinPriceWidget", "onEnabled called")
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 0)
if (widgetCount >= 1) {
Log.e(TAG, "Only one widget instance is allowed.")
return
}
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply()
Log.d(TAG, "onEnabled called")
WidgetUpdateWorker.scheduleWork(context)
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Log.d("BitcoinPriceWidget", "onDisabled called")
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - 1).apply()
Log.d(TAG, "onDisabled called")
clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.edit().clear().apply() // Clear all preferences in the group
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - appWidgetIds.size).apply()
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().clear().apply()
Log.d(TAG, "Cache cleared from $SHARED_PREF_NAME")
}
}

View File

@ -2,6 +2,7 @@ package io.bluewallet.bluewallet
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
@ -16,6 +17,14 @@ import com.facebook.react.modules.i18nmanager.I18nUtil
class MainApplication : Application(), ReactApplication {
private lateinit var sharedPref: SharedPreferences
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
if (key == "preferredCurrency") {
prefs.edit().remove("previous_price").apply()
WidgetUpdateWorker.scheduleWork(this)
}
}
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
@ -35,10 +44,10 @@ class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
sharedPref = getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
@ -48,14 +57,17 @@ class MainApplication : Application(), ReactApplication {
load()
}
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
initializeBugsnag()
}
// Retrieve the "donottrack" value. Default to "0" if not found.
override fun onTerminate() {
super.onTerminate()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun initializeBugsnag() {
val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
// Check if do not track is not enabled and initialize Bugsnag if so
if (isDoNotTrackEnabled != "1") {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this)
}
}

View File

@ -2,20 +2,21 @@ package io.bluewallet.bluewallet
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object MarketAPI {
private const val TAG = "MarketAPI"
private val client = OkHttpClient()
var baseUrl: String? = null
fun fetchPrice(context: Context, currency: String): String? {
suspend fun fetchPrice(context: Context, currency: String): String? {
return try {
// Load the JSON data from the assets
val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
val json = JSONObject(fiatUnitsJson)
val currencyInfo = json.getJSONObject(currency)
@ -25,26 +26,16 @@ object MarketAPI {
val urlString = buildURLString(source, endPointKey)
Log.d(TAG, "Fetching price from URL: $urlString")
val url = URL(urlString)
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.connect()
val request = Request.Builder().url(urlString).build()
val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
val responseCode = urlConnection.responseCode
if (responseCode != 200) {
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
if (!response.isSuccessful) {
Log.e(TAG, "Failed to fetch price. Response code: ${response.code}")
return null
}
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = StringBuilder()
val buffer = CharArray(1024)
var read: Int
while (reader.read(buffer).also { read = it } != -1) {
jsonResponse.append(buffer, 0, read)
}
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
val jsonResponse = response.body?.string() ?: return null
parseJSONBasedOnSource(jsonResponse, source, endPointKey)
} catch (e: Exception) {
Log.e(TAG, "Error fetching price", e)
null

View File

@ -1,8 +1,10 @@
package io.bluewallet.bluewallet
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import android.view.View
@ -13,8 +15,10 @@ import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
companion object {
const val TAG = "WidgetUpdateWorker"
@ -35,66 +39,57 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
}
private lateinit var sharedPref: SharedPreferences
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
override fun doWork(): Result {
override suspend fun doWork(): Result {
Log.d(TAG, "Widget update worker running")
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
registerPreferenceChangeListener()
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 intent = Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent)
// Show loading indicator
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
views.setViewVisibility(R.id.price_value, View.GONE)
views.setViewVisibility(R.id.last_updated_label, View.GONE)
views.setViewVisibility(R.id.last_updated_time, View.GONE)
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
appWidgetManager.updateAppWidget(appWidgetIds, views)
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
}
val fetchedPrice = fetchPrice(preferredCurrency)
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
return Result.success()
}
private fun registerPreferenceChangeListener() {
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") {
Log.d(TAG, "Preference changed: $key")
updateWidgetOnPreferenceChange()
}
}
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onStopped() {
super.onStopped()
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private fun updateWidgetOnPreferenceChange() {
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 preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
val previousPrice = sharedPref.getString("previous_price", null)
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
fetchPrice(preferredCurrency) { fetchedPrice, error ->
handlePriceResult(
appWidgetManager, appWidgetIds, views, sharedPref,
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
)
private suspend fun fetchPrice(currency: String?): String? {
return withContext(Dispatchers.IO) {
MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
}
}
@ -107,24 +102,27 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
previousPrice: String?,
currentTime: String,
preferredCurrency: String?,
preferredCurrencyLocale: String?,
error: String?
preferredCurrencyLocale: String?
) {
val isPriceFetched = fetchedPrice != null
val isPriceCached = previousPrice != null
if (error != null || !isPriceFetched) {
Log.e(TAG, "Error fetching price: $error")
if (!isPriceFetched) {
Log.e(TAG, "Error fetching price.")
if (!isPriceCached) {
showLoadingError(views)
} else {
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
}
} else {
displayFetchedPrice(
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
savePrice(sharedPref, fetchedPrice)
if (fetchedPrice != null) {
displayFetchedPrice(
views, fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
)
}
if (fetchedPrice != null) {
savePrice(sharedPref, fetchedPrice)
}
}
appWidgetManager.updateAppWidget(appWidgetIds, views)
@ -132,7 +130,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
private fun showLoadingError(views: RemoteViews) {
views.apply {
setViewVisibility(R.id.loading_indicator, View.VISIBLE)
setViewVisibility(R.id.loading_indicator, View.GONE)
setViewVisibility(R.id.price_value, View.GONE)
setViewVisibility(R.id.last_updated_label, View.GONE)
setViewVisibility(R.id.last_updated_time, View.GONE)
@ -216,15 +214,6 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
return currencyFormat
}
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
val price = MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
if (price == null) {
callback(null, "Failed to fetch price")
} else {
callback(price, null)
}
}
private fun savePrice(sharedPref: SharedPreferences, price: String) {
sharedPref.edit().putString("previous_price", price).apply()
}

View File

@ -13,13 +13,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
android:visibility="visible"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_gravity="end">
<TextView
android:id="@+id/last_updated_label"
style="@style/WidgetTextSecondary"
@ -29,69 +30,78 @@
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
<TextView
android:id="@+id/last_updated_time"
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:text="--:--"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:visibility="gone"/>
android:visibility="gone"
android:layout_gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
android:gravity="end"
android:layout_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"
android:visibility="gone"/>
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone">
<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:text="Loading..."
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/previous_price"
style="@style/WidgetTextPrimary"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:layout_gravity="end"/>
<LinearLayout
android:id="@+id/price_arrow_container"
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>
android:orientation="horizontal"
android:gravity="end"
android:visibility="gone"
android:layout_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"
android:layout_gravity="end"/>
<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"
android:layout_gravity="end"/>
<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"
android:layout_gravity="end"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>