diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 0a64ad4a7..0c4927bcc 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools"> - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 99628da25..b07ba48de 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ - + - - - - - + android:name=".BitcoinPriceWidget" + android:exported="true" + android:label="Bitcoin Price Widget"> + + + + + 0) { + for (int appWidgetId : appWidgetIds) { + Log.d(TAG, "Updating widget ID: " + appWidgetId); - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); - // Set up the pending intent to open the app when the widget is clicked - Intent launchAppIntent = new Intent(context, MainActivity.class); - PendingIntent launchAppPendingIntent = PendingIntent.getActivity(context, 0, launchAppIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - views.setOnClickPendingIntent(R.id.widget_layout, launchAppPendingIntent); + // Set up the pending intent to open the app when the widget is clicked + Intent launchAppIntent = new Intent(context, MainActivity.class); + PendingIntent launchAppPendingIntent = PendingIntent.getActivity(context, 0, launchAppIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widget_layout, launchAppPendingIntent); - appWidgetManager.updateAppWidget(appWidgetId, views); + // Set the loading indicator visible initially + views.setViewVisibility(R.id.loading_indicator, View.VISIBLE); + views.setViewVisibility(R.id.price_value, View.GONE); + views.setViewVisibility(R.id.last_updated, View.GONE); + views.setViewVisibility(R.id.last_updated_time, View.GONE); - executorService.execute(new FetchBitcoinPriceTask(context, appWidgetManager, appWidgetId)); - } + appWidgetManager.updateAppWidget(appWidgetId, views); - scheduleNextUpdate(context); - } - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - if (ACTION_UPDATE.equals(intent.getAction())) { - Log.d(TAG, "Received update action"); - - ComponentName widget = new ComponentName(context, BitcoinPriceWidget.class); - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - int[] appWidgetIds = appWidgetManager.getAppWidgetIds(widget); - onUpdate(context, appWidgetManager, appWidgetIds); - } - } - - private void scheduleNextUpdate(final Context context) { - handler.postDelayed(new Runnable() { - @Override - public void run() { - Intent intent = new Intent(context, BitcoinPriceWidget.class); - intent.setAction(ACTION_UPDATE); - context.sendBroadcast(intent); - scheduleNextUpdate(context); + executorService.execute(new FetchBitcoinPriceTask(context, appWidgetManager, appWidgetId)); } - }, 10 * 60 * 1000); // Update every 10 minutes + + scheduleNextUpdate(context); + } + } + + private void initializeExecutorService() { + if (executorService == null || executorService.isShutdown()) { + executorService = Executors.newSingleThreadExecutor(); + } + } + + private void scheduleNextUpdate(Context context) { + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(UpdateWidgetWorker.class) + .setInitialDelay(UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES) + .build(); + + WorkManager.getInstance(context).enqueue(workRequest); } private static class FetchBitcoinPriceTask implements Runnable { @@ -106,7 +109,9 @@ public class BitcoinPriceWidget extends AppWidgetProvider { Log.d(TAG, "Starting to fetch Bitcoin price..."); - String price = MarketAPI.fetchPrice("USD"); // Using hardcoded "USD" for now + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String preferredCurrency = prefs.getString("preferredCurrency", "USD"); + String price = MarketAPI.fetchPrice(context, preferredCurrency); if (price != null) { updateWidgetWithPrice(context, price); @@ -118,47 +123,63 @@ public class BitcoinPriceWidget extends AppWidgetProvider { private void updateWidgetWithPrice(Context context, String price) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); - String prevPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null); + + // Fetch current and previous data + String prevPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_prev_price", "N/A"); + String prevTime = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_prev_time", "N/A"); + String currentPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_current_price", null); + String currentTime = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_current_time", null); + SharedPreferences.Editor editor = prefs.edit(); - Log.d(TAG, "Fetch completed with price: " + price); + String newTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date()); + Log.d(TAG, "Fetch completed with price: " + price + " at " + newTime + ". Previous price: " + prevPrice + " at " + prevTime); NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.US); views.setTextViewText(R.id.price_value, currencyFormat.format(Double.parseDouble(price))); - if (prevPrice != null) { - double previousPrice = Double.parseDouble(prevPrice); - double currentPrice = Double.parseDouble(price); - if (currentPrice > previousPrice) { + if (currentPrice != null) { + double previousPrice = Double.parseDouble(currentPrice); + double newPrice = Double.parseDouble(price); + if (newPrice > previousPrice) { views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float); - } else if (currentPrice < previousPrice) { + } else if (newPrice < previousPrice) { views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float); } else { views.setImageViewResource(R.id.price_arrow, 0); } - if (currentPrice != previousPrice) { + if (newPrice != previousPrice) { views.setTextViewText(R.id.previous_price, "from " + currencyFormat.format(previousPrice)); - views.setViewVisibility(R.id.price_arrow, View.VISIBLE); + views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE); views.setViewVisibility(R.id.previous_price, View.VISIBLE); } else { views.setTextViewText(R.id.previous_price, ""); - views.setViewVisibility(R.id.price_arrow, View.GONE); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); views.setViewVisibility(R.id.previous_price, View.GONE); } } else { views.setImageViewResource(R.id.price_arrow, 0); views.setTextViewText(R.id.previous_price, ""); - views.setViewVisibility(R.id.price_arrow, View.GONE); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); views.setViewVisibility(R.id.previous_price, View.GONE); } - editor.putString(PREF_PREFIX_KEY + appWidgetId, price); + // Shift current to previous + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_prev_price", currentPrice); + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_prev_time", currentTime); + + // Set new current + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_current_price", price); + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_current_time", newTime); editor.apply(); - DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()); - String currentTime = timeFormat.format(new Date()); views.setTextViewText(R.id.last_updated, "Last Updated"); - views.setTextViewText(R.id.last_updated_time, currentTime); + views.setTextViewText(R.id.last_updated_time, newTime); + + views.setViewVisibility(R.id.loading_indicator, View.GONE); + views.setViewVisibility(R.id.price_value, View.VISIBLE); + views.setViewVisibility(R.id.last_updated, View.VISIBLE); + views.setViewVisibility(R.id.last_updated_time, View.VISIBLE); appWidgetManager.updateAppWidget(appWidgetId, views); } @@ -172,10 +193,13 @@ public class BitcoinPriceWidget extends AppWidgetProvider { views.setTextViewText(R.id.last_updated_time, ""); views.setImageViewResource(R.id.price_arrow, 0); views.setTextViewText(R.id.previous_price, ""); - views.setViewVisibility(R.id.price_arrow, View.GONE); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); views.setViewVisibility(R.id.previous_price, View.GONE); - Toast.makeText(context, "Failed to fetch Bitcoin price", Toast.LENGTH_SHORT).show(); + views.setViewVisibility(R.id.loading_indicator, View.GONE); + appWidgetManager.updateAppWidget(appWidgetId, views); + + Log.e(TAG, "Failed to fetch Bitcoin price"); } } } diff --git a/android/app/src/main/java/io/bluewallet/bluewallet/Constants.java b/android/app/src/main/java/io/bluewallet/bluewallet/Constants.java new file mode 100644 index 000000000..693b34878 --- /dev/null +++ b/android/app/src/main/java/io/bluewallet/bluewallet/Constants.java @@ -0,0 +1,5 @@ +package io.bluewallet.bluewallet; + +public class Constants { + public static final int UPDATE_INTERVAL_MINUTES = 3; +} \ No newline at end of file diff --git a/android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java b/android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java index fc2c3976c..1c0886f73 100644 --- a/android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java +++ b/android/app/src/main/java/io/bluewallet/bluewallet/MainApplication.java @@ -16,6 +16,10 @@ import com.facebook.soloader.SoLoader; import java.lang.reflect.InvocationTargetException; import com.facebook.react.modules.i18nmanager.I18nUtil; import java.util.List; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import java.util.concurrent.TimeUnit; public class MainApplication extends Application implements ReactApplication { @@ -75,5 +79,8 @@ public class MainApplication extends Application implements ReactApplication { // Initialize Bugsnag or your error tracking here Bugsnag.start(this); } + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(UpdateWidgetWorker.class, 10, TimeUnit.MINUTES) + .build(); +WorkManager.getInstance(this).enqueueUniquePeriodicWork("UpdateWidgetWork", ExistingPeriodicWorkPolicy.REPLACE, workRequest); } } diff --git a/android/app/src/main/java/io/bluewallet/bluewallet/MarketAPI.java b/android/app/src/main/java/io/bluewallet/bluewallet/MarketAPI.java index 69e6ff968..bda8a3241 100644 --- a/android/app/src/main/java/io/bluewallet/bluewallet/MarketAPI.java +++ b/android/app/src/main/java/io/bluewallet/bluewallet/MarketAPI.java @@ -1,5 +1,7 @@ package io.bluewallet.bluewallet; +import android.content.Context; + import org.json.JSONArray; import org.json.JSONObject; @@ -10,19 +12,20 @@ import java.net.URL; public class MarketAPI { - private static final String HARD_CODED_JSON = "{\n" + - " \"USD\": {\n" + - " \"endPointKey\": \"USD\",\n" + - " \"locale\": \"en-US\",\n" + - " \"source\": \"Kraken\",\n" + - " \"symbol\": \"$\",\n" + - " \"country\": \"United States (US Dollar)\"\n" + - " }\n" + - "}"; - - public static String fetchPrice(String currency) { + public static String fetchPrice(Context context, String currency) { try { - JSONObject json = new JSONObject(HARD_CODED_JSON); + // Load JSON from assets + InputStreamReader isr = new InputStreamReader(context.getAssets().open("fiatUnits.json")); + StringBuilder jsonBuilder = new StringBuilder(); + char[] buffer = new char[1024]; + int length; + while ((length = isr.read(buffer)) != -1) { + jsonBuilder.append(buffer, 0, length); + } + isr.close(); + + String jsonString = jsonBuilder.toString(); + JSONObject json = new JSONObject(jsonString); JSONObject currencyInfo = json.getJSONObject(currency); String source = currencyInfo.getString("source"); String endPointKey = currencyInfo.getString("endPointKey"); @@ -42,9 +45,9 @@ public class MarketAPI { InputStreamReader reader = new InputStreamReader(urlConnection.getInputStream()); StringBuilder jsonResponse = new StringBuilder(); int read; - char[] buffer = new char[1024]; - while ((read = reader.read(buffer)) != -1) { - jsonResponse.append(buffer, 0, read); + char[] buffer2 = new char[1024]; + while ((read = reader.read(buffer2)) != -1) { + jsonResponse.append(buffer2, 0, read); } return parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey); diff --git a/android/app/src/main/java/io/bluewallet/bluewallet/UpdateWidgetWorker.java b/android/app/src/main/java/io/bluewallet/bluewallet/UpdateWidgetWorker.java new file mode 100644 index 000000000..272098960 --- /dev/null +++ b/android/app/src/main/java/io/bluewallet/bluewallet/UpdateWidgetWorker.java @@ -0,0 +1,149 @@ +package io.bluewallet.bluewallet; + +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +public class UpdateWidgetWorker extends Worker { + + private static final String TAG = "UpdateWidgetWorker"; + private static final String PREFS_NAME = "BitcoinPriceWidgetPrefs"; + private static final String PREF_PREFIX_KEY = "appwidget_"; + private static final int UPDATE_INTERVAL_MINUTES = 10; // Adjustable interval in minutes + + public UpdateWidgetWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Context context = getApplicationContext(); + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + ComponentName widget = new ComponentName(context, BitcoinPriceWidget.class); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(widget); + + for (int appWidgetId : appWidgetIds) { + fetchAndUpdatePrice(context, appWidgetManager, appWidgetId); + } + + scheduleNextUpdate(context); // Schedule the next update + + return Result.success(); + } + + private void fetchAndUpdatePrice(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { + Log.d(TAG, "Fetching Bitcoin price..."); + + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String preferredCurrency = prefs.getString("preferredCurrency", "USD"); + String price = MarketAPI.fetchPrice(context, preferredCurrency); + + if (price != null) { + updateWidgetWithPrice(context, appWidgetManager, appWidgetId, price); + } else { + handleError(context, appWidgetManager, appWidgetId); + } + } + + private void updateWidgetWithPrice(Context context, AppWidgetManager appWidgetManager, int appWidgetId, String price) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0); + + // Fetch current and previous data + String prevPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_prev_price", "N/A"); + String prevTime = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_prev_time", "N/A"); + String currentPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_current_price", null); + String currentTime = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_current_time", null); + + SharedPreferences.Editor editor = prefs.edit(); + + String newTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date()); + Log.d(TAG, "Fetch completed with price: " + price + " at " + newTime + ". Previous price: " + prevPrice + " at " + prevTime); + NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.US); + views.setTextViewText(R.id.price_value, currencyFormat.format(Double.parseDouble(price))); + + if (currentPrice != null) { + double previousPrice = Double.parseDouble(currentPrice); + double newPrice = Double.parseDouble(price); + if (newPrice > previousPrice) { + views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float); + } else if (newPrice < previousPrice) { + views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float); + } else { + views.setImageViewResource(R.id.price_arrow, 0); + } + + if (newPrice != previousPrice) { + views.setTextViewText(R.id.previous_price, "from " + currencyFormat.format(previousPrice)); + views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE); + views.setViewVisibility(R.id.previous_price, View.VISIBLE); + } else { + views.setTextViewText(R.id.previous_price, ""); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); + views.setViewVisibility(R.id.previous_price, View.GONE); + } + } else { + views.setImageViewResource(R.id.price_arrow, 0); + views.setTextViewText(R.id.previous_price, ""); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); + views.setViewVisibility(R.id.previous_price, View.GONE); + } + + // Shift current to previous + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_prev_price", currentPrice); + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_prev_time", currentTime); + + // Set new current + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_current_price", price); + editor.putString(PREF_PREFIX_KEY + appWidgetId + "_current_time", newTime); + editor.apply(); + + views.setTextViewText(R.id.last_updated, "Last Updated"); + views.setTextViewText(R.id.last_updated_time, newTime); + + views.setViewVisibility(R.id.loading_indicator, View.GONE); + views.setViewVisibility(R.id.price_value, View.VISIBLE); + views.setViewVisibility(R.id.last_updated, View.VISIBLE); + views.setViewVisibility(R.id.last_updated_time, View.VISIBLE); + + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + private void handleError(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); + String errorMessage = "Network Error"; + Log.e(TAG, errorMessage); + views.setTextViewText(R.id.price_value, errorMessage); + views.setTextViewText(R.id.last_updated, ""); + views.setTextViewText(R.id.last_updated_time, ""); + views.setImageViewResource(R.id.price_arrow, 0); + views.setTextViewText(R.id.previous_price, ""); + views.setViewVisibility(R.id.price_arrow_container, View.GONE); + views.setViewVisibility(R.id.previous_price, View.GONE); + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + private void scheduleNextUpdate(Context context) { + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(UpdateWidgetWorker.class) + .setInitialDelay(UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES) + .build(); + WorkManager.getInstance(context).enqueue(workRequest); + } +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/widget_layout.xml b/android/app/src/main/res/layout/widget_layout.xml index 9f63ed75b..f30c0392f 100644 --- a/android/app/src/main/res/layout/widget_layout.xml +++ b/android/app/src/main/res/layout/widget_layout.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" android:orientation="vertical" android:gravity="end"> - + + android:layout_marginEnd="8dp" /> + android:layout_marginTop="2dp" /> + android:layout_marginBottom="8dp" /> + android:layout_marginBottom="8dp" /> + android:layout_marginBottom="8dp" /> + + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 43148ce9c..bb89cac3b 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ -#Tue Jul 21 23:04:55 CDT 2020 +#Wed Jun 26 20:30:20 AST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip -networkTimeout=10000 \ No newline at end of file