I'm building a simple information display widget. All the offline stuff works OK. How can I process an asynchronous web request. (I can process one fine in a normal activity)
I'm trying to use a Handler, based on another stackoverflow answer, but the widget hangs without any obvious error.
I've simplified my code as much as I can to make it easier to post, and it is using a simple bitcoin price index that is available to anyone, as a simple test scenario (my application will be more complex)
Manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.uk.kicktechnic.lcdclockwidget">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light"><!--android:theme="@style/Theme.LCDClockWidget"-->
<activity
android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidgetGrantSettingsActivity"
android:label="@string/title_activity_initial_grant_settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/l_c_d_clock_widget_info" />
</receiver>
<activity android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidgetConfigureActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
</application>
</manifest>
Code:
public class LCDClockWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
//views.setTextViewText(R.id.appwidget_sun, String.valueOf(appWidgetId)); //original - show widget ID
Intent intentUpdate = new Intent(context, LCDClockWidget.class);
intentUpdate.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
views.setTextViewText(R.id.appwidget_moon, "set in updateAppWidget");
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
loadBTC(context, appWidgetManager, appWidgetId);
views.setTextViewText(R.id.appwidget_moon, "set in onUpdate");
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
static void loadBTC(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
try {
Request request = new Request.Builder()
.url("https://api.coindesk.com/v1/bpi/currentprice.json")
.build();
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(context, "Error during BPI loading : "
+ e.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(Call call, Response response)
throws IOException {
final String body = response.body().string();
//original code - works in a normal activity
//runOnUiThread(new Runnable() {
// @Override
// public void run() {
// parseBpiResponse(body, context);
// }
//});
//based on a stackoverflow answer
Handler mHandler = new Handler();
mHandler.post(new Runnable(){
public void run() {
parseBpiResponse(body, context);
}
});
}
});
} catch (Exception e) {
//Toast.makeText(context, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
static void parseBpiResponse(String body, Context context) {
try {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
StringBuilder builder = new StringBuilder();
JSONObject jsonObject = new JSONObject(body);
//JSONObject timeObject = jsonObject.getJSONObject("time");
//builder.append(timeObject.getString("updated")).append("\n\n");
JSONObject bpiObject = jsonObject.getJSONObject("bpi");
JSONObject usdObject = bpiObject.getJSONObject("USD");
builder.append("$ ").append(usdObject.getString("rate")).append("\n");
JSONObject gbpObject = bpiObject.getJSONObject("GBP");
builder.append("£ ").append(gbpObject.getString("rate")).append("\n");
JSONObject euroObject = bpiObject.getJSONObject("EUR");
builder.append("€ ").append(euroObject.getString("rate")).append("\n");
views.setTextViewText(R.id.appwidget_btc, builder.toString());
} catch (Exception e) {
}
}
}
EDIT 1:
Following the advice and using the article: https://medium.com/@sambhaji2134/jobintentservice-android-example-7f58bd2720bf
I've added this (and removed any web/BTC stuff for now)
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
Toast.makeText(context, "Widget has been updated! ", Toast.LENGTH_SHORT).show();
mServiceResultReceiver = new BTCServiceResultReceiver(new Handler());
mServiceResultReceiver.setReceiver((BTCServiceResultReceiver.Receiver) this);
showDataFromBackground(context, mServiceResultReceiver);
}
}
public void showData(String data, Context context) {
//mTextView.setText(String.format("%s\n%s", mTextView.getText(), data));
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
views.setTextViewText(R.id.appwidget_btc, data);
}
public void onReceiveResult(int resultCode, Bundle resultData, Context context) {
final int SHOW_RESULT = 123;
switch (resultCode) {
case SHOW_RESULT:
if (resultData != null) {
showData(resultData.getString("data"), context);
}
break;
}
}
My hacked together ServiceResultReciever and JobIntentService is pretty much as per the article:
public class BTCServiceResultReceiver extends ResultReceiver {
private Receiver mReceiver;
/**
* Create a new ResultReceive to receive results. Your
* {@link #onReceiveResult} method will be called from the thread running
* <var>handler</var> if given, or from an arbitrary thread if null.
*
* @param handler the handler object
*/
public BTCServiceResultReceiver(Handler handler) {
super(handler);
}
public void setReceiver(Receiver receiver) {
mReceiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (mReceiver != null) {
mReceiver.onReceiveResult(resultCode, resultData);
}
}
public interface Receiver {
void onReceiveResult(int resultCode, Bundle resultData);
}
}
public class BTCJobIntentService extends JobIntentService {
private static final String TAG = JobService.class.getSimpleName();
public static final String RECEIVER = "receiver";
public static final int SHOW_RESULT = 123;
/**
* Result receiver object to send results
*/
private ResultReceiver mResultReceiver;
/**
* Unique job ID for this service.
*/
static final int DOWNLOAD_JOB_ID = 1000;
/**
* Actions download
*/
private static final String ACTION_DOWNLOAD = "action.DOWNLOAD_DATA";
static void enqueueWork(Context context, BTCServiceResultReceiver workerResultReceiver) {
Intent intent = new Intent(context, JobService.class);
intent.putExtra(RECEIVER, workerResultReceiver);
intent.setAction(ACTION_DOWNLOAD);
enqueueWork(context, JobService.class, DOWNLOAD_JOB_ID, intent);
}
@Override
protected void onHandleWork(@NonNull Intent intent) {
// We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
Log.i("SimpleJobIntentService", "Executing work: " + intent);
String label = intent.getStringExtra("label");
if (label == null) {
label = intent.toString();
}
toast("Executing: " + label);
for (int i = 0; i < 5; i++) {
Log.i("SimpleJobIntentService", "Running service " + (i + 1) + "/5 @ " + SystemClock.elapsedRealtime());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
Log.i("SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime());
}
@Override
public void onDestroy() {
super.onDestroy();
toast("All work complete");
}
@SuppressWarnings("deprecation")
final Handler mHandler = new Handler();
// Helper for showing tests
void toast(final CharSequence text) {
mHandler.post(new Runnable() {
@Override public void run() {
Toast.makeText(BTCJobIntentService.this, text, Toast.LENGTH_SHORT).show();
}
});
}
You are trying to do the work in a background thread, but there is no guarantee that your work will complete before your process terminates.
More importantly, you are updating views
in parseBpiResponse()
well after your updateAppWidget()
call. You start the network I/O but then immediately do updateAppWidget()
, and so your changes to views
occur too late.
At minimum, you need to updateAppWidget()
in parseBpiResponse()
as well.
Better yet would be to have onUpdate()
move all of this logic into a JobIntentService
, so your process is more likely to stick around long enough to complete the network I/O and request the update to the app widget.