REF: Android Widget to Kotlin

This commit is contained in:
Marcos Rodriguez Velez 2024-07-07 14:42:03 -04:00
parent f151668775
commit 27e99affd3
No known key found for this signature in database
GPG key ID: 6030B2F48CCE86D7
9 changed files with 249 additions and 365 deletions

View file

@ -1,125 +0,0 @@
package io.bluewallet.bluewallet;
import android.app.Application;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.PowerManager;
import android.util.Log;
import android.widget.RemoteViews;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class BitcoinPriceWidget extends AppWidgetProvider {
private static final String TAG = "BitcoinPriceWidget";
private static final String ACTION_UPDATE = "io.bluewallet.bluewallet.UPDATE_WIDGET";
private static final long UPDATE_INTERVAL_MINUTES = 15; // Update interval in minutes
private static final int MAX_RETRIES = 3;
private static PowerManager.WakeLock wakeLock;
private static int retryCount = 0;
private static boolean isScreenOn = true;
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
registerScreenReceiver(context);
schedulePeriodicUpdates(context);
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
unregisterScreenReceiver(context);
WorkManager.getInstance(context).cancelUniqueWork("UpdateWidgetWork");
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
if (isScreenOn) {
scheduleWork(context);
}
}
private void schedulePeriodicUpdates(Context context) {
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(WidgetUpdateWorker.class,
UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES)
.setInitialDelay(UPDATE_INTERVAL_MINUTES, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"UpdateWidgetWork",
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
);
}
private void registerScreenReceiver(Context context) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
context.getApplicationContext().registerReceiver(screenReceiver, filter);
}
private void unregisterScreenReceiver(Context context) {
context.getApplicationContext().unregisterReceiver(screenReceiver);
}
private final BroadcastReceiver screenReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() != null) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
switch (intent.getAction()) {
case Intent.ACTION_SCREEN_ON:
isScreenOn = true;
Log.d(TAG, "Screen ON");
acquireWakeLock(context);
scheduleWork(context);
break;
case Intent.ACTION_SCREEN_OFF:
isScreenOn = false;
Log.d(TAG, "Screen OFF");
releaseWakeLock();
break;
}
}
}
}
};
private void acquireWakeLock(Context context) {
if (wakeLock == null) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/);
}
}
}
private void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
private void scheduleWork(Context context) {
Log.d(TAG, "Scheduling work for widget update");
WorkManager.getInstance(context).enqueue(WidgetUpdateWorker.createWorkRequest());
}
}

View file

@ -0,0 +1,35 @@
package io.bluewallet.bluewallet
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import android.widget.RemoteViews
import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
override fun onEnabled(context: Context) {
super.onEnabled(context)
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context.packageName, javaClass.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
appWidgetIds.forEach { appWidgetId ->
WorkManager.getInstance(context).enqueue(WidgetUpdateWorker.createWorkRequest(appWidgetId))
}
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
appWidgetIds.forEach { appWidgetId ->
WorkManager.getInstance(context).enqueue(WidgetUpdateWorker.createWorkRequest(appWidgetId))
}
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
WorkManager.getInstance(context).cancelAllWorkByTag(javaClass.name)
}
}

View file

@ -1,42 +0,0 @@
package io.bluewallet.bluewallet;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "BlueWallet";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
}

View file

@ -0,0 +1,39 @@
package io.bluewallet.bluewallet
import android.content.pm.ActivityInfo
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
override fun getMainComponentName(): String {
return "BlueWallet"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
if (resources.getBoolean(R.bool.portrait_only)) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
/**
* Returns the instance of the [ReactActivityDelegate]. Here we use a util class [DefaultReactActivityDelegate]
* which allows you to easily enable Fabric and Concurrent React (aka React 18) with two boolean flags.
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return DefaultReactActivityDelegate(
this,
mainComponentName,
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.fabricEnabled
)
}
}

View file

@ -1,85 +0,0 @@
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;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
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 {
private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
sharedI18nUtilInstance.allowRTL(getApplicationContext(), true);
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
if (!isDoNotTrackEnabled.equals("1")) {
Bugsnag.start(this);
}
// Schedule periodic widget updates
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(WidgetUpdateWorker.class, 4, TimeUnit.MINUTES).build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork("UpdateWidgetWork", ExistingPeriodicWorkPolicy.REPLACE, workRequest);
}
}

View file

@ -0,0 +1,61 @@
package io.bluewallet.bluewallet
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.bugsnag.android.Bugsnag
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import com.facebook.react.modules.i18nmanager.I18nUtil
import java.util.concurrent.TimeUnit
class MainApplication : Application(), ReactApplication {
private val mReactNativeHost = object : DefaultReactNativeHost(this) {
override fun getUseDeveloperSupport() = BuildConfig.DEBUG
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
return packages
}
override fun getJSMainModuleName() = "index"
override fun isNewArchEnabled() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override fun isHermesEnabled() = BuildConfig.IS_HERMES_ENABLED
}
override fun getReactNativeHost() = mReactNativeHost
override fun onCreate() {
super.onCreate()
val sharedI18nUtilInstance = I18nUtil.getInstance()
sharedI18nUtilInstance.allowRTL(applicationContext, true)
SoLoader.init(this, /* native exopackage */ false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load()
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
// Retrieve the "donottrack" value. Default to "0" if not found.
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

@ -1,108 +0,0 @@
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 java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class WidgetUpdateWorker extends Worker {
private static final String TAG = "WidgetUpdateWorker";
private static final String PREFS_NAME = "BitcoinPriceWidgetPrefs";
private static final String PREF_PREFIX_KEY_CURRENT = "appwidget_current_";
private static final String PREF_PREFIX_KEY_PREVIOUS = "appwidget_previous_";
public WidgetUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
Context context = getApplicationContext();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, BitcoinPriceWidget.class);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
return Result.success();
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
String prevPrice = prefs.getString(PREF_PREFIX_KEY_PREVIOUS + appWidgetId, "N/A");
String currentPrice = prefs.getString(PREF_PREFIX_KEY_CURRENT + appWidgetId, "N/A");
Log.d(TAG, "Previous price: " + prevPrice);
Log.d(TAG, "Current price: " + currentPrice);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// Fetch the latest price
String newPrice = MarketAPI.fetchPrice(context, "USD");
if (newPrice != null) {
String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date());
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault());
// Update the widget views
views.setTextViewText(R.id.price_value, currencyFormat.format(Double.parseDouble(newPrice)));
views.setTextViewText(R.id.last_updated_time, currentTime);
views.setViewVisibility(R.id.last_updated_label, View.VISIBLE);
views.setViewVisibility(R.id.last_updated_time, View.VISIBLE);
if (!prevPrice.equals("N/A") && !prevPrice.equals(newPrice)) {
views.setViewVisibility(R.id.price_arrow_container, View.VISIBLE);
views.setTextViewText(R.id.previous_price, currencyFormat.format(Double.parseDouble(prevPrice)));
views.setViewVisibility(R.id.previous_price_label, View.VISIBLE);
views.setViewVisibility(R.id.previous_price, View.VISIBLE);
if (Double.parseDouble(newPrice) > Double.parseDouble(prevPrice)) {
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 {
views.setViewVisibility(R.id.price_arrow_container, View.GONE);
views.setViewVisibility(R.id.previous_price_label, View.GONE);
views.setViewVisibility(R.id.previous_price, View.GONE);
}
// Save the new price and time
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_PREFIX_KEY_PREVIOUS + appWidgetId, currentPrice);
editor.putString(PREF_PREFIX_KEY_CURRENT + appWidgetId, newPrice);
editor.apply();
Log.d(TAG, "Fetch completed with price: " + newPrice + " at " + currentTime + ". Previous price: " + prevPrice);
appWidgetManager.updateAppWidget(appWidgetId, views);
// Log the next update time
long nextUpdateTimeMillis = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(4);
String nextUpdateTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date(nextUpdateTimeMillis));
Log.d(TAG, "Next fetch scheduled at: " + nextUpdateTime);
} else {
Log.e(TAG, "Failed to fetch Bitcoin price");
views.setTextViewText(R.id.price_value, "Error");
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
public static OneTimeWorkRequest createWorkRequest() {
return new OneTimeWorkRequest.Builder(WidgetUpdateWorker.class).build();
}
}

View file

@ -0,0 +1,112 @@
package io.bluewallet.bluewallet
import android.content.Context
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.ExistingPeriodicWorkPolicy
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class WidgetUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
companion object {
private const val FETCH_INTERVAL_MINUTES = 15L
private const val PREFS_NAME = "BitcoinPriceWidgetPrefs"
private const val PREF_PREFIX_KEY = "appwidget_"
fun createWorkRequest(appWidgetId: Int): PeriodicWorkRequest {
return PeriodicWorkRequest.Builder(WidgetUpdateWorker::class.java, FETCH_INTERVAL_MINUTES, TimeUnit.MINUTES)
.addTag(appWidgetId.toString())
.build()
}
}
override fun doWork(): Result {
val appWidgetId = inputData.getInt("appWidgetId", -1)
if (appWidgetId == -1) return Result.failure()
val context = applicationContext
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val previousPrice = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null)
val previousTime = prefs.getString("${PREF_PREFIX_KEY}${appWidgetId}_time", null)
val price = MarketAPI.fetchPrice(context, "USD")
if (price != null) {
updateWidgetWithPrice(context, appWidgetId, price, previousPrice, previousTime)
Log.d("WidgetUpdateWorker", "Fetch completed with price: $price at ${getCurrentTime()}. Previous price: $previousPrice at $previousTime. Next fetch at: ${getNextFetchTime()}")
} else {
handleError(context, appWidgetId)
Log.e("WidgetUpdateWorker", "Failed to fetch Bitcoin price. Next fetch at: ${getNextFetchTime()}")
}
return Result.success()
}
private fun getCurrentTime(): String {
return SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
}
private fun getNextFetchTime(): String {
val nextFetch = System.currentTimeMillis() + FETCH_INTERVAL_MINUTES * 60 * 1000
return SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date(nextFetch))
}
private fun updateWidgetWithPrice(context: Context, appWidgetId: Int, price: String, previousPrice: String?, previousTime: String?) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.US)
views.setTextViewText(R.id.price_value, currencyFormat.format(price.toDouble()))
if (previousPrice != null && previousTime != null) {
val previousPriceValue = previousPrice.toDouble()
val currentPriceValue = price.toDouble()
if (currentPriceValue != previousPriceValue) {
views.setTextViewText(R.id.previous_price, "From ${currencyFormat.format(previousPriceValue)}")
views.setViewVisibility(R.id.price_arrow, View.VISIBLE)
views.setViewVisibility(R.id.previous_price, View.VISIBLE)
if (currentPriceValue > previousPriceValue) {
views.setImageViewResource(R.id.price_arrow, R.drawable.ic_arrow_upward)
} else {
views.setImageViewResource(R.id.price_arrow, R.drawable.ic_arrow_downward)
}
} else {
views.setViewVisibility(R.id.price_arrow, View.GONE)
views.setViewVisibility(R.id.previous_price, View.GONE)
}
} else {
views.setViewVisibility(R.id.price_arrow, View.GONE)
views.setViewVisibility(R.id.previous_price, View.GONE)
}
prefs.putString(PREF_PREFIX_KEY + appWidgetId, price)
prefs.putString("${PREF_PREFIX_KEY}${appWidgetId}_time", getCurrentTime())
prefs.apply()
views.setTextViewText(R.id.last_updated, "Last Updated")
views.setTextViewText(R.id.last_updated_time, getCurrentTime())
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun handleError(context: Context, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.widget_layout)
views.setTextViewText(R.id.price_value, "Error")
views.setViewVisibility(R.id.price_arrow, View.GONE)
views.setViewVisibility(R.id.previous_price, View.GONE)
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

View file

@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minWidth="160dp"
android:minHeight="140dp"
android:minHeight="80dp"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal|vertical"
android:minResizeWidth="160dp"
android:minResizeHeight="140dp" />
android:resizeMode="none" />